Merge branch 'master' into refactor_proforma_import_export

This commit is contained in:
Karol
2022-09-13 22:47:50 +02:00
67 changed files with 1199 additions and 601 deletions

View File

@ -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();
});
});

View File

@ -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>");
}
}

View File

@ -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) {

View File

@ -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));

View File

@ -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() {

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
[]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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?

View File

@ -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')

View File

@ -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() });

View File

@ -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

View File

@ -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?

View File

@ -1,5 +0,0 @@
# frozen_string_literal: true
json.set! :files do
json.array! @exercise.files.visible, :content, :id
end

View File

@ -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'