Merge branch 'master' into refactor_proforma_import_export
This commit is contained in:
@ -32,4 +32,7 @@ $.jstree.defaults.core.worker = false;
|
||||
// See https://github.com/rails/jquery-ujs/issues/456 for details
|
||||
$(document).on('turbolinks:load', function(){
|
||||
$.rails.refreshCSRFTokens();
|
||||
$('.reloadCurrentPage').on('click', function() {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
|
@ -78,22 +78,12 @@ var CodeOceanEditor = {
|
||||
if ($('#output-' + index).isPresent()) {
|
||||
return $('#output-' + index);
|
||||
} else {
|
||||
var element = $('<pre class="mb-2">').attr('id', 'output-' + index);
|
||||
var element = $('<div class="mb-2 output-element">').attr('id', 'output-' + index);
|
||||
$('#output').append(element);
|
||||
return element;
|
||||
}
|
||||
},
|
||||
|
||||
findOrCreateRenderElement: function (index) {
|
||||
if ($('#render-' + index).isPresent()) {
|
||||
return $('#render-' + index);
|
||||
} else {
|
||||
var element = $('<div>').attr('id', 'render-' + index);
|
||||
$('#render').append(element);
|
||||
return element;
|
||||
}
|
||||
},
|
||||
|
||||
getCardClass: function (result) {
|
||||
if (result.file_role === 'teacher_defined_linter') {
|
||||
return 'card bg-info text-white'
|
||||
@ -648,7 +638,7 @@ var CodeOceanEditor = {
|
||||
|
||||
augmentStacktraceInOutput: function () {
|
||||
if (this.tracepositions_regex) {
|
||||
$('#output>pre').each($.proxy(function(index, element) {
|
||||
$('#output > .output-element').each($.proxy(function(index, element) {
|
||||
element = $(element)
|
||||
|
||||
const text = _.escape(element.text());
|
||||
@ -656,16 +646,11 @@ var CodeOceanEditor = {
|
||||
|
||||
let matches;
|
||||
|
||||
// Switch both lines below to enable the output of images and render <IMG/> tags.
|
||||
// Also consider `printOutput` in evaluation.js
|
||||
|
||||
// let augmented_text = element.text();
|
||||
let augmented_text = element.html();
|
||||
while (matches = this.tracepositions_regex.exec(text)) {
|
||||
const frame = $('div.frame[data-filename="' + matches[1] + '"]')
|
||||
|
||||
if (frame.length > 0) {
|
||||
// augmented_text = augmented_text.replace(new RegExp(matches[0], 'g'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
|
||||
augmented_text = augmented_text.replace(new RegExp(_.unescape(matches[0]), 'g'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
|
||||
}
|
||||
}
|
||||
|
@ -174,11 +174,6 @@ CodeOceanEditorEvaluation = {
|
||||
/**
|
||||
* Output-Logic
|
||||
*/
|
||||
renderWebsocketOutput: function (msg) {
|
||||
var element = this.findOrCreateRenderElement(0);
|
||||
element.append(msg.data);
|
||||
},
|
||||
|
||||
printWebsocketOutput: function (msg) {
|
||||
if (!msg.data || msg.data === "\r") {
|
||||
return;
|
||||
@ -189,7 +184,7 @@ CodeOceanEditorEvaluation = {
|
||||
},
|
||||
|
||||
clearOutput: function () {
|
||||
$('#output pre').remove();
|
||||
$('#output > .output-element').remove();
|
||||
CodeOceanEditorTurtle.hideCanvas();
|
||||
},
|
||||
|
||||
@ -207,43 +202,54 @@ CodeOceanEditorEvaluation = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (output.stdout !== undefined && !output.stdout.startsWith("<img")) {
|
||||
output.stdout = _.escape(output.stdout);
|
||||
const sanitizedStdout = this.sanitizeOutput(output.stdout);
|
||||
const sanitizedStderr = this.sanitizeOutput(output.stderr);
|
||||
|
||||
const element = this.findOrCreateOutputElement(index);
|
||||
const pre = $('<span>');
|
||||
|
||||
if (sanitizedStdout !== '') {
|
||||
if (colorize) {
|
||||
pre.addClass('text-success');
|
||||
}
|
||||
pre.append(sanitizedStdout)
|
||||
}
|
||||
|
||||
var element = this.findOrCreateOutputElement(index);
|
||||
// Switch all four lines below to enable the output of images and render <IMG/> tags.
|
||||
// Also consider `augmentStacktraceInOutput` in editor.js.erb
|
||||
if (!colorize) {
|
||||
if (output.stdout !== undefined && output.stdout !== '') {
|
||||
output.stdout = output.stdout.replace(this.nonPrintableRegEx, "")
|
||||
|
||||
element.append(output.stdout)
|
||||
//element.text(element.text() + output.stdout)
|
||||
if (sanitizedStderr !== '') {
|
||||
if (colorize) {
|
||||
pre.addClass('text-warning');
|
||||
} else {
|
||||
pre.append('StdErr: ');
|
||||
}
|
||||
|
||||
if (output.stderr !== undefined && output.stderr !== '') {
|
||||
output.stderr = output.stderr.replace(this.nonPrintableRegEx, "")
|
||||
|
||||
element.append('StdErr: ' + output.stderr);
|
||||
//element.text('StdErr: ' + element.text() + output.stderr);
|
||||
}
|
||||
|
||||
} else if (output.stderr) {
|
||||
output.stderr = output.stderr.replace(this.nonPrintableRegEx, "")
|
||||
|
||||
element.addClass('text-warning').append(output.stderr);
|
||||
//element.addClass('text-warning').text(element.text() + output.stderr);
|
||||
this.QaApiOutputBuffer.stderr += output.stderr;
|
||||
} else if (output.stdout) {
|
||||
output.stdout = output.stdout.replace(this.nonPrintableRegEx, "")
|
||||
|
||||
element.addClass('text-success').append(output.stdout);
|
||||
//element.addClass('text-success').text(element.text() + output.stdout);
|
||||
this.QaApiOutputBuffer.stdout += output.stdout;
|
||||
} else {
|
||||
element.addClass('text-muted').text($('#output').data('message-no-output'));
|
||||
pre.append(sanitizedStderr);
|
||||
}
|
||||
|
||||
if (sanitizedStdout === '' && sanitizedStderr === '') {
|
||||
if (colorize) {
|
||||
pre.addClass('text-muted');
|
||||
}
|
||||
pre.text($('#output').data('message-no-output'))
|
||||
}
|
||||
|
||||
element.append(pre);
|
||||
},
|
||||
|
||||
sanitizeOutput: function (rawContent) {
|
||||
let sanitizedContent = _.escape(rawContent).replace(this.nonPrintableRegEx, "");
|
||||
|
||||
if (rawContent !== undefined && rawContent.trim().startsWith("<img")) {
|
||||
const doc = new DOMParser().parseFromString(rawContent, "text/html");
|
||||
// Get the parsed element, it is automatically wrapped in a <html><body> document
|
||||
const parsedElement = doc.firstChild.lastChild.firstChild;
|
||||
|
||||
if (parsedElement.src.startsWith("data:image")) {
|
||||
const sanitizedImg = document.createElement('img');
|
||||
sanitizedImg.src = parsedElement.src;
|
||||
sanitizedContent = sanitizedImg.outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitizedContent;
|
||||
},
|
||||
|
||||
getDeadlineInformation: function(deadline, translation_key, otherwise) {
|
||||
|
@ -44,7 +44,7 @@ CodeOceanEditorWebsocket = {
|
||||
this.websocket.on('clear', this.clearOutput.bind(this));
|
||||
this.websocket.on('turtle', this.handleTurtleCommand.bind(this));
|
||||
this.websocket.on('turtlebatch', this.handleTurtlebatchCommand.bind(this));
|
||||
this.websocket.on('render', this.renderWebsocketOutput.bind(this));
|
||||
this.websocket.on('render', this.printWebsocketOutput.bind(this));
|
||||
this.websocket.on('exit', this.handleExitCommand.bind(this));
|
||||
this.websocket.on('status', this.showStatus.bind(this));
|
||||
this.websocket.on('hint', this.showHint.bind(this));
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
renderPagedown = function() {
|
||||
$(".wmd-output").each(function (i) {
|
||||
const converter = new Markdown.Converter();
|
||||
const converter = Markdown.getSanitizingConverter();
|
||||
const content = $(this).html();
|
||||
return $(this).html(converter.makeHtml(content));
|
||||
})
|
||||
@ -20,7 +20,7 @@ createPagedownEditor = function( selector, context ) {
|
||||
return;
|
||||
}
|
||||
const attr = $(input).attr('id').split('wmd-input')[1];
|
||||
const converter = new Markdown.Converter();
|
||||
const converter = Markdown.getSanitizingConverter();
|
||||
Markdown.Extra.init(converter);
|
||||
const help = {
|
||||
handler() {
|
||||
|
@ -25,7 +25,7 @@ i.fa-solid, i.fa-regular, i.fa-solid {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
pre {
|
||||
pre, .output-element {
|
||||
background-color: #FAFAFA;
|
||||
margin: 0;
|
||||
padding: .25rem!important;
|
||||
|
@ -20,6 +20,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
#content-left-sidebar, #content-right-sidebar {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.frame {
|
||||
display: none;
|
||||
@ -77,20 +80,13 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#outputInformation {
|
||||
#output {
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
#output {
|
||||
white-space: pre;
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 14px;
|
||||
|
||||
.output-element {
|
||||
overflow: auto;
|
||||
margin: 2em 0;
|
||||
|
||||
p {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
pre + pre {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
|
||||
after_action :verify_authorized, except: %i[welcome]
|
||||
around_action :mnemosyne_trace
|
||||
around_action :switch_locale
|
||||
before_action :set_sentry_context, :allow_iframe_requests, :load_embed_options
|
||||
before_action :set_sentry_context, :load_embed_options
|
||||
protect_from_forgery(with: :exception, prepend: true)
|
||||
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error
|
||||
@ -40,7 +40,10 @@ class ApplicationController < ActionController::Base
|
||||
token = AuthenticationToken.find_by(shared_secret: params[:token])
|
||||
return unless token
|
||||
|
||||
auto_login(token.user) if token.expire_at.future?
|
||||
if token.expire_at.future?
|
||||
token.update(expire_at: Time.zone.now)
|
||||
auto_login(token.user)
|
||||
end
|
||||
end
|
||||
|
||||
def set_sentry_context
|
||||
@ -93,10 +96,6 @@ class ApplicationController < ActionController::Base
|
||||
# Show root page
|
||||
end
|
||||
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options')
|
||||
end
|
||||
|
||||
def load_embed_options
|
||||
@embed_options = if session[:embed_options].present? && session[:embed_options].is_a?(Hash)
|
||||
session[:embed_options].symbolize_keys
|
||||
|
@ -10,6 +10,15 @@ module CodeOcean
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def show_protected_upload
|
||||
@file = CodeOcean::File.find(params[:id])
|
||||
authorize!
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download] || @file.name_with_extension != params[:filename]
|
||||
|
||||
real_location = Pathname(@file.native_file.current_path).realpath
|
||||
send_file(real_location, type: @file.native_file.content_type, filename: @file.name_with_extension, disposition: 'attachment')
|
||||
end
|
||||
|
||||
def create
|
||||
@file = CodeOcean::File.new(file_params)
|
||||
if @file.file_template_id
|
||||
|
@ -5,8 +5,14 @@ module FileParameters
|
||||
if exercise && params
|
||||
params.reject do |_, file_attributes|
|
||||
file = CodeOcean::File.find_by(id: file_attributes[:file_id])
|
||||
next true if file.nil? || file.hidden || file.read_only
|
||||
# avoid that public files from other contexts can be created
|
||||
file.nil? || file.hidden || file.read_only || (file.context_type == 'Exercise' && file.context_id != exercise.id) || (file.context_type == 'CommunitySolution' && controller_name != 'community_solutions')
|
||||
# `next` is similar to an early return and will proceed with the next iteration of the loop
|
||||
next true if file.context_type == 'Exercise' && file.context_id != exercise.id
|
||||
next true if file.context_type == 'Submission' && file.context.user != current_user
|
||||
next true if file.context_type == 'CommunitySolution' && controller_name != 'community_solutions'
|
||||
|
||||
false
|
||||
end
|
||||
else
|
||||
[]
|
||||
|
@ -22,6 +22,8 @@ class ExercisesController < ApplicationController
|
||||
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check], raise: false
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise
|
||||
|
||||
def authorize!
|
||||
authorize(@exercise || @exercises)
|
||||
end
|
||||
@ -294,8 +296,6 @@ class ExercisesController < ApplicationController
|
||||
private :update_exercise_tips
|
||||
|
||||
def implement
|
||||
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO: TESTESTEST
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
user_solved_exercise = @exercise.solved_by?(current_user)
|
||||
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?',
|
||||
Time.zone.now.beginning_of_day).count
|
||||
@ -324,6 +324,8 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
@embed_options[:disable_score] = true unless @exercise.teacher_defined_assessment?
|
||||
|
||||
@hide_rfc_button = @embed_options[:disable_rfc]
|
||||
|
||||
@search = Search.new
|
||||
@ -432,6 +434,19 @@ class ExercisesController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def not_authorized_for_exercise(_exception)
|
||||
return render_not_authorized unless current_user
|
||||
return render_not_authorized unless %w[implement working_times intervention search reload].include?(action_name)
|
||||
|
||||
if current_user.admin? || current_user.teacher?
|
||||
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished?
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
else
|
||||
render_not_authorized
|
||||
end
|
||||
end
|
||||
private :not_authorized_for_exercise
|
||||
|
||||
def set_execution_environments
|
||||
@execution_environments = ExecutionEnvironment.all.order(:name)
|
||||
end
|
||||
|
@ -9,7 +9,7 @@ class ExternalUsersController < ApplicationController
|
||||
private :authorize!
|
||||
|
||||
def index
|
||||
@search = ExternalUser.ransack(params[:q])
|
||||
@search = ExternalUser.ransack(params[:q], {auth_object: current_user})
|
||||
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
@ -43,15 +43,15 @@ class ExternalUsersController < ApplicationController
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE user_id = #{@user.id}
|
||||
WHERE #{ExternalUser.sanitize_sql(['user_id = ?', @user.id])}
|
||||
AND user_type = 'ExternalUser'
|
||||
#{current_user.admin? ? '' : "AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"}
|
||||
#{current_user.admin? ? '' : "AND #{ExternalUser.sanitize_sql(['study_group_id IN (?)', current_user.study_groups.pluck(:id)])} AND cause = 'submit'"}
|
||||
GROUP BY exercise_id,
|
||||
user_id,
|
||||
id
|
||||
) AS foo
|
||||
) AS bar
|
||||
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND et.tag_id = #{tag} "}
|
||||
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND #{ExternalUser.sanitize_sql(['et.tag_id = ?', tag])}"}
|
||||
GROUP BY user_id,
|
||||
bar.exercise_id;
|
||||
"
|
||||
@ -60,10 +60,14 @@ class ExternalUsersController < ApplicationController
|
||||
def statistics
|
||||
@user = ExternalUser.find(params[:id])
|
||||
authorize!
|
||||
if params[:tag].present?
|
||||
tag = Tag.find(params[:tag])
|
||||
authorize(tag, :show?)
|
||||
end
|
||||
|
||||
statistics = {}
|
||||
|
||||
ApplicationRecord.connection.execute(working_time_query(params[:tag])).each do |tuple|
|
||||
ApplicationRecord.connection.execute(working_time_query(tag&.id)).each do |tuple|
|
||||
statistics[tuple['exercise_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
|
@ -67,7 +67,7 @@ class InternalUsersController < ApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
@search = InternalUser.ransack(params[:q])
|
||||
@search = InternalUser.ransack(params[:q], {auth_object: current_user})
|
||||
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
@ -70,12 +70,9 @@ class ProxyExercisesController < ApplicationController
|
||||
|
||||
def show
|
||||
@search = @proxy_exercise.exercises.ransack
|
||||
@exercises = @proxy_exercise.exercises.ransack.result.order(:title) # @search.result.order(:title)
|
||||
@exercises = @proxy_exercise.exercises.ransack.result.order(:title)
|
||||
end
|
||||
|
||||
# we might want to think about auth here
|
||||
def reload; end
|
||||
|
||||
def update
|
||||
myparams = proxy_exercise_params
|
||||
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].compact_blank)
|
||||
|
@ -12,7 +12,17 @@ class SubmissionsController < ApplicationController
|
||||
before_action :set_testrun, only: %i[run score test]
|
||||
before_action :set_files, only: %i[download show]
|
||||
before_action :set_files_and_specific_file, only: %i[download_file render_file run test]
|
||||
before_action :set_mime_type, only: %i[download_file render_file]
|
||||
before_action :set_content_type_nosniff, only: %i[download download_file render_file]
|
||||
|
||||
# Overwrite the CSP header for the :render_file action
|
||||
content_security_policy only: :render_file do |policy|
|
||||
policy.img_src :none
|
||||
policy.script_src :none
|
||||
policy.font_src :none
|
||||
policy.style_src :none
|
||||
policy.connect_src :none
|
||||
policy.form_action :none
|
||||
end
|
||||
|
||||
def create
|
||||
@submission = Submission.new(submission_params)
|
||||
@ -56,7 +66,11 @@ class SubmissionsController < ApplicationController
|
||||
def download_file
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
|
||||
|
||||
send_data(@file.read, filename: @file.name_with_extension)
|
||||
if @file.native_file?
|
||||
redirect_to protected_upload_path(id: @file.id, filename: @file.name_with_extension)
|
||||
else
|
||||
send_data(@file.content, filename: @file.name_with_extension, disposition: 'attachment')
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@ -66,13 +80,13 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def render_file
|
||||
if @file.native_file?
|
||||
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
|
||||
else
|
||||
render(plain: @file.content)
|
||||
end
|
||||
# If a file should not be downloaded, it should not be rendered either
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
|
||||
|
||||
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def run
|
||||
# These method-local socket variables are required in order to use one socket
|
||||
# in the callbacks of the other socket. As the callbacks for the client socket
|
||||
@ -83,7 +97,7 @@ class SubmissionsController < ApplicationController
|
||||
client_socket = tubesock
|
||||
|
||||
client_socket.onopen do |_event|
|
||||
kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
return kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
end
|
||||
|
||||
client_socket.onclose do |_event|
|
||||
@ -167,7 +181,8 @@ class SubmissionsController < ApplicationController
|
||||
@testrun[:status] = :failed
|
||||
"\n#{t('exercises.implement.exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}"
|
||||
end
|
||||
send_and_store client_socket, {cmd: :write, stream: :stdout, data: "#{exit_statement}\n"}
|
||||
stream = @testrun[:status] == :ok ? :stdout : :stderr
|
||||
send_and_store client_socket, {cmd: :write, stream: stream, data: "#{exit_statement}\n"}
|
||||
if exit_code == 137
|
||||
send_and_store client_socket, {cmd: :status, status: :out_of_memory}
|
||||
@testrun[:status] = :out_of_memory
|
||||
@ -194,12 +209,13 @@ class SubmissionsController < ApplicationController
|
||||
ensure
|
||||
save_testrun_output 'run'
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity:
|
||||
|
||||
def score
|
||||
hijack do |tubesock|
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_score]
|
||||
return kill_client_socket(tubesock) if @embed_options[:disable_score] || !@submission.exercise.teacher_defined_assessment?
|
||||
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.calculate_score))
|
||||
@ -226,7 +242,7 @@ class SubmissionsController < ApplicationController
|
||||
hijack do |tubesock|
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
return kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.test(@file)))
|
||||
@ -363,9 +379,9 @@ class SubmissionsController < ApplicationController
|
||||
@files = @submission.collect_files.select(&:visible)
|
||||
end
|
||||
|
||||
def set_mime_type
|
||||
@mime_type = Mime::Type.lookup_by_extension(@file.file_type.file_extension.gsub(/^\./, ''))
|
||||
response.headers['Content-Type'] = @mime_type.to_s
|
||||
def set_content_type_nosniff
|
||||
# When sending a file, we want to ensure that browsers follow our Content-Type header
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
end
|
||||
|
||||
def set_submission
|
||||
|
@ -74,7 +74,7 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
|
||||
def update
|
||||
submission = begin
|
||||
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').final.first
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
@ -127,14 +127,16 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
user_type = current_user.class.name
|
||||
latest_submission = Submission
|
||||
.where(user_id: user_id, user_type: user_type, exercise_id: exercise_id)
|
||||
.order(created_at: :desc).first
|
||||
.order(created_at: :desc).final.first
|
||||
|
||||
authorize(latest_submission, :show?)
|
||||
|
||||
params[:user_exercise_feedback]
|
||||
.permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime)
|
||||
.merge(user_id: user_id,
|
||||
user_type: user_type,
|
||||
submission: latest_submission,
|
||||
normalized_score: latest_submission.normalized_score)
|
||||
normalized_score: latest_submission&.normalized_score)
|
||||
end
|
||||
|
||||
def validate_inputs(uef_params)
|
||||
|
@ -8,7 +8,7 @@ module ExerciseHelper
|
||||
end
|
||||
|
||||
def qa_js_tag
|
||||
javascript_include_tag "#{qa_url}/assets/qa_api.js"
|
||||
javascript_include_tag "#{qa_url}/assets/qa_api.js", integrity: true, crossorigin: 'anonymous'
|
||||
end
|
||||
|
||||
def qa_url
|
||||
|
@ -23,7 +23,7 @@ class UserMailer < ApplicationMailer
|
||||
token = AuthenticationToken.generate!(request_for_comment.user)
|
||||
@receiver_displayname = request_for_comment.user.displayname
|
||||
@commenting_user_displayname = commenting_user.displayname
|
||||
@comment_text = comment.text
|
||||
@comment_text = ERB::Util.html_escape comment.text
|
||||
@rfc_link = request_for_comment_url(request_for_comment, token: token.shared_secret)
|
||||
mail(
|
||||
subject: t('mailers.user_mailer.got_new_comment.subject',
|
||||
@ -35,7 +35,7 @@ class UserMailer < ApplicationMailer
|
||||
token = AuthenticationToken.generate!(subscription.user)
|
||||
@receiver_displayname = subscription.user.displayname
|
||||
@author_displayname = from_user.displayname
|
||||
@comment_text = comment.text
|
||||
@comment_text = ERB::Util.html_escape comment.text
|
||||
@rfc_link = request_for_comment_url(subscription.request_for_comment, token: token.shared_secret)
|
||||
@unsubscribe_link = unsubscribe_subscription_url(subscription)
|
||||
mail(
|
||||
@ -45,10 +45,10 @@ class UserMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
def send_thank_you_note(request_for_comment, receiver)
|
||||
token = AuthenticationToken.generate!(request_for_comment.user)
|
||||
token = AuthenticationToken.generate!(receiver)
|
||||
@receiver_displayname = receiver.displayname
|
||||
@author = request_for_comment.user.displayname
|
||||
@thank_you_note = request_for_comment.thank_you_note
|
||||
@thank_you_note = ERB::Util.html_escape request_for_comment.thank_you_note
|
||||
@rfc_link = request_for_comment_url(request_for_comment, token: token.shared_secret)
|
||||
mail(subject: t('mailers.user_mailer.send_thank_you_note.subject', author: @author), to: receiver.email)
|
||||
end
|
||||
|
@ -4,6 +4,7 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
|
||||
before_validation :strip_strings
|
||||
before_validation :remove_null_bytes
|
||||
|
||||
def strip_strings
|
||||
# trim whitespace from beginning and end of string attributes
|
||||
@ -16,6 +17,15 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def remove_null_bytes
|
||||
# remove null bytes from string attributes
|
||||
attribute_names.each do |name|
|
||||
if send(name.to_sym).respond_to?(:tr)
|
||||
send("#{name}=".to_sym, send(name).tr("\0", ''))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.ransackable_associations(_auth_object = nil)
|
||||
[]
|
||||
end
|
||||
|
@ -58,8 +58,7 @@ module CodeOcean
|
||||
|
||||
def read
|
||||
if native_file?
|
||||
valid = Pathname(native_file.current_path).fnmatch? ::File.join(native_file.root, '**')
|
||||
return nil unless valid
|
||||
return nil unless native_file_location_valid?
|
||||
|
||||
native_file.read
|
||||
else
|
||||
@ -67,6 +66,12 @@ module CodeOcean
|
||||
end
|
||||
end
|
||||
|
||||
def native_file_location_valid?
|
||||
real_location = Pathname(native_file.current_path).realpath
|
||||
upload_location = Pathname(::File.join(native_file.root, 'uploads')).realpath
|
||||
real_location.fnmatch? ::File.join(upload_location.to_s, '**')
|
||||
end
|
||||
|
||||
def ancestor_id
|
||||
file_id || id
|
||||
end
|
||||
|
@ -205,6 +205,10 @@ class Exercise < ApplicationRecord
|
||||
"
|
||||
end
|
||||
|
||||
def teacher_defined_assessment?
|
||||
files.any?(&:teacher_defined_assessment?)
|
||||
end
|
||||
|
||||
def get_working_times_for_study_group(study_group_id, user = nil)
|
||||
user_progress = []
|
||||
additional_user_data = []
|
||||
@ -251,7 +255,6 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = self.class.sanitize_sql("[#{quantiles.join(',')}]")
|
||||
result = ActiveRecord::Base.transaction do
|
||||
self.class.connection.execute("
|
||||
SET LOCAL intervalstyle = 'iso_8601';
|
||||
@ -358,7 +361,7 @@ class Exercise < ApplicationRecord
|
||||
GROUP BY e.external_id,
|
||||
f.user_id,
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||
SELECT unnest(percentile_cont(#{self.class.sanitize_sql(['array[?]', quantiles])}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
")
|
||||
end
|
||||
|
@ -42,7 +42,11 @@ class User < ApplicationRecord
|
||||
displayname
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[name email external_id consumer_id role]
|
||||
def self.ransackable_attributes(auth_object)
|
||||
if auth_object.admin?
|
||||
%w[name email external_id consumer_id role]
|
||||
else
|
||||
%w[name external_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -7,6 +7,8 @@ module CodeOcean
|
||||
end
|
||||
|
||||
def show?
|
||||
return false if @record.native_file? && !@record.native_file_location_valid?
|
||||
|
||||
if @record.context.is_a?(Exercise)
|
||||
admin? || author? || !@record.hidden
|
||||
else
|
||||
@ -14,6 +16,16 @@ module CodeOcean
|
||||
end
|
||||
end
|
||||
|
||||
def show_protected_upload?
|
||||
return false if @record.native_file? && !@record.native_file_location_valid?
|
||||
|
||||
if @record.context.is_a?(Exercise)
|
||||
admin? || author? || (!@record.context.unpublished && !@record.hidden)
|
||||
else
|
||||
admin? || author?
|
||||
end
|
||||
end
|
||||
|
||||
def create?
|
||||
if @record.context.is_a?(Exercise)
|
||||
admin? || author?
|
||||
|
@ -29,8 +29,16 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
||||
define_method(action) { (admin? || teacher_in_study_group? || author?) && @user.codeharbor_link }
|
||||
end
|
||||
|
||||
%i[implement? working_times? intervention? search? submit? reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
%i[implement? working_times? intervention? search? reload?].each do |action|
|
||||
define_method(action) do
|
||||
return no_one unless @record.files.visible.exists?
|
||||
|
||||
admin? || teacher_in_study_group? || author? || (everyone && !@record.unpublished?)
|
||||
end
|
||||
end
|
||||
|
||||
def submit?
|
||||
everyone && @record.teacher_defined_assessment?
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
@ -39,8 +47,8 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
||||
@scope.all
|
||||
elsif @user.teacher?
|
||||
@scope.where(
|
||||
'user_id IN (SELECT user_id FROM study_group_memberships WHERE study_group_id IN (?))
|
||||
OR (user_id = ? AND user_type = ?)
|
||||
'exercises.user_id IN (SELECT user_id FROM study_group_memberships WHERE study_group_id IN (?))
|
||||
OR (exercises.user_id = ? AND exercises.user_type = ?)
|
||||
OR public = TRUE',
|
||||
@user.study_groups.pluck(:id),
|
||||
@user.id, @user.class.name
|
||||
|
@ -13,10 +13,6 @@ class ProxyExercisePolicy < AdminOrAuthorPolicy
|
||||
define_method(action) { admin? || author? }
|
||||
end
|
||||
|
||||
[:reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if @user.admin?
|
||||
|
@ -79,7 +79,7 @@ div.d-grid id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-botto
|
||||
.heading = t('exercises.implement.error_hints.heading')
|
||||
ul.body.mb-0
|
||||
#output
|
||||
pre.overflow-scroll = t('exercises.implement.no_output_yet')
|
||||
.output-element.overflow-scroll = t('exercises.implement.no_output_yet')
|
||||
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] && !@embed_options[:disable_hints] && !@embed_options[:hide_test_results]
|
||||
#flowrHint.mb-2.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
|
||||
.card-header = t('exercises.implement.flowr.heading')
|
||||
|
@ -25,7 +25,7 @@ h1 = link_to_if(policy(@exercise).show?, @exercise, exercise_path(@exercise))
|
||||
span.date = feedback.created_at
|
||||
.card-collapse role="tabpanel"
|
||||
.card-body.feedback
|
||||
.text = render_markdown(feedback.feedback_text)
|
||||
.text style="white-space: pre-wrap;" = feedback.feedback_text
|
||||
.difficulty = "#{t('user_exercise_feedback.difficulty')} #{comment_presets[feedback.difficulty].join(' - ')}" if feedback.difficulty
|
||||
.worktime = "#{t('user_exercise_feedback.working_time')} #{time_presets[feedback.user_estimated_worktime].join(' - ')}" if feedback.user_estimated_worktime
|
||||
- if policy(@exercise).detailed_statistics?
|
||||
@ -36,4 +36,5 @@ h1 = link_to_if(policy(@exercise).show?, @exercise, exercise_path(@exercise))
|
||||
|
||||
= render('shared/pagination', collection: @feedbacks)
|
||||
|
||||
script type="text/javascript" $(function () { $('[data-bs-toggle="tooltip"]').tooltip() });
|
||||
= javascript_tag nonce: true do
|
||||
| $(function () { $('[data-bs-toggle="tooltip"]').tooltip() });
|
||||
|
@ -1,24 +1,32 @@
|
||||
h1 = ExternalUser.model_name.human(count: 2)
|
||||
|
||||
= render(layout: 'shared/form_filters') do |f|
|
||||
.col-md-9.col
|
||||
.row.align-items-center
|
||||
.col
|
||||
= f.label(:name_cont, t('activerecord.attributes.external_user.name'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:name_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.name'))
|
||||
.col.mt-0.mt-sm-3.mt-md-0
|
||||
= f.label(:email_cont, t('activerecord.attributes.external_user.email'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:email_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.email'))
|
||||
.col.mt-3.mt-lg-0
|
||||
= f.label(:external_id_cont, t('activerecord.attributes.external_user.external_id'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:external_id_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.external_id'))
|
||||
.row
|
||||
.col-auto
|
||||
= f.label(:role_eq, t('activerecord.attributes.external_user.role'), class: 'visually-hidden form-label')
|
||||
= f.select(:role_eq, User::ROLES.map { |role| [t("users.roles.#{role}"), role] }, { include_blank: true }, class: 'form-control', prompt: t('activerecord.attributes.external_user.role'))
|
||||
.col-auto.mt-3.mt-lg-0
|
||||
= f.label(:consumer_id_eq, t('activerecord.attributes.external_user.consumer'), class: 'visually-hidden form-label')
|
||||
= f.collection_select(:consumer_id_eq, Consumer.with_external_users, :id, :name, class: 'form-control', prompt: t('activerecord.attributes.external_user.consumer'))
|
||||
- if current_user.admin?
|
||||
.col-md-9.col
|
||||
.row.align-items-center
|
||||
.col
|
||||
= f.label(:name_cont, t('activerecord.attributes.external_user.name'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:name_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.name'))
|
||||
.col.mt-0.mt-sm-3.mt-md-0
|
||||
= f.label(:email_cont, t('activerecord.attributes.external_user.email'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:email_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.email'))
|
||||
.col.mt-3.mt-lg-0
|
||||
= f.label(:external_id_cont, t('activerecord.attributes.external_user.external_id'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:external_id_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.external_id'))
|
||||
.row
|
||||
.col-auto
|
||||
= f.label(:role_eq, t('activerecord.attributes.external_user.role'), class: 'visually-hidden form-label')
|
||||
= f.select(:role_eq, User::ROLES.map { |role| [t("users.roles.#{role}"), role] }, { include_blank: true }, class: 'form-control', prompt: t('activerecord.attributes.external_user.role'))
|
||||
.col-auto.mt-3.mt-lg-0
|
||||
= f.label(:consumer_id_eq, t('activerecord.attributes.external_user.consumer'), class: 'visually-hidden form-label')
|
||||
= f.collection_select(:consumer_id_eq, Consumer.with_external_users, :id, :name, class: 'form-control', prompt: t('activerecord.attributes.external_user.consumer'))
|
||||
- else
|
||||
.col-auto
|
||||
= f.label(:name_cont, t('activerecord.attributes.external_user.name'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:name_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.name'))
|
||||
.col-auto
|
||||
= f.label(:external_id_cont, t('activerecord.attributes.external_user.external_id'), class: 'visually-hidden form-label')
|
||||
= f.search_field(:external_id_cont, class: 'form-control', placeholder: t('activerecord.attributes.external_user.external_id'))
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
|
@ -9,14 +9,14 @@ html lang="#{I18n.locale || I18n.default_locale}"
|
||||
= favicon_link_tag('/favicon.png', type: 'image/png')
|
||||
= favicon_link_tag('/favicon.png', rel: 'apple-touch-icon', type: 'image/png')
|
||||
= action_cable_meta_tag
|
||||
= stylesheet_pack_tag('application', 'stylesheets', media: 'all', 'data-turbolinks-track': true)
|
||||
= stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': true)
|
||||
= javascript_pack_tag('application', 'data-turbolinks-track': true, defer: false)
|
||||
= javascript_include_tag('application', 'data-turbolinks-track': true)
|
||||
= stylesheet_pack_tag('application', 'stylesheets', media: 'all', 'data-turbolinks-track': true, integrity: true, crossorigin: 'anonymous')
|
||||
= stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': true, integrity: true, crossorigin: 'anonymous')
|
||||
= javascript_pack_tag('application', 'data-turbolinks-track': true, defer: false, integrity: true, crossorigin: 'anonymous')
|
||||
= javascript_include_tag('application', 'data-turbolinks-track': true, integrity: true, crossorigin: 'anonymous')
|
||||
= yield(:head)
|
||||
= csrf_meta_tags
|
||||
= timeago_script_tag
|
||||
script type="text/javascript"
|
||||
= timeago_script_tag nonce: true
|
||||
= javascript_tag nonce: true do
|
||||
| I18n.defaultLocale = "#{I18n.default_locale}";
|
||||
| I18n.locale = "#{I18n.locale}";
|
||||
- if SentryJavascript.active?
|
||||
|
@ -1,5 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.set! :files do
|
||||
json.array! @exercise.files.visible, :content, :id
|
||||
end
|
@ -79,7 +79,7 @@
|
||||
|
||||
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent')
|
||||
|
||||
javascript:
|
||||
javascript [nonce=content_security_policy_nonce]:
|
||||
|
||||
$('.modal-content').draggable({
|
||||
handle: '.modal-header'
|
||||
|
Reference in New Issue
Block a user