Merge branch 'master' into add_roles_via_LTI
# Conflicts: # app/views/application/_breadcrumbs.html.slim # app/views/application/welcome.html.slim # app/views/exercise_collections/show.html.slim # app/views/external_users/index.html.slim # app/views/layouts/application.html.slim # app/views/proxy_exercises/index.html.slim # app/views/user_exercise_feedbacks/index.html.slim # app/views/user_mailer/send_thank_you_note.slim
This commit is contained in:
@ -11,7 +11,7 @@ window.CodeOcean = {
|
|||||||
var ANIMATION_DURATION = 500;
|
var ANIMATION_DURATION = 500;
|
||||||
|
|
||||||
$.isController = function(name) {
|
$.isController = function(name) {
|
||||||
return $('.container[data-controller="' + name + '"]').isPresent();
|
return $('div[data-controller="' + name + '"]').isPresent();
|
||||||
};
|
};
|
||||||
|
|
||||||
$.fn.isPresent = function() {
|
$.fn.isPresent = function() {
|
||||||
|
@ -110,6 +110,10 @@ configureEditors: function () {
|
|||||||
|
|
||||||
// The event ready.jstree is fired too early and thus doesn't work.
|
// The event ready.jstree is fired too early and thus doesn't work.
|
||||||
selectFileInJsTree: function(filetree, file_id) {
|
selectFileInJsTree: function(filetree, file_id) {
|
||||||
|
if (!filetree.is(':visible'))
|
||||||
|
// The left sidebar is not shown and thus the filetree is not rendered.
|
||||||
|
return;
|
||||||
|
|
||||||
if (!filetree.hasClass('jstree-loading')) {
|
if (!filetree.hasClass('jstree-loading')) {
|
||||||
filetree.jstree("deselect_all");
|
filetree.jstree("deselect_all");
|
||||||
filetree.jstree().select_node(file_id);
|
filetree.jstree().select_node(file_id);
|
||||||
@ -224,6 +228,11 @@ configureEditors: function () {
|
|||||||
// remove last (empty) that is there by default line
|
// remove last (empty) that is there by default line
|
||||||
document.removeLines(document.getLength() - 1, document.getLength() - 1);
|
document.removeLines(document.getLength() - 1, document.getLength() - 1);
|
||||||
editor.setReadOnly($(element).data('read-only') !== undefined);
|
editor.setReadOnly($(element).data('read-only') !== undefined);
|
||||||
|
if (editor.getReadOnly()) {
|
||||||
|
editor.setHighlightActiveLine(false);
|
||||||
|
editor.setHighlightGutterLine(false);
|
||||||
|
editor.renderer.$cursorLayer.element.style.opacity = 0;
|
||||||
|
}
|
||||||
editor.setShowPrintMargin(false);
|
editor.setShowPrintMargin(false);
|
||||||
editor.setTheme(this.THEME);
|
editor.setTheme(this.THEME);
|
||||||
|
|
||||||
@ -458,7 +467,7 @@ configureEditors: function () {
|
|||||||
|
|
||||||
var editor = this.editor_for_file.get(file);
|
var editor = this.editor_for_file.get(file);
|
||||||
editor.gotoLine(line, 0);
|
editor.gotoLine(line, 0);
|
||||||
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
|
|
||||||
augmentStacktraceInOutput: function () {
|
augmentStacktraceInOutput: function () {
|
||||||
@ -722,6 +731,7 @@ configureEditors: function () {
|
|||||||
this.initPrompt();
|
this.initPrompt();
|
||||||
this.renderScore();
|
this.renderScore();
|
||||||
this.showFirstFile();
|
this.showFirstFile();
|
||||||
|
this.resizeAceEditors();
|
||||||
|
|
||||||
$(window).on("beforeunload", this.unloadAutoSave.bind(this));
|
$(window).on("beforeunload", this.unloadAutoSave.bind(this));
|
||||||
// create autosave when the editor is opened the first time
|
// create autosave when the editor is opened the first time
|
||||||
|
@ -27,8 +27,11 @@ CodeOceanEditorEvaluation = {
|
|||||||
printScoringResult: function (result, index) {
|
printScoringResult: function (result, index) {
|
||||||
$('#results').show();
|
$('#results').show();
|
||||||
var card = $('#dummies').children().first().clone();
|
var card = $('#dummies').children().first().clone();
|
||||||
this.populateCard(card, result, index);
|
if (card.isPresent()) {
|
||||||
$('#results ul').first().append(card);
|
// the card won't be present if @embed_options[:hide_test_results] == true
|
||||||
|
this.populateCard(card, result, index);
|
||||||
|
$('#results ul').first().append(card);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
printScoringResults: function (response) {
|
printScoringResults: function (response) {
|
||||||
@ -141,14 +144,19 @@ CodeOceanEditorEvaluation = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
printOutput: function (output, colorize, index) {
|
printOutput: function (output, colorize, index) {
|
||||||
|
if (output.stderr === undefined && output.stdout === undefined) {
|
||||||
|
// Prevent empty element with no text at all
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var element = this.findOrCreateOutputElement(index);
|
var element = this.findOrCreateOutputElement(index);
|
||||||
if (!colorize) {
|
if (!colorize) {
|
||||||
if (output.stdout != undefined && output.stdout != '') {
|
if (output.stdout !== undefined && output.stdout !== '') {
|
||||||
//element.append(output.stdout)
|
//element.append(output.stdout)
|
||||||
element.text(element.text() + output.stdout)
|
element.text(element.text() + output.stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.stderr != undefined && output.stderr != '') {
|
if (output.stderr !== undefined && output.stderr !== '') {
|
||||||
//element.append('StdErr: ' + output.stderr);
|
//element.append('StdErr: ' + output.stderr);
|
||||||
element.text('StdErr: ' + element.text() + output.stderr);
|
element.text('StdErr: ' + element.text() + output.stderr);
|
||||||
}
|
}
|
||||||
|
@ -172,8 +172,9 @@ CodeOceanEditorRequestForComments = {
|
|||||||
this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
|
this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
|
||||||
|
|
||||||
$('#comment-modal').modal('hide');
|
$('#comment-modal').modal('hide');
|
||||||
|
$('#question').val('');
|
||||||
// we disabled the button to prevent that the user spams RFCs, but decided against this now.
|
// we disabled the button to prevent that the user spams RFCs, but decided against this now.
|
||||||
//var button = $('#requestComments');
|
//var button = $('#requestComments');
|
||||||
//button.prop('disabled', true);
|
//button.prop('disabled', true);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ CodeOceanEditorSubmissions = {
|
|||||||
*/
|
*/
|
||||||
createSubmission: function (initiator, filter, callback) {
|
createSubmission: function (initiator, filter, callback) {
|
||||||
this.showSpinner(initiator);
|
this.showSpinner(initiator);
|
||||||
|
var url = $(initiator).data('url') || $('#editor').data('submissions-url');
|
||||||
var jqxhr = this.ajax({
|
var jqxhr = this.ajax({
|
||||||
data: {
|
data: {
|
||||||
submission: {
|
submission: {
|
||||||
@ -21,7 +22,7 @@ CodeOceanEditorSubmissions = {
|
|||||||
},
|
},
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: $(initiator).data('url') || $('#editor').data('submissions-url')
|
url: url + '.json'
|
||||||
});
|
});
|
||||||
jqxhr.always(this.hideSpinner.bind(this));
|
jqxhr.always(this.hideSpinner.bind(this));
|
||||||
jqxhr.done(this.createSubmissionCallback.bind(this));
|
jqxhr.done(this.createSubmissionCallback.bind(this));
|
||||||
|
@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
|
|||||||
MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
|
MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
|
||||||
|
|
||||||
after_action :verify_authorized, except: [:help, :welcome]
|
after_action :verify_authorized, except: [:help, :welcome]
|
||||||
before_action :set_locale, :allow_iframe_requests
|
before_action :set_locale, :allow_iframe_requests, :load_embed_options
|
||||||
protect_from_forgery(with: :exception, prepend: true)
|
protect_from_forgery(with: :exception, prepend: true)
|
||||||
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
||||||
|
|
||||||
@ -39,4 +39,14 @@ class ApplicationController < ActionController::Base
|
|||||||
def allow_iframe_requests
|
def allow_iframe_requests
|
||||||
response.headers.delete('X-Frame-Options')
|
response.headers.delete('X-Frame-Options')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_embed_options
|
||||||
|
if session[:embed_options].present? && session[:embed_options].is_a?(Hash)
|
||||||
|
@embed_options = session[:embed_options].symbolize_keys
|
||||||
|
else
|
||||||
|
@embed_options = {}
|
||||||
|
end
|
||||||
|
@embed_options
|
||||||
|
end
|
||||||
|
private :load_embed_options
|
||||||
end
|
end
|
||||||
|
@ -22,6 +22,7 @@ module Lti
|
|||||||
if (exercise_id.nil?)
|
if (exercise_id.nil?)
|
||||||
session.delete(:consumer_id)
|
session.delete(:consumer_id)
|
||||||
session.delete(:external_user_id)
|
session.delete(:external_user_id)
|
||||||
|
session.delete(:embed_options)
|
||||||
else
|
else
|
||||||
LtiParameter.where(consumers_id: consumer_id,
|
LtiParameter.where(consumers_id: consumer_id,
|
||||||
external_users_id: user_id,
|
external_users_id: user_id,
|
||||||
@ -148,6 +149,26 @@ module Lti
|
|||||||
end
|
end
|
||||||
private :set_current_user
|
private :set_current_user
|
||||||
|
|
||||||
|
def set_embedding_options
|
||||||
|
@embed_options = {}
|
||||||
|
[:hide_navbar,
|
||||||
|
:hide_exercise_description,
|
||||||
|
:disable_run,
|
||||||
|
:disable_score,
|
||||||
|
:disable_rfc,
|
||||||
|
:disable_interventions,
|
||||||
|
:hide_sidebar,
|
||||||
|
:read_only,
|
||||||
|
:hide_test_results,
|
||||||
|
:disable_hints].each do |option|
|
||||||
|
value = params["custom_embed_options_#{option}".to_sym] == 'true'
|
||||||
|
# Optimize storage and save only those that are true, the session cookie is limited to 4KB
|
||||||
|
@embed_options[option] = value if value.present?
|
||||||
|
end
|
||||||
|
session[:embed_options] = @embed_options
|
||||||
|
end
|
||||||
|
private :set_embedding_options
|
||||||
|
|
||||||
def store_lti_session_data(options = {})
|
def store_lti_session_data(options = {})
|
||||||
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
|
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
|
||||||
external_users_id: @current_user.id,
|
external_users_id: @current_user.id,
|
||||||
|
@ -55,6 +55,11 @@ module SubmissionScoring
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if @embed_options.present? && @embed_options[:hide_test_results] && outputs.present?
|
||||||
|
outputs.each do |output|
|
||||||
|
output.except!(:error_messages, :count, :failed, :filename, :message, :passed, :stderr, :stdout)
|
||||||
|
end
|
||||||
|
end
|
||||||
outputs
|
outputs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -188,7 +188,15 @@ class ExercisesController < ApplicationController
|
|||||||
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise
|
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise
|
||||||
user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day or user_got_intervention_in_exercise
|
user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day or user_got_intervention_in_exercise
|
||||||
|
|
||||||
@show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s
|
unless @embed_options[:disable_interventions]
|
||||||
|
@show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s
|
||||||
|
@show_break_interventions = false
|
||||||
|
else
|
||||||
|
@show_rfc_interventions = false
|
||||||
|
@show_break_interventions = false
|
||||||
|
end
|
||||||
|
|
||||||
|
@hide_rfc_button = @embed_options[:disable_rfc]
|
||||||
|
|
||||||
|
|
||||||
@search = Search.new
|
@search = Search.new
|
||||||
|
@ -8,7 +8,7 @@ class FlowrController < ApplicationController
|
|||||||
.order('testruns.created_at DESC').first
|
.order('testruns.created_at DESC').first
|
||||||
|
|
||||||
# Return if no submission was found
|
# Return if no submission was found
|
||||||
if submission.blank?
|
if submission.blank? || @embed_options[:disable_hints] || @embed_options[:hide_test_results]
|
||||||
skip_authorization
|
skip_authorization
|
||||||
render json: [], status: :ok
|
render json: [], status: :ok
|
||||||
return
|
return
|
||||||
|
@ -9,7 +9,7 @@ class ProxyExercisesController < ApplicationController
|
|||||||
private :authorize!
|
private :authorize!
|
||||||
|
|
||||||
def clone
|
def clone
|
||||||
proxy_exercise = @proxy_exercise.duplicate(token: nil, exercises: @proxy_exercise.exercises)
|
proxy_exercise = @proxy_exercise.duplicate(public: false, token: nil, exercises: @proxy_exercise.exercises, user: current_user)
|
||||||
proxy_exercise.send(:generate_token)
|
proxy_exercise.send(:generate_token)
|
||||||
if proxy_exercise.save
|
if proxy_exercise.save
|
||||||
redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
|
redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
|
||||||
@ -39,7 +39,7 @@ class ProxyExercisesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def proxy_exercise_params
|
def proxy_exercise_params
|
||||||
params[:proxy_exercise].permit(:description, :title, :exercise_ids => []) if params[:proxy_exercise].present?
|
params[:proxy_exercise].permit(:description, :title, :public, :exercise_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:proxy_exercise].present?
|
||||||
end
|
end
|
||||||
private :proxy_exercise_params
|
private :proxy_exercise_params
|
||||||
|
|
||||||
|
@ -96,6 +96,10 @@ class RequestForCommentsController < ApplicationController
|
|||||||
# POST /request_for_comments
|
# POST /request_for_comments
|
||||||
# POST /request_for_comments.json
|
# POST /request_for_comments.json
|
||||||
def create
|
def create
|
||||||
|
# Consider all requests as JSON
|
||||||
|
request.format = 'json'
|
||||||
|
raise Pundit::NotAuthorizedError if @embed_options[:disable_rfc]
|
||||||
|
|
||||||
@request_for_comment = RequestForComment.new(request_for_comment_params)
|
@request_for_comment = RequestForComment.new(request_for_comment_params)
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @request_for_comment.save
|
if @request_for_comment.save
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
class SessionsController < ApplicationController
|
class SessionsController < ApplicationController
|
||||||
include Lti
|
include Lti
|
||||||
|
|
||||||
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token].each do |method_name|
|
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token, :set_embedding_options].each do |method_name|
|
||||||
before_action(method_name, only: :create_through_lti)
|
before_action(method_name, only: :create_through_lti)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -122,13 +122,18 @@ class SubmissionsController < ApplicationController
|
|||||||
def run
|
def run
|
||||||
# TODO reimplement SSEs with websocket commands
|
# TODO reimplement SSEs with websocket commands
|
||||||
# with_server_sent_events do |server_sent_event|
|
# with_server_sent_events do |server_sent_event|
|
||||||
# output = @docker_client.execute_run_command(@submission, params[:filename])
|
# output = @docker_client.execute_run_command(@submission, sanitize_filename)
|
||||||
|
|
||||||
# server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout]
|
# server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout]
|
||||||
# server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr]
|
# server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr]
|
||||||
# end
|
# end
|
||||||
|
|
||||||
hijack do |tubesock|
|
hijack do |tubesock|
|
||||||
|
if @embed_options[:disable_run]
|
||||||
|
kill_socket(tubesock)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
# probably add:
|
# probably add:
|
||||||
# ensure
|
# ensure
|
||||||
# #guarantee that the thread is releasing the DB connection after it is done
|
# #guarantee that the thread is releasing the DB connection after it is done
|
||||||
@ -142,7 +147,7 @@ class SubmissionsController < ApplicationController
|
|||||||
# give the docker_client the tubesock object, so that it can send messages (timeout)
|
# give the docker_client the tubesock object, so that it can send messages (timeout)
|
||||||
@docker_client.tubesock = tubesock
|
@docker_client.tubesock = tubesock
|
||||||
|
|
||||||
result = @docker_client.execute_run_command(@submission, params[:filename])
|
result = @docker_client.execute_run_command(@submission, sanitize_filename)
|
||||||
tubesock.send_data JSON.dump({'cmd' => 'status', 'status' => result[:status]})
|
tubesock.send_data JSON.dump({'cmd' => 'status', 'status' => result[:status]})
|
||||||
|
|
||||||
if result[:status] == :container_running
|
if result[:status] == :container_running
|
||||||
@ -195,6 +200,10 @@ class SubmissionsController < ApplicationController
|
|||||||
# save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb)
|
# save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb)
|
||||||
save_run_output
|
save_run_output
|
||||||
|
|
||||||
|
if @run_output.blank?
|
||||||
|
parse_message t('exercises.implement.no_output', timestamp: l(Time.now, format: :short)), 'stdout', tubesock
|
||||||
|
end
|
||||||
|
|
||||||
# Hijacked connection needs to be notified correctly
|
# Hijacked connection needs to be notified correctly
|
||||||
tubesock.send_data JSON.dump({'cmd' => 'exit'})
|
tubesock.send_data JSON.dump({'cmd' => 'exit'})
|
||||||
tubesock.close
|
tubesock.close
|
||||||
@ -214,8 +223,8 @@ class SubmissionsController < ApplicationController
|
|||||||
@run_output = 'timeout: ' + @run_output # add information that this run timed out to the buffer
|
@run_output = 'timeout: ' + @run_output # add information that this run timed out to the buffer
|
||||||
else
|
else
|
||||||
# Filter out information about run_command, test_command, user or working directory
|
# Filter out information about run_command, test_command, user or working directory
|
||||||
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
|
run_command = @submission.execution_environment.run_command % command_substitutions(sanitize_filename)
|
||||||
test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename])
|
test_command = @submission.execution_environment.test_command % command_substitutions(sanitize_filename)
|
||||||
unless /root|workspace|#{run_command}|#{test_command}/.match(message)
|
unless /root|workspace|#{run_command}|#{test_command}/.match(message)
|
||||||
parse_message(message, 'stdout', tubesock)
|
parse_message(message, 'stdout', tubesock)
|
||||||
end
|
end
|
||||||
@ -291,6 +300,11 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
def score
|
def score
|
||||||
hijack do |tubesock|
|
hijack do |tubesock|
|
||||||
|
if @embed_options[:disable_score]
|
||||||
|
kill_socket(tubesock)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
||||||
# tubesock is the socket to the client
|
# tubesock is the socket to the client
|
||||||
|
|
||||||
@ -308,6 +322,7 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_hints(tubesock, errors)
|
def send_hints(tubesock, errors)
|
||||||
|
return if @embed_options[:disable_hints]
|
||||||
errors = errors.to_a.uniq { |e| e.hint}
|
errors = errors.to_a.uniq { |e| e.hint}
|
||||||
errors.each do | error |
|
errors.each do | error |
|
||||||
tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description})
|
tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description})
|
||||||
@ -320,7 +335,7 @@ class SubmissionsController < ApplicationController
|
|||||||
private :set_docker_client
|
private :set_docker_client
|
||||||
|
|
||||||
def set_file
|
def set_file
|
||||||
@file = @files.detect { |file| file.name_with_extension == params[:filename] }
|
@file = @files.detect { |file| file.name_with_extension == sanitize_filename }
|
||||||
head :not_found unless @file
|
head :not_found unless @file
|
||||||
end
|
end
|
||||||
private :set_file
|
private :set_file
|
||||||
@ -361,7 +376,7 @@ class SubmissionsController < ApplicationController
|
|||||||
hijack do |tubesock|
|
hijack do |tubesock|
|
||||||
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
||||||
|
|
||||||
output = @docker_client.execute_test_command(@submission, params[:filename])
|
output = @docker_client.execute_test_command(@submission, sanitize_filename)
|
||||||
|
|
||||||
# tubesock is the socket to the client
|
# tubesock is the socket to the client
|
||||||
tubesock.send_data JSON.dump(output)
|
tubesock.send_data JSON.dump(output)
|
||||||
@ -405,4 +420,8 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
path
|
path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sanitize_filename
|
||||||
|
params[:filename].gsub(/\.json$/, '')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
class UserMailer < ActionMailer::Base
|
class UserMailer < ActionMailer::Base
|
||||||
|
|
||||||
|
def mail(*args)
|
||||||
|
# used to prevent the delivery to pseudonymous users without a valid email address
|
||||||
|
super unless args.first[:to].blank?
|
||||||
|
end
|
||||||
|
|
||||||
def activation_needed_email(user)
|
def activation_needed_email(user)
|
||||||
@activation_url = activate_internal_user_url(user, token: user.activation_token)
|
@activation_url = activate_internal_user_url(user, token: user.activation_token)
|
||||||
mail(subject: t('mailers.user_mailer.activation_needed.subject'), to: user.email)
|
mail(subject: t('mailers.user_mailer.activation_needed.subject'), to: user.email)
|
||||||
|
@ -5,11 +5,10 @@ class ExternalUser < ApplicationRecord
|
|||||||
validates :external_id, presence: true
|
validates :external_id, presence: true
|
||||||
|
|
||||||
def displayname
|
def displayname
|
||||||
result = name
|
if name.blank?
|
||||||
if(result == nil || result == "")
|
"User " + id.to_s
|
||||||
result = "User " + id.to_s
|
else
|
||||||
|
name
|
||||||
end
|
end
|
||||||
result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
class ProxyExercise < ApplicationRecord
|
class ProxyExercise < ApplicationRecord
|
||||||
|
include Creation
|
||||||
|
include DefaultValues
|
||||||
|
|
||||||
after_initialize :generate_token
|
after_initialize :generate_token
|
||||||
after_initialize :set_reason
|
after_initialize :set_reason
|
||||||
|
after_initialize :set_default_values
|
||||||
|
|
||||||
has_and_belongs_to_many :exercises
|
has_and_belongs_to_many :exercises
|
||||||
has_many :user_proxy_exercise_exercises
|
has_many :user_proxy_exercise_exercises
|
||||||
|
|
||||||
|
validates :public, boolean_presence: true
|
||||||
|
|
||||||
def count_files
|
def count_files
|
||||||
exercises.count
|
exercises.count
|
||||||
end
|
end
|
||||||
@ -19,6 +24,11 @@ class ProxyExercise < ApplicationRecord
|
|||||||
end
|
end
|
||||||
private :generate_token
|
private :generate_token
|
||||||
|
|
||||||
|
def set_default_values
|
||||||
|
set_default_values_if_present(public: false)
|
||||||
|
end
|
||||||
|
private :set_default_values
|
||||||
|
|
||||||
def duplicate(attributes = {})
|
def duplicate(attributes = {})
|
||||||
proxy_exercise = dup
|
proxy_exercise = dup
|
||||||
proxy_exercise.attributes = attributes
|
proxy_exercise.attributes = attributes
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
- if current_user.try(:admin?) or current_user.try(:teacher?)
|
- if current_user.try(:admin?) or current_user.try(:teacher?)
|
||||||
ul.breadcrumb
|
.container
|
||||||
- if model = Kernel.const_get(controller_path.classify) rescue nil
|
ul.breadcrumb
|
||||||
- object = model.find_by(id: params[:id])
|
- if model = Kernel.const_get(controller_path.classify) rescue nil
|
||||||
- if model.try(:nested_resource?)
|
- object = model.find_by(id: params[:id])
|
||||||
li.breadcrumb-item = model.model_name.human(count: 2)
|
- if model.try(:nested_resource?)
|
||||||
- if object
|
li.breadcrumb-item = model.model_name.human(count: 2)
|
||||||
li.breadcrumb-item = object
|
- if object
|
||||||
- else
|
li.breadcrumb-item = object
|
||||||
li.breadcrumb-item = link_to_if(policy(model).show?, model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
|
|
||||||
- if object
|
|
||||||
li.breadcrumb-item = link_to_if(policy(object).show?, object, send(:"#{model.model_name.singular}_path", object))
|
|
||||||
li.breadcrumb-item.active
|
|
||||||
- if I18n.translation_present?("shared.#{params[:action]}")
|
|
||||||
= t("shared.#{params[:action]}")
|
|
||||||
- else
|
- else
|
||||||
= t("#{controller_name}.index.#{params[:action]}")
|
li.breadcrumb-item = link_to_if(policy(model).show?, model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
|
||||||
- else
|
- if object
|
||||||
li.breadcrumb-item.active = t("breadcrumbs.#{controller_name}.#{params[:action]}")
|
li.breadcrumb-item = link_to_if(policy(object).show?, object, send(:"#{model.model_name.singular}_path", object))
|
||||||
|
li.breadcrumb-item.active
|
||||||
|
- if I18n.translation_present?("shared.#{params[:action]}")
|
||||||
|
= t("shared.#{params[:action]}")
|
||||||
|
- else
|
||||||
|
= t("#{controller_name}.index.#{params[:action]}")
|
||||||
|
- else
|
||||||
|
li.breadcrumb-item.active = t("breadcrumbs.#{controller_name}.#{params[:action]}")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#flash-container
|
#flash-container.container
|
||||||
#flash.container.fixed_error_messages data-message-failure=t('shared.message_failure')
|
#flash.container.fixed_error_messages data-message-failure=t('shared.message_failure')
|
||||||
- %w[alert danger info notice success warning].each do |severity|
|
- %w[alert danger info notice success warning].each do |severity|
|
||||||
div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)} alert-dismissible fade show"
|
div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)} alert-dismissible fade show"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
h1 = t('.title', application_name: application_name)
|
h1 = t('.title', application_name: application_name)
|
||||||
|
|
||||||
- if current_user.try(:admin?) or current_user.try(:teacher?)
|
- if current_user.try(:admin?) or current_user.try(:teacher?)
|
||||||
p = t('.text_signed_in_as_internal_user', user_name: current_user.name)
|
p = t('.text_signed_in_as_internal_user', user_name: current_user.displayname)
|
||||||
- elsif current_user.try(:external_user?)
|
- elsif current_user.try(:external_user?)
|
||||||
p = t('.text_signed_in_as_external_user', application_name: application_name)
|
p = t('.text_signed_in_as_external_user', application_name: application_name)
|
||||||
- else
|
- else
|
||||||
|
@ -4,5 +4,5 @@ h1 = @execution_environment
|
|||||||
.form-group
|
.form-group
|
||||||
label for='command' = t('.command')
|
label for='command' = t('.command')
|
||||||
input#command.form-control type='text'
|
input#command.form-control type='text'
|
||||||
pre#output data-message-no-output=t('exercises.implement.no_output')
|
pre#output data-message-no-output=t('exercises.implement.no_output', timestamp: l(Time.now, format: :short))
|
||||||
p = t('exercises.implement.no_output_yet')
|
p = t('exercises.implement.no_output_yet')
|
||||||
|
@ -3,7 +3,7 @@ h1
|
|||||||
= render('shared/edit_button', object: @exercise_collection)
|
= render('shared/edit_button', object: @exercise_collection)
|
||||||
|
|
||||||
= row(label: 'exercise_collections.name', value: @exercise_collection.name)
|
= row(label: 'exercise_collections.name', value: @exercise_collection.name)
|
||||||
= row(label: 'exercise_collections.user', value: link_to_if(policy(@exercise_collection.user).show?, @exercise_collection.user.name, @exercise_collection.user)) unless @exercise_collection.user.nil?
|
= row(label: 'exercise_collections.user', value: link_to_if(policy(@exercise_collection.user).show?, @exercise_collection.user.displayname, @exercise_collection.user)) unless @exercise_collection.user.nil?
|
||||||
= row(label: 'exercise_collections.use_anomaly_detection', value: @exercise_collection.use_anomaly_detection)
|
= row(label: 'exercise_collections.use_anomaly_detection', value: @exercise_collection.use_anomaly_detection)
|
||||||
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
||||||
|
|
||||||
@ -24,5 +24,5 @@ h4.mt-4 = t('activerecord.attributes.exercise_collections.exercises')
|
|||||||
td = exercise_collection_item.position
|
td = exercise_collection_item.position
|
||||||
td = link_to_if(policy(exercise).show?, exercise.title, exercise)
|
td = link_to_if(policy(exercise).show?, exercise.title, exercise)
|
||||||
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
|
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
|
||||||
td = link_to_if(exercise.user && policy(exercise.user).show?, exercise.user.name, exercise.user)
|
td = link_to_if(exercise.user && policy(exercise.user).show?, exercise.user.displayname, exercise.user)
|
||||||
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") if policy(exercise).statistics?
|
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") if policy(exercise).statistics?
|
||||||
|
@ -5,26 +5,29 @@
|
|||||||
- show_rfc_interventions = @show_rfc_interventions || "false"
|
- show_rfc_interventions = @show_rfc_interventions || "false"
|
||||||
- hide_rfc_button = @hide_rfc_button || false
|
- hide_rfc_button = @hide_rfc_button || false
|
||||||
#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path(@exercise)
|
#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path(@exercise)
|
||||||
div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
|
- unless @embed_options[:hide_sidebar]
|
||||||
|
div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
|
||||||
div.editor-col.col.p-0 id='frames'
|
div.editor-col.col.p-0 id='frames'
|
||||||
#editor-buttons.btn-group.enforce-bottom-margin
|
#editor-buttons.btn-group.enforce-bottom-margin
|
||||||
= render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy'))
|
= render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy'))
|
||||||
= render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render'))
|
= render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render')) unless @embed_options[:hide_run_button]
|
||||||
= render('editor_button', data: {:'data-message-failure' => t('exercises.editor.run_failure'), :'data-message-network' => t('exercises.editor.network'), :'data-message-success' => t('exercises.editor.run_success'), :'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-play', id: 'run', label: t('exercises.editor.run'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
|
= render('editor_button', data: {:'data-message-failure' => t('exercises.editor.run_failure'), :'data-message-network' => t('exercises.editor.network'), :'data-message-success' => t('exercises.editor.run_success'), :'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-play', id: 'run', label: t('exercises.editor.run'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r')) unless @embed_options[:disable_run]
|
||||||
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-stop', id: 'stop', label: t('exercises.editor.stop'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
|
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-stop', id: 'stop', label: t('exercises.editor.stop'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r')) unless @embed_options[:disable_run]
|
||||||
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-rocket', id: 'test', label: t('exercises.editor.test'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + t'))
|
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-rocket', id: 'test', label: t('exercises.editor.test'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + t')) unless @embed_options[:disable_run]
|
||||||
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s'))
|
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) unless @embed_options[:disable_score]
|
||||||
// todo: check this
|
// todo: check this
|
||||||
- if not hide_rfc_button
|
- unless hide_rfc_button
|
||||||
= render('editor_button', icon: 'fa fa-comment', id: 'requestComments', label: t('exercises.editor.requestComments'), title: t('exercises.editor.requestCommentsTooltip'))
|
= render('editor_button', icon: 'fa fa-comment', id: 'requestComments', label: t('exercises.editor.requestComments'), title: t('exercises.editor.requestCommentsTooltip'))
|
||||||
- @files.each do |file|
|
- @files.each do |file|
|
||||||
|
- file.read_only = true if @embed_options[:read_only]
|
||||||
= render('editor_frame', exercise: exercise, file: file)
|
= render('editor_frame', exercise: exercise, file: file)
|
||||||
#autosave-label
|
#autosave-label
|
||||||
= t('exercises.editor.lastsaved')
|
= t('exercises.editor.lastsaved')
|
||||||
span
|
span
|
||||||
button style="display:none" id="autosave"
|
button style="display:none" id="autosave"
|
||||||
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )
|
- unless @embed_options[:disable_run] && @embed_options[:disable_score]
|
||||||
|
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )
|
||||||
|
|
||||||
|
|
||||||
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
|
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') unless @embed_options[:disable_rfc]
|
||||||
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal')
|
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal') unless @embed_options[:disable_interventions]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
div id='output_sidebar_collapsed'
|
div id='output_sidebar_collapsed'
|
||||||
= render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '')
|
= render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '')
|
||||||
div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output')
|
div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output_yet')
|
||||||
.row
|
.row
|
||||||
= render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar'))
|
= render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar'))
|
||||||
|
|
||||||
@ -10,17 +10,18 @@ div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom
|
|||||||
#results
|
#results
|
||||||
h2 = t('exercises.implement.results')
|
h2 = t('exercises.implement.results')
|
||||||
p.test-count == t('exercises.implement.test_count', count: 0)
|
p.test-count == t('exercises.implement.test_count', count: 0)
|
||||||
ul.list-unstyled
|
- unless @embed_options[:hide_test_results]
|
||||||
ul#dummies.d-none.list-unstyled
|
ul.list-unstyled
|
||||||
li.card.mt-2
|
ul#dummies.d-none.list-unstyled
|
||||||
.card-header.py-2
|
li.card.mt-2
|
||||||
h5.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0)
|
.card-header.py-2
|
||||||
.card-body.bg-white.text-dark
|
h5.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0)
|
||||||
= row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
|
.card-body.bg-white.text-dark
|
||||||
= row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
|
= row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
|
||||||
= row(label: 'exercises.implement.feedback')
|
= row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
|
||||||
= row(label: 'exercises.implement.error_messages')
|
= row(label: 'exercises.implement.feedback')
|
||||||
/= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#'))
|
= row(label: 'exercises.implement.error_messages')
|
||||||
|
/= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#'))
|
||||||
#score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score)
|
#score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score)
|
||||||
h4
|
h4
|
||||||
span == "#{t('activerecord.attributes.submission.score')}: "
|
span == "#{t('activerecord.attributes.submission.score')}: "
|
||||||
@ -44,12 +45,13 @@ div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom
|
|||||||
input#prompt-input.form-control type='text'
|
input#prompt-input.form-control type='text'
|
||||||
span.input-group-btn
|
span.input-group-btn
|
||||||
button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')
|
button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')
|
||||||
#error-hints
|
- unless @embed_options[:disable_hints]
|
||||||
.heading = t('exercises.implement.error_hints.heading')
|
#error-hints
|
||||||
ul.body
|
.heading = t('exercises.implement.error_hints.heading')
|
||||||
|
ul.body
|
||||||
#output.mt-2
|
#output.mt-2
|
||||||
pre = t('exercises.implement.no_output_yet')
|
pre = t('exercises.implement.no_output_yet')
|
||||||
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]
|
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] && !@embed_options[:disable_hints] && !@embed_options[:hide_test_results]
|
||||||
#flowrHint.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
|
#flowrHint.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
|
||||||
.card-header = t('exercises.implement.flowr.heading')
|
.card-header = t('exercises.implement.flowr.heading')
|
||||||
.card-body.text-dark.bg-white
|
.card-body.text-dark.bg-white
|
||||||
|
@ -13,7 +13,7 @@ h1 = link_to_if(policy(@exercise).show?, @exercise, exercise_path(@exercise))
|
|||||||
li.card.mt-2
|
li.card.mt-2
|
||||||
.card-header role="tab" id="heading"
|
.card-header role="tab" id="heading"
|
||||||
div.clearfix.feedback-header
|
div.clearfix.feedback-header
|
||||||
span.username = link_to(feedback.user.name, statistics_external_user_exercise_path(id: @exercise.id, external_user_id: feedback.user.id))
|
span.username = link_to(feedback.user.displayname, statistics_external_user_exercise_path(id: @exercise.id, external_user_id: feedback.user.id))
|
||||||
- if feedback.anomaly_notification
|
- if feedback.anomaly_notification
|
||||||
i class="fa fa-envelope-o" data-placement="top" data-toggle="tooltip" data-container="body" title=feedback.anomaly_notification.reason
|
i class="fa fa-envelope-o" data-placement="top" data-toggle="tooltip" data-container="body" title=feedback.anomaly_notification.reason
|
||||||
span.date = feedback.created_at
|
span.date = feedback.created_at
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
.row
|
.row
|
||||||
#editor-column.col-md-12
|
#editor-column.col-md-12
|
||||||
.exercise.clearfix
|
- unless @embed_options[:hide_exercise_description]
|
||||||
div
|
.exercise.clearfix
|
||||||
span.badge.badge-pill.badge-primary.float-right.score
|
div
|
||||||
|
span.badge.badge-pill.badge-primary.float-right.score
|
||||||
|
|
||||||
h1 id="exercise-headline"
|
h1 id="exercise-headline"
|
||||||
i class="fa fa-chevron-down" id="description-symbol"
|
i class="fa fa-chevron-down" id="description-symbol"
|
||||||
= @exercise.title
|
= @exercise.title
|
||||||
|
|
||||||
#description-card.lead.description-card
|
#description-card.lead.description-card
|
||||||
= render_markdown(@exercise.description)
|
= render_markdown(@exercise.description)
|
||||||
|
|
||||||
a#toggle href="#" data-show=t('shared.show') data-hide=t('shared.hide') = t('shared.hide')
|
a#toggle href="#" data-show=t('shared.show') data-hide=t('shared.hide') = t('shared.hide')
|
||||||
|
|
||||||
#alert.alert.alert-danger role='alert'
|
#alert.alert.alert-danger role='alert'
|
||||||
h4 = t('.alert.title')
|
h4 = t('.alert.title')
|
||||||
|
@ -10,7 +10,7 @@ h1 = ExternalUser.model_name.human(count: 2)
|
|||||||
tbody
|
tbody
|
||||||
- @users.each do |user|
|
- @users.each do |user|
|
||||||
tr
|
tr
|
||||||
td = link_to_if(policy(user).show?, user.name)
|
td = link_to_if(policy(user).show?, user.displayname)
|
||||||
td = link_to_if(policy(user.consumer).show?, user.consumer, user.consumer)
|
td = link_to_if(policy(user.consumer).show?, user.consumer, user.consumer)
|
||||||
td = link_to(t('shared.show'), user) if policy(user).show?
|
td = link_to(t('shared.show'), user) if policy(user).show?
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
h1 = @user.name
|
h1 = @user.displayname
|
||||||
|
|
||||||
= row(label: 'external_user.name', value: @user.name)
|
= row(label: 'external_user.name', value: @user.name)
|
||||||
//= row(label: 'external_user.email', value: @user.email)
|
//= row(label: 'external_user.email', value: @user.email)
|
||||||
|
@ -13,22 +13,23 @@ html lang='en'
|
|||||||
= yield(:head)
|
= yield(:head)
|
||||||
= csrf_meta_tags
|
= csrf_meta_tags
|
||||||
body
|
body
|
||||||
nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation'
|
- unless @embed_options[:hide_navbar]
|
||||||
.container
|
nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation'
|
||||||
.navbar-brand
|
.container
|
||||||
i.fa.fa-code
|
.navbar-brand
|
||||||
= application_name
|
i.fa.fa-code
|
||||||
button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation'
|
= application_name
|
||||||
span.navbar-toggler-icon
|
button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation'
|
||||||
#navbar-collapse.collapse.navbar-collapse
|
span.navbar-toggler-icon
|
||||||
= render('navigation', cached: true)
|
#navbar-collapse.collapse.navbar-collapse
|
||||||
ul.nav.navbar-nav.ml-auto
|
= render('navigation', cached: true)
|
||||||
= render('locale_selector', cached: true)
|
ul.nav.navbar-nav.ml-auto
|
||||||
li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link')
|
= render('locale_selector', cached: true)
|
||||||
= render('session')
|
li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link')
|
||||||
.container data-controller=controller_name
|
= render('session')
|
||||||
|
div data-controller=controller_name
|
||||||
= render('flash')
|
= render('flash')
|
||||||
= render('breadcrumbs') if current_user.try(:admin?) or current_user.try(:teacher?)
|
= render('breadcrumbs') if (current_user.try(:admin?) or current_user.try(:teacher?)) && !@embed_options[:hide_navbar]
|
||||||
- if (controller_name == "exercises" && action_name == "implement")
|
- if (controller_name == "exercises" && action_name == "implement")
|
||||||
.container-fluid
|
.container-fluid
|
||||||
= yield
|
= yield
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
.form-group
|
.form-group
|
||||||
= f.label(:description)
|
= f.label(:description)
|
||||||
= f.pagedown :description, input_html: { preview: true, rows: 10 }
|
= f.pagedown :description, input_html: { preview: true, rows: 10 }
|
||||||
|
.form-check.mb-3
|
||||||
|
label.form-check-label
|
||||||
|
= f.check_box(:public, class: 'form-check-input')
|
||||||
|
= t('activerecord.attributes.exercise.public')
|
||||||
|
|
||||||
h3 Exercises
|
h3 Exercises
|
||||||
.table-responsive
|
.table-responsive
|
||||||
|
@ -11,13 +11,17 @@ h1 = ProxyExercise.model_name.human(count: 2)
|
|||||||
tr
|
tr
|
||||||
th.p-1 = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title'))
|
th.p-1 = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title'))
|
||||||
th.p-1 = t('activerecord.attributes.exercise.token')
|
th.p-1 = t('activerecord.attributes.exercise.token')
|
||||||
|
th.p-1 = t('activerecord.attributes.exercise.public')
|
||||||
th.p-1 = t('activerecord.attributes.proxy_exercise.files_count')
|
th.p-1 = t('activerecord.attributes.proxy_exercise.files_count')
|
||||||
th.p-1 colspan=2 = t('shared.actions')
|
th.p-1 colspan=2 = t('shared.actions')
|
||||||
tbody
|
tbody
|
||||||
- @proxy_exercises.each do |proxy_exercise|
|
- @proxy_exercises.each do |proxy_exercise|
|
||||||
tr data-id=proxy_exercise.id
|
tr data-id=proxy_exercise.id
|
||||||
td.p-1.pt-2 = link_to_if(policy(proxy_exercise).show?, proxy_exercise.title, proxy_exercise)
|
td.p-1.pt-2 = link_to_if(policy(proxy_exercise).show?, proxy_exercise.title, proxy_exercise)
|
||||||
td.p-1.pt-2 = proxy_exercise.token
|
td.p-1.pt-2
|
||||||
|
code
|
||||||
|
= proxy_exercise.token
|
||||||
|
td.p-1.pt-2.public data-value=proxy_exercise.public? = symbol_for(proxy_exercise.public?)
|
||||||
td.p-1.pt-2 = proxy_exercise.count_files
|
td.p-1.pt-2 = proxy_exercise.count_files
|
||||||
td.p-1.pt-2 = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit?
|
td.p-1.pt-2 = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit?
|
||||||
|
|
||||||
|
@ -10,9 +10,12 @@ h1
|
|||||||
= render('shared/edit_button', object: @proxy_exercise)
|
= render('shared/edit_button', object: @proxy_exercise)
|
||||||
|
|
||||||
= row(label: 'exercise.title', value: @proxy_exercise.title)
|
= row(label: 'exercise.title', value: @proxy_exercise.title)
|
||||||
|
= row(label: 'exercise.user', value: link_to_if(policy(@proxy_exercise.author).show?, @proxy_exercise.author, @proxy_exercise.author))
|
||||||
= row(label: 'proxy_exercise.files_count', value: @exercises.count)
|
= row(label: 'proxy_exercise.files_count', value: @exercises.count)
|
||||||
|
= row(label: 'exercise.public', value: @proxy_exercise.public?)
|
||||||
= row(label: 'exercise.description', value: @proxy_exercise.description)
|
= row(label: 'exercise.description', value: @proxy_exercise.description)
|
||||||
= row(label: 'exercise.token', value: @proxy_exercise.token)
|
= row(label: 'exercise.embedding_parameters', class: 'mb-4') do
|
||||||
|
= content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: embedding_parameters(@proxy_exercise))
|
||||||
|
|
||||||
h2.mt-4 Exercises
|
h2.mt-4 Exercises
|
||||||
.table-responsive
|
.table-responsive
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
json.extract! @submission, :id, :files
|
json.extract! @submission, :id, :files
|
||||||
json.download_url download_submission_path(@submission)
|
json.download_url download_submission_path(@submission, format: :json)
|
||||||
json.score_url score_submission_path(@submission)
|
json.score_url score_submission_path(@submission, format: :json)
|
||||||
json.stop_url stop_submission_path(@submission)
|
json.stop_url stop_submission_path(@submission, format: :json)
|
||||||
json.download_file_url download_file_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}')
|
json.download_file_url download_file_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')
|
||||||
json.render_url render_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}')
|
json.render_url render_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')
|
||||||
json.run_url run_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}')
|
json.run_url run_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')
|
||||||
json.test_url test_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}')
|
json.test_url test_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')
|
||||||
|
@ -18,7 +18,7 @@ h1 = UserExerciseFeedback.model_name.human(count: 2)
|
|||||||
tbody
|
tbody
|
||||||
- @uefs.each do |uef|
|
- @uefs.each do |uef|
|
||||||
tr
|
tr
|
||||||
td = link_to_if(policy(uef.user).show?, uef.user.name)
|
td = link_to_if(policy(uef.user).show?, uef.user.displayname)
|
||||||
td = link_to_if(policy(uef.exercise).show?, uef.exercise.title, uef.exercise)
|
td = link_to_if(policy(uef.exercise).show?, uef.exercise.title, uef.exercise)
|
||||||
td = link_to(t('shared.show'), uef) if policy(uef).show?
|
td = link_to(t('shared.show'), uef) if policy(uef).show?
|
||||||
td = link_to(t('shared.destroy'), uef, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(uef).destroy?
|
td = link_to(t('shared.destroy'), uef, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(uef).destroy?
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
== t('mailers.user_mailer.send_thank_you_note.body',
|
== t('mailers.user_mailer.send_thank_you_note.body',
|
||||||
receiver_displayname: @receiver_displayname,
|
receiver_displayname: @receiver_displayname,
|
||||||
link_to_comment: link_to(@rfc_link, @rfc_link),
|
link_to_comment: link_to(@rfc_link, @rfc_link),
|
||||||
author: @author,
|
author: @author.displayname,
|
||||||
thank_you_note: @thank_you_note )
|
thank_you_note: @thank_you_note )
|
||||||
|
@ -301,7 +301,7 @@ de:
|
|||||||
file: 'Test-Datei <span class="number">%{number}</span> (<span class="filename">%{filename}</span>)'
|
file: 'Test-Datei <span class="number">%{number}</span> (<span class="filename">%{filename}</span>)'
|
||||||
hint: Tipp
|
hint: Tipp
|
||||||
no_files: Die Aufgabe umfasst noch keine sichtbaren Dateien.
|
no_files: Die Aufgabe umfasst noch keine sichtbaren Dateien.
|
||||||
no_output: Die letzte Code-Ausführung hat keine Ausgabe erzeugt.
|
no_output: Die letzte Code-Ausführung terminierte am %{timestamp} ohne Ausgabe.
|
||||||
no_output_yet: Bisher existiert noch keine Ausgabe.
|
no_output_yet: Bisher existiert noch keine Ausgabe.
|
||||||
output: Programm-Ausgabe
|
output: Programm-Ausgabe
|
||||||
passed_tests: Erfolgreiche Tests
|
passed_tests: Erfolgreiche Tests
|
||||||
|
@ -301,7 +301,7 @@ en:
|
|||||||
file: 'Test File <span class="number">%{number}</span> (<span class="filename">%{filename}</span>)'
|
file: 'Test File <span class="number">%{number}</span> (<span class="filename">%{filename}</span>)'
|
||||||
hint: Hint
|
hint: Hint
|
||||||
no_files: The exercise does not comprise visible files yet.
|
no_files: The exercise does not comprise visible files yet.
|
||||||
no_output: The last code run has not generated any output.
|
no_output: The last code run finished on %{timestamp} without any output.
|
||||||
no_output_yet: There is no output yet.
|
no_output_yet: There is no output yet.
|
||||||
output: Program Output
|
output: Program Output
|
||||||
passed_tests: Passed Tests
|
passed_tests: Passed Tests
|
||||||
|
@ -150,7 +150,7 @@ Rails.application.routes.draw do
|
|||||||
post '/lti/launch', as: 'lti_launch', to: 'sessions#create_through_lti'
|
post '/lti/launch', as: 'lti_launch', to: 'sessions#create_through_lti'
|
||||||
get '/lti/return', as: 'lti_return', to: 'sessions#destroy_through_lti'
|
get '/lti/return', as: 'lti_return', to: 'sessions#destroy_through_lti'
|
||||||
get '/sign_in', as: 'sign_in', to: 'sessions#new'
|
get '/sign_in', as: 'sign_in', to: 'sessions#new'
|
||||||
delete '/sign_out', as: 'sign_out', to: 'sessions#destroy'
|
match '/sign_out', as: 'sign_out', to: 'sessions#destroy', via: [:get, :delete]
|
||||||
|
|
||||||
resources :submissions, only: [:create, :index, :show] do
|
resources :submissions, only: [:create, :index, :show] do
|
||||||
member do
|
member do
|
||||||
|
9
db/migrate/20181119161514_add_user_to_proxy_exercise.rb
Normal file
9
db/migrate/20181119161514_add_user_to_proxy_exercise.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class AddUserToProxyExercise < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_reference :proxy_exercises, :user, polymorphic: true, index: true
|
||||||
|
add_column :proxy_exercises, :public, :boolean, null: false, default: false
|
||||||
|
|
||||||
|
internal_user = InternalUser.find_by(id: 46) || InternalUser.first
|
||||||
|
ProxyExercise.update_all(user_id: internal_user.id, user_type: internal_user.class.name)
|
||||||
|
end
|
||||||
|
end
|
@ -264,6 +264,10 @@ ActiveRecord::Schema.define(version: 2018_11_29_093207) do
|
|||||||
t.string "token"
|
t.string "token"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
|
t.string "user_type"
|
||||||
|
t.bigint "user_id"
|
||||||
|
t.boolean "public"
|
||||||
|
t.index ["user_type", "user_id"], name: "index_proxy_exercises_on_user_type_and_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "remote_evaluation_mappings", force: :cascade do |t|
|
create_table "remote_evaluation_mappings", force: :cascade do |t|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
$( document ).on('turbolinks:load', function() {
|
$( document ).on('turbolinks:load', function() {
|
||||||
var DURATION = 10000;
|
var DURATION = 5000;
|
||||||
var SEVERITIES = ['danger', 'info', 'success', 'warning'];
|
var SEVERITIES = ['danger', 'info', 'success', 'warning'];
|
||||||
|
|
||||||
var buildFlash = function(options) {
|
var buildFlash = function(options) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
#flash-container {
|
#flash-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -21px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash {
|
.flash {
|
||||||
|
@ -19,6 +19,7 @@ describe Lti do
|
|||||||
it 'clears the session' do
|
it 'clears the session' do
|
||||||
expect(controller.session).to receive(:delete).with(:consumer_id)
|
expect(controller.session).to receive(:delete).with(:consumer_id)
|
||||||
expect(controller.session).to receive(:delete).with(:external_user_id)
|
expect(controller.session).to receive(:delete).with(:external_user_id)
|
||||||
|
expect(controller.session).to receive(:delete).with(:embed_options)
|
||||||
controller.send(:clear_lti_session_data)
|
controller.send(:clear_lti_session_data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -186,7 +186,7 @@ describe SubmissionsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'ends with a placeholder' do
|
it 'ends with a placeholder' do
|
||||||
expect(url).to end_with(Submission::FILENAME_URL_PLACEHOLDER)
|
expect(url).to end_with(Submission::FILENAME_URL_PLACEHOLDER + '.json')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -196,7 +196,7 @@ describe SubmissionsController do
|
|||||||
let(:url) { JSON.parse(response.body).with_indifferent_access.fetch("#{action}_url") }
|
let(:url) { JSON.parse(response.body).with_indifferent_access.fetch("#{action}_url") }
|
||||||
|
|
||||||
it "corresponds to the #{action} path" do
|
it "corresponds to the #{action} path" do
|
||||||
expect(url).to eq(Rails.application.routes.url_helpers.send(:"#{action}_submission_path", submission))
|
expect(url).to eq(Rails.application.routes.url_helpers.send(:"#{action}_submission_path", submission, format: :json))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :proxy_exercise, class: ProxyExercise do
|
factory :proxy_exercise, class: ProxyExercise do
|
||||||
|
created_by_teacher
|
||||||
token { 'dummytoken' }
|
token { 'dummytoken' }
|
||||||
title { 'Dummy' }
|
title { 'Dummy' }
|
||||||
end
|
end
|
||||||
|
@ -38,8 +38,8 @@ describe 'Authentication' do
|
|||||||
visit(root_path)
|
visit(root_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "displays the user's name" do
|
it "displays the user's displayname" do
|
||||||
expect(page).to have_content(user.name)
|
expect(page).to have_content(user.displayname)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'displays a sign out link' do
|
it 'displays a sign out link' do
|
||||||
|
@ -10,6 +10,7 @@ describe 'exercises/implement.html.slim' do
|
|||||||
assign(:exercise, exercise)
|
assign(:exercise, exercise)
|
||||||
assign(:files, files)
|
assign(:files, files)
|
||||||
assign(:paths, [])
|
assign(:paths, [])
|
||||||
|
assign(:embed_options, {})
|
||||||
render
|
render
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user