From 59257d17f2bca876781c60bd7c072a617e816a0d Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Thu, 22 Nov 2018 19:16:03 +0100 Subject: [PATCH 1/9] Add user (ID and type) to proxy exercise This is required for the existing policies to work --- app/controllers/proxy_exercises_controller.rb | 4 ++-- app/models/proxy_exercise.rb | 10 ++++++++++ app/views/proxy_exercises/_form.html.slim | 4 ++++ app/views/proxy_exercises/index.html.slim | 6 +++++- app/views/proxy_exercises/show.html.slim | 5 ++++- .../20181119161514_add_user_to_proxy_exercise.rb | 9 +++++++++ db/schema.rb | 4 ++++ spec/factories/proxy_exercise.rb | 1 + 8 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20181119161514_add_user_to_proxy_exercise.rb diff --git a/app/controllers/proxy_exercises_controller.rb b/app/controllers/proxy_exercises_controller.rb index 40bd20ca..2f3534d8 100644 --- a/app/controllers/proxy_exercises_controller.rb +++ b/app/controllers/proxy_exercises_controller.rb @@ -9,7 +9,7 @@ class ProxyExercisesController < ApplicationController private :authorize! 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) if proxy_exercise.save redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human)) @@ -39,7 +39,7 @@ class ProxyExercisesController < ApplicationController end 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 private :proxy_exercise_params diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 328fffa6..bb17b30a 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -1,11 +1,16 @@ class ProxyExercise < ApplicationRecord + include Creation + include DefaultValues after_initialize :generate_token after_initialize :set_reason + after_initialize :set_default_values has_and_belongs_to_many :exercises has_many :user_proxy_exercise_exercises + validates :public, boolean_presence: true + def count_files exercises.count end @@ -19,6 +24,11 @@ class ProxyExercise < ApplicationRecord end private :generate_token + def set_default_values + set_default_values_if_present(public: false) + end + private :set_default_values + def duplicate(attributes = {}) proxy_exercise = dup proxy_exercise.attributes = attributes diff --git a/app/views/proxy_exercises/_form.html.slim b/app/views/proxy_exercises/_form.html.slim index 601ef950..831fb17e 100644 --- a/app/views/proxy_exercises/_form.html.slim +++ b/app/views/proxy_exercises/_form.html.slim @@ -6,6 +6,10 @@ .form-group = f.label(:description) = 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 .table-responsive diff --git a/app/views/proxy_exercises/index.html.slim b/app/views/proxy_exercises/index.html.slim index a2e7e460..198ff991 100644 --- a/app/views/proxy_exercises/index.html.slim +++ b/app/views/proxy_exercises/index.html.slim @@ -11,13 +11,17 @@ h1 = ProxyExercise.model_name.human(count: 2) tr 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.public') th.p-1 = t('activerecord.attributes.proxy_exercise.files_count') th.p-1 colspan=6 = t('shared.actions') tbody - @proxy_exercises.each do |proxy_exercise| tr data-id=proxy_exercise.id td.p-1.pt-2 = link_to(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 = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit? diff --git a/app/views/proxy_exercises/show.html.slim b/app/views/proxy_exercises/show.html.slim index 07f5061a..4868443c 100644 --- a/app/views/proxy_exercises/show.html.slim +++ b/app/views/proxy_exercises/show.html.slim @@ -11,9 +11,12 @@ h1 = render('shared/edit_button', object: @proxy_exercise) = 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: 'exercise.public', value: @proxy_exercise.public?) = 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 .table-responsive diff --git a/db/migrate/20181119161514_add_user_to_proxy_exercise.rb b/db/migrate/20181119161514_add_user_to_proxy_exercise.rb new file mode 100644 index 00000000..d250117b --- /dev/null +++ b/db/migrate/20181119161514_add_user_to_proxy_exercise.rb @@ -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) + ProxyExercise.update_all(user_id: internal_user.id, user_type: internal_user.class.name) + end +end diff --git a/db/schema.rb b/db/schema.rb index 82ec19bf..5ee3a783 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -263,6 +263,10 @@ ActiveRecord::Schema.define(version: 2018_11_29_093207) do t.string "token" t.datetime "created_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 create_table "remote_evaluation_mappings", force: :cascade do |t| diff --git a/spec/factories/proxy_exercise.rb b/spec/factories/proxy_exercise.rb index 3927541f..3118da81 100644 --- a/spec/factories/proxy_exercise.rb +++ b/spec/factories/proxy_exercise.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :proxy_exercise, class: ProxyExercise do + created_by_teacher token { 'dummytoken' } title { 'Dummy' } end From a0d8b30ef2f2f91a41855adf88ccb73947011022 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Mon, 10 Dec 2018 16:53:43 +0100 Subject: [PATCH 2/9] Implement support for some basic embed options for work sheets via LTI This commit also fixes an issue with the flash messages being positioned too high and displayed for too long --- app/assets/javascripts/base.js | 2 +- app/assets/javascripts/editor/editor.js.erb | 12 ++++++- app/assets/javascripts/editor/evaluation.js | 16 ++++++--- .../editor/participantsupport.js.erb | 3 +- app/assets/javascripts/editor/submissions.js | 3 +- app/controllers/application_controller.rb | 12 ++++++- app/controllers/concerns/lti.rb | 21 +++++++++++ .../concerns/submission_scoring.rb | 5 +++ app/controllers/exercises_controller.rb | 10 +++++- app/controllers/flowr_controller.rb | 2 +- .../request_for_comments_controller.rb | 4 +++ app/controllers/sessions_controller.rb | 2 +- app/controllers/submissions_controller.rb | 11 ++++++ app/views/application/_breadcrumbs.html.slim | 35 ++++++++++--------- app/views/application/_flash.html.slim | 2 +- app/views/exercises/_editor.html.slim | 23 ++++++------ app/views/exercises/_editor_output.html.slim | 32 +++++++++-------- app/views/exercises/implement.html.slim | 19 +++++----- app/views/layouts/application.html.slim | 31 ++++++++-------- app/views/submissions/show.json.jbuilder | 14 ++++---- lib/assets/javascripts/flash.js | 2 +- lib/assets/stylesheets/flash.css.scss | 1 - spec/concerns/lti_spec.rb | 1 + .../submissions_controller_spec.rb | 4 +-- .../exercises/implement.html.slim_spec.rb | 1 + 25 files changed, 178 insertions(+), 90 deletions(-) diff --git a/app/assets/javascripts/base.js b/app/assets/javascripts/base.js index b7263d48..08e9afd1 100644 --- a/app/assets/javascripts/base.js +++ b/app/assets/javascripts/base.js @@ -11,7 +11,7 @@ window.CodeOcean = { var ANIMATION_DURATION = 500; $.isController = function(name) { - return $('.container[data-controller="' + name + '"]').isPresent(); + return $('div[data-controller="' + name + '"]').isPresent(); }; $.fn.isPresent = function() { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index c4cb46f9..33271a44 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -110,6 +110,10 @@ configureEditors: function () { // The event ready.jstree is fired too early and thus doesn't work. 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')) { filetree.jstree("deselect_all"); filetree.jstree().select_node(file_id); @@ -224,6 +228,11 @@ configureEditors: function () { // remove last (empty) that is there by default line document.removeLines(document.getLength() - 1, document.getLength() - 1); 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.setTheme(this.THEME); @@ -458,7 +467,7 @@ configureEditors: function () { var editor = this.editor_for_file.get(file); editor.gotoLine(line, 0); - + event.preventDefault(); }, augmentStacktraceInOutput: function () { @@ -722,6 +731,7 @@ configureEditors: function () { this.initPrompt(); this.renderScore(); this.showFirstFile(); + this.resizeAceEditors(); $(window).on("beforeunload", this.unloadAutoSave.bind(this)); // create autosave when the editor is opened the first time diff --git a/app/assets/javascripts/editor/evaluation.js b/app/assets/javascripts/editor/evaluation.js index 5e631b2b..74a77ce3 100644 --- a/app/assets/javascripts/editor/evaluation.js +++ b/app/assets/javascripts/editor/evaluation.js @@ -27,8 +27,11 @@ CodeOceanEditorEvaluation = { printScoringResult: function (result, index) { $('#results').show(); var card = $('#dummies').children().first().clone(); - this.populateCard(card, result, index); - $('#results ul').first().append(card); + if (card.isPresent()) { + // 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) { @@ -141,14 +144,19 @@ CodeOceanEditorEvaluation = { }, 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); if (!colorize) { - if (output.stdout != undefined && output.stdout != '') { + if (output.stdout !== undefined && output.stdout !== '') { //element.append(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.text('StdErr: ' + element.text() + output.stderr); } diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index 9a8e54f8..23e98009 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -172,8 +172,9 @@ CodeOceanEditorRequestForComments = { this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this)); $('#comment-modal').modal('hide'); + $('#question').val(''); // we disabled the button to prevent that the user spams RFCs, but decided against this now. //var button = $('#requestComments'); //button.prop('disabled', true); - }, + } }; diff --git a/app/assets/javascripts/editor/submissions.js b/app/assets/javascripts/editor/submissions.js index f55bdbb5..20911535 100644 --- a/app/assets/javascripts/editor/submissions.js +++ b/app/assets/javascripts/editor/submissions.js @@ -10,6 +10,7 @@ CodeOceanEditorSubmissions = { */ createSubmission: function (initiator, filter, callback) { this.showSpinner(initiator); + var url = $(initiator).data('url') || $('#editor').data('submissions-url'); var jqxhr = this.ajax({ data: { submission: { @@ -21,7 +22,7 @@ CodeOceanEditorSubmissions = { }, dataType: 'json', method: 'POST', - url: $(initiator).data('url') || $('#editor').data('submissions-url') + url: url + '.json' }); jqxhr.always(this.hideSpinner.bind(this)); jqxhr.done(this.createSubmissionCallback.bind(this)); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c58303ba..2fcece66 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base MEMBER_ACTIONS = [:destroy, :edit, :show, :update] 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) rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized @@ -38,4 +38,14 @@ class ApplicationController < ActionController::Base def allow_iframe_requests response.headers.delete('X-Frame-Options') 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 diff --git a/app/controllers/concerns/lti.rb b/app/controllers/concerns/lti.rb index e8241af3..8ca57be9 100644 --- a/app/controllers/concerns/lti.rb +++ b/app/controllers/concerns/lti.rb @@ -22,6 +22,7 @@ module Lti if (exercise_id.nil?) session.delete(:consumer_id) session.delete(:external_user_id) + session.delete(:embed_options) else LtiParameter.where(consumers_id: consumer_id, external_users_id: user_id, @@ -133,6 +134,26 @@ module Lti end 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 = {}) lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id, external_users_id: @current_user.id, diff --git a/app/controllers/concerns/submission_scoring.rb b/app/controllers/concerns/submission_scoring.rb index 06bba11c..7436ac51 100644 --- a/app/controllers/concerns/submission_scoring.rb +++ b/app/controllers/concerns/submission_scoring.rb @@ -55,6 +55,11 @@ module SubmissionScoring } 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 end end diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 839aef4c..79a669cf 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -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_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 diff --git a/app/controllers/flowr_controller.rb b/app/controllers/flowr_controller.rb index e9f742b7..310d91d2 100644 --- a/app/controllers/flowr_controller.rb +++ b/app/controllers/flowr_controller.rb @@ -8,7 +8,7 @@ class FlowrController < ApplicationController .order('testruns.created_at DESC').first # Return if no submission was found - if submission.blank? + if submission.blank? || @embed_options[:disable_hints] || @embed_options[:hide_test_results] skip_authorization render json: [], status: :ok return diff --git a/app/controllers/request_for_comments_controller.rb b/app/controllers/request_for_comments_controller.rb index 29c5a6ea..39f1ebb8 100644 --- a/app/controllers/request_for_comments_controller.rb +++ b/app/controllers/request_for_comments_controller.rb @@ -96,6 +96,10 @@ class RequestForCommentsController < ApplicationController # POST /request_for_comments # POST /request_for_comments.json 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) respond_to do |format| if @request_for_comment.save diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8f698d1a..bd1a16ec 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,7 +1,7 @@ class SessionsController < ApplicationController 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) end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 039efa02..d6a92055 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -129,6 +129,11 @@ class SubmissionsController < ApplicationController # end hijack do |tubesock| + if @embed_options[:disable_run] + kill_socket(tubesock) + return + end + # probably add: # ensure # #guarantee that the thread is releasing the DB connection after it is done @@ -291,6 +296,11 @@ class SubmissionsController < ApplicationController def score 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? # tubesock is the socket to the client @@ -308,6 +318,7 @@ class SubmissionsController < ApplicationController end def send_hints(tubesock, errors) + return if @embed_options[:disable_hints] errors = errors.to_a.uniq { |e| e.hint} errors.each do | error | tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description}) diff --git a/app/views/application/_breadcrumbs.html.slim b/app/views/application/_breadcrumbs.html.slim index 7899ae0e..570f5624 100644 --- a/app/views/application/_breadcrumbs.html.slim +++ b/app/views/application/_breadcrumbs.html.slim @@ -1,19 +1,20 @@ - if current_user.try(:internal_user?) - ul.breadcrumb - - if model = Kernel.const_get(controller_path.classify) rescue nil - - object = model.find_by(id: params[:id]) - - if model.try(:nested_resource?) - li.breadcrumb-item = model.model_name.human(count: 2) - - if object - li.breadcrumb-item = object - - else - li.breadcrumb-item = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) - - if object - li.breadcrumb-item = link_to(object, send(:"#{model.model_name.singular}_path", object)) - li.breadcrumb-item.active - - if I18n.translation_present?("shared.#{params[:action]}") - = t("shared.#{params[:action]}") + .container + ul.breadcrumb + - if model = Kernel.const_get(controller_path.classify) rescue nil + - object = model.find_by(id: params[:id]) + - if model.try(:nested_resource?) + li.breadcrumb-item = model.model_name.human(count: 2) + - if object + li.breadcrumb-item = object - else - = t("#{controller_name}.index.#{params[:action]}") - - else - li.breadcrumb-item.active = t("breadcrumbs.#{controller_name}.#{params[:action]}") + li.breadcrumb-item = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) + - if object + li.breadcrumb-item = link_to(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]}") diff --git a/app/views/application/_flash.html.slim b/app/views/application/_flash.html.slim index 4a78c032..4675a913 100644 --- a/app/views/application/_flash.html.slim +++ b/app/views/application/_flash.html.slim @@ -1,4 +1,4 @@ -#flash-container +#flash-container.container #flash.container.fixed_error_messages data-message-failure=t('shared.message_failure') - %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" diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 700a9855..7323f21e 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -5,26 +5,29 @@ - show_rfc_interventions = @show_rfc_interventions || "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) - 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' #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', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render')) - = 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-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-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-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) + = 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')) 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')) 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')) 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')) unless @embed_options[:disable_score] // 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')) - @files.each do |file| + - file.read_only = true if @embed_options[:read_only] = render('editor_frame', exercise: exercise, file: file) #autosave-label = t('exercises.editor.lastsaved') span 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: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal') += 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') unless @embed_options[:disable_interventions] diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index dd6849da..91f289d8 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -10,17 +10,18 @@ div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom #results h2 = t('exercises.implement.results') p.test-count == t('exercises.implement.test_count', count: 0) - ul.list-unstyled - ul#dummies.d-none.list-unstyled - li.card.mt-2 - .card-header.py-2 - h5.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0) - .card-body.bg-white.text-dark - = row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) - = row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) - = row(label: 'exercises.implement.feedback') - = row(label: 'exercises.implement.error_messages') - /= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#')) + - unless @embed_options[:hide_test_results] + ul.list-unstyled + ul#dummies.d-none.list-unstyled + li.card.mt-2 + .card-header.py-2 + h5.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0) + .card-body.bg-white.text-dark + = row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) + = row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) + = row(label: 'exercises.implement.feedback') + = 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) h4 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' span.input-group-btn button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send') - #error-hints - .heading = t('exercises.implement.error_hints.heading') - ul.body + - unless @embed_options[:disable_hints] + #error-hints + .heading = t('exercises.implement.error_hints.heading') + ul.body #output.mt-2 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' .card-header = t('exercises.implement.flowr.heading') .card-body.text-dark.bg-white diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 1e872cbf..96f4ff74 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -1,17 +1,18 @@ .row #editor-column.col-md-12 - .exercise.clearfix - div - span.badge.badge-pill.badge-primary.float-right.score + - unless @embed_options[:hide_exercise_description] + .exercise.clearfix + div + span.badge.badge-pill.badge-primary.float-right.score - h1 id="exercise-headline" - i class="fa fa-chevron-down" id="description-symbol" - = @exercise.title + h1 id="exercise-headline" + i class="fa fa-chevron-down" id="description-symbol" + = @exercise.title - #description-card.lead.description-card - = render_markdown(@exercise.description) + #description-card.lead.description-card + = 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' h4 = t('.alert.title') diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 64b5549f..cdc3eb4e 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -13,22 +13,23 @@ html lang='en' = yield(:head) = csrf_meta_tags body - nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation' - .container - .navbar-brand - i.fa.fa-code - = application_name - button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation' - span.navbar-toggler-icon - #navbar-collapse.collapse.navbar-collapse - = render('navigation', cached: true) - ul.nav.navbar-nav.ml-auto - = render('locale_selector', cached: true) - li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link') - = render('session') - .container data-controller=controller_name + - unless @embed_options[:hide_navbar] + nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation' + .container + .navbar-brand + i.fa.fa-code + = application_name + button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation' + span.navbar-toggler-icon + #navbar-collapse.collapse.navbar-collapse + = render('navigation', cached: true) + ul.nav.navbar-nav.ml-auto + = render('locale_selector', cached: true) + li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link') + = render('session') + div data-controller=controller_name = render('flash') - = render('breadcrumbs') if current_user.try(:internal_user?) + = render('breadcrumbs') if current_user.try(:internal_user?) && !@embed_options[:hide_navbar] - if (controller_name == "exercises" && action_name == "implement") .container-fluid = yield diff --git a/app/views/submissions/show.json.jbuilder b/app/views/submissions/show.json.jbuilder index 07e199f4..b66919dc 100644 --- a/app/views/submissions/show.json.jbuilder +++ b/app/views/submissions/show.json.jbuilder @@ -1,8 +1,8 @@ json.extract! @submission, :id, :files -json.download_url download_submission_path(@submission) -json.score_url score_submission_path(@submission) -json.stop_url stop_submission_path(@submission) -json.download_file_url download_file_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') -json.render_url render_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') -json.run_url run_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') -json.test_url test_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') +json.download_url download_submission_path(@submission, format: :json) +json.score_url score_submission_path(@submission, format: :json) +json.stop_url stop_submission_path(@submission, format: :json) +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.', format: :json).gsub(/a\.\.json$/, '{filename}.json') +json.run_url run_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json') +json.test_url test_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json') diff --git a/lib/assets/javascripts/flash.js b/lib/assets/javascripts/flash.js index d7273864..7e7669a2 100644 --- a/lib/assets/javascripts/flash.js +++ b/lib/assets/javascripts/flash.js @@ -1,5 +1,5 @@ $( document ).on('turbolinks:load', function() { - var DURATION = 10000; + var DURATION = 5000; var SEVERITIES = ['danger', 'info', 'success', 'warning']; var buildFlash = function(options) { diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index 0abb6dc9..d339aff2 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -1,6 +1,5 @@ #flash-container { position: relative; - top: -21px; } .flash { diff --git a/spec/concerns/lti_spec.rb b/spec/concerns/lti_spec.rb index bdc5901e..37bb170f 100644 --- a/spec/concerns/lti_spec.rb +++ b/spec/concerns/lti_spec.rb @@ -19,6 +19,7 @@ describe Lti do it 'clears the session' do 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(:embed_options) controller.send(:clear_lti_session_data) end end diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb index 6aaf8555..97dd2eb3 100644 --- a/spec/controllers/submissions_controller_spec.rb +++ b/spec/controllers/submissions_controller_spec.rb @@ -186,7 +186,7 @@ describe SubmissionsController do end 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 @@ -196,7 +196,7 @@ describe SubmissionsController do let(:url) { JSON.parse(response.body).with_indifferent_access.fetch("#{action}_url") } 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 diff --git a/spec/views/exercises/implement.html.slim_spec.rb b/spec/views/exercises/implement.html.slim_spec.rb index fe9ab0d0..1944aa31 100644 --- a/spec/views/exercises/implement.html.slim_spec.rb +++ b/spec/views/exercises/implement.html.slim_spec.rb @@ -10,6 +10,7 @@ describe 'exercises/implement.html.slim' do assign(:exercise, exercise) assign(:files, files) assign(:paths, []) + assign(:embed_options, {}) render end From 6bf1bde2ea90cadba7080a24b1fa61a5add32d20 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Tue, 11 Dec 2018 14:30:00 +0100 Subject: [PATCH 3/9] Allow sign out request via GET --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 4b1a4719..dd3e16c5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -150,7 +150,7 @@ Rails.application.routes.draw do post '/lti/launch', as: 'lti_launch', to: 'sessions#create_through_lti' get '/lti/return', as: 'lti_return', to: 'sessions#destroy_through_lti' 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 member do From 25602972abed0213c7c83cbb34eea89fd5260f03 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Wed, 12 Dec 2018 14:11:57 +0100 Subject: [PATCH 4/9] Prevent delivery of mails to non-existent empty mail addresses --- app/mailers/user_mailer.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 497c2a31..b7b3d52f 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,4 +1,10 @@ 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) @activation_url = activate_internal_user_url(user, token: user.activation_token) mail(subject: t('mailers.user_mailer.activation_needed.subject'), to: user.email) From b4b9ab48d05f37445ee7238d07013dc1f0e0070f Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Wed, 12 Dec 2018 16:47:49 +0100 Subject: [PATCH 5/9] External User: Set name to displayname and introduce real_name --- app/models/external_user.rb | 28 +++++++++++++++++++------ app/views/external_users/show.html.slim | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/models/external_user.rb b/app/models/external_user.rb index b877f08d..61efaea5 100644 --- a/app/models/external_user.rb +++ b/app/models/external_user.rb @@ -4,12 +4,28 @@ class ExternalUser < ApplicationRecord validates :consumer_id, presence: true validates :external_id, presence: true - def displayname - result = name - if(result == nil || result == "") - result = "User " + id.to_s - end - result + def name + # Internal name, shown to teachers and administrators + pseudo_name end + def displayname + # External name, shown to the user itself and other users, e.g. on RfCs + pseudo_name + end + + def real_name + # Name attribute of the object as persistet in the database + self[:name] + end + + def pseudo_name + if real_name.blank? + "User " + id.to_s + else + real_name + end + end + private :pseudo_name + end diff --git a/app/views/external_users/show.html.slim b/app/views/external_users/show.html.slim index 33e2dd6b..c68a91f5 100644 --- a/app/views/external_users/show.html.slim +++ b/app/views/external_users/show.html.slim @@ -1,6 +1,6 @@ h1 = @user.name -= row(label: 'external_user.name', value: @user.name) += row(label: 'external_user.name', value: @user.real_name) //= row(label: 'external_user.email', value: @user.email) = row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer)) From d45a68a1230daa3b8d05b9a8e378181277313a97 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Thu, 13 Dec 2018 12:57:49 +0100 Subject: [PATCH 6/9] Minor: Fix spelling in comment --- app/assets/javascripts/editor/evaluation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/evaluation.js b/app/assets/javascripts/editor/evaluation.js index 74a77ce3..6c90abd7 100644 --- a/app/assets/javascripts/editor/evaluation.js +++ b/app/assets/javascripts/editor/evaluation.js @@ -28,7 +28,7 @@ CodeOceanEditorEvaluation = { $('#results').show(); var card = $('#dummies').children().first().clone(); if (card.isPresent()) { - // the card won't be present if @embed_options[::hide_test_results] == true + // the card won't be present if @embed_options[:hide_test_results] == true this.populateCard(card, result, index); $('#results ul').first().append(card); } From 4a1cd3037cfc03184bd2ec11a1ff27c294d136a0 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Thu, 13 Dec 2018 13:55:45 +0100 Subject: [PATCH 7/9] Fix execution of code via Ajax. Add output message for program runs without any output --- app/controllers/submissions_controller.rb | 20 +++++++++++++------ .../execution_environments/shell.html.slim | 2 +- app/views/exercises/_editor_output.html.slim | 2 +- config/locales/de.yml | 2 +- config/locales/en.yml | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index d6a92055..bc3ec4a6 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -122,7 +122,7 @@ class SubmissionsController < ApplicationController def run # TODO reimplement SSEs with websocket commands # 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({stderr: output[:stderr]}, event: 'output') if output[:stderr] @@ -147,7 +147,7 @@ class SubmissionsController < ApplicationController # give the docker_client the tubesock object, so that it can send messages (timeout) @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]}) if result[:status] == :container_running @@ -200,6 +200,10 @@ class SubmissionsController < ApplicationController # save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb) 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 tubesock.send_data JSON.dump({'cmd' => 'exit'}) tubesock.close @@ -219,8 +223,8 @@ class SubmissionsController < ApplicationController @run_output = 'timeout: ' + @run_output # add information that this run timed out to the buffer else # Filter out information about run_command, test_command, user or working directory - run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) - test_command = @submission.execution_environment.test_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(sanitize_filename) unless /root|workspace|#{run_command}|#{test_command}/.match(message) parse_message(message, 'stdout', tubesock) end @@ -331,7 +335,7 @@ class SubmissionsController < ApplicationController private :set_docker_client 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 end private :set_file @@ -372,7 +376,7 @@ class SubmissionsController < ApplicationController hijack do |tubesock| 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.send_data JSON.dump(output) @@ -416,4 +420,8 @@ class SubmissionsController < ApplicationController end path end + + def sanitize_filename + params[:filename].gsub(/\.json$/, '') + end end diff --git a/app/views/execution_environments/shell.html.slim b/app/views/execution_environments/shell.html.slim index 5b499a35..ae35c7b8 100644 --- a/app/views/execution_environments/shell.html.slim +++ b/app/views/execution_environments/shell.html.slim @@ -4,5 +4,5 @@ h1 = @execution_environment .form-group label for='command' = t('.command') 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') diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index 91f289d8..7948e548 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,6 +1,6 @@ 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: '') -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 = 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')) diff --git a/config/locales/de.yml b/config/locales/de.yml index 4cbd120c..f385a3d3 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -300,7 +300,7 @@ de: file: 'Test-Datei %{number} (%{filename})' hint: Tipp 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. output: Programm-Ausgabe passed_tests: Erfolgreiche Tests diff --git a/config/locales/en.yml b/config/locales/en.yml index 40922a02..0bcfd014 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -300,7 +300,7 @@ en: file: 'Test File %{number} (%{filename})' hint: Hint 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. output: Program Output passed_tests: Passed Tests From 88081bb5417e3d90f7f5cb9dbe2f2984192f22bd Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Thu, 13 Dec 2018 13:11:32 +0100 Subject: [PATCH 8/9] Replace usage of name in favor of displayname --- app/models/external_user.rb | 21 ++----------------- app/views/application/welcome.html.slim | 2 +- app/views/exercise_collections/show.html.slim | 4 ++-- app/views/exercises/feedback.html.slim | 2 +- app/views/external_users/index.html.slim | 2 +- app/views/external_users/show.html.slim | 4 ++-- .../user_exercise_feedbacks/index.html.slim | 2 +- .../user_mailer/send_thank_you_note.slim | 2 +- spec/features/authentication_spec.rb | 4 ++-- 9 files changed, 13 insertions(+), 30 deletions(-) diff --git a/app/models/external_user.rb b/app/models/external_user.rb index 61efaea5..fbd3cd3d 100644 --- a/app/models/external_user.rb +++ b/app/models/external_user.rb @@ -4,28 +4,11 @@ class ExternalUser < ApplicationRecord validates :consumer_id, presence: true validates :external_id, presence: true - def name - # Internal name, shown to teachers and administrators - pseudo_name - end - def displayname - # External name, shown to the user itself and other users, e.g. on RfCs - pseudo_name - end - - def real_name - # Name attribute of the object as persistet in the database - self[:name] - end - - def pseudo_name - if real_name.blank? + if name.blank? "User " + id.to_s else - real_name + name end end - private :pseudo_name - end diff --git a/app/views/application/welcome.html.slim b/app/views/application/welcome.html.slim index 8f9448c7..84692ec1 100644 --- a/app/views/application/welcome.html.slim +++ b/app/views/application/welcome.html.slim @@ -3,6 +3,6 @@ h1 = t('.title', application_name: application_name) - if current_user.try(:external_user?) p = t('.text_signed_in_as_external_user', application_name: application_name) - elsif current_user.try(:internal_user?) - 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) - else p == t('.text_signed_out', application_name: application_name, sign_in_path: sign_in_path) diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index 4fa6a8ac..db0945f3 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -3,7 +3,7 @@ h1 = render('shared/edit_button', object: @exercise_collection) = row(label: 'exercise_collections.name', value: @exercise_collection.name) -= row(label: 'exercise_collections.user', value: link_to(@exercise_collection.user.name, @exercise_collection.user)) unless @exercise_collection.user.nil? += row(label: 'exercise_collections.user', value: link_to(@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.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 = link_to(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.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") diff --git a/app/views/exercises/feedback.html.slim b/app/views/exercises/feedback.html.slim index 5090d0e2..26180cfd 100644 --- a/app/views/exercises/feedback.html.slim +++ b/app/views/exercises/feedback.html.slim @@ -13,7 +13,7 @@ h1 = link_to(@exercise, exercise_path(@exercise)) li.card.mt-2 .card-header role="tab" id="heading" 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 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 diff --git a/app/views/external_users/index.html.slim b/app/views/external_users/index.html.slim index 34359b2a..bff2509f 100644 --- a/app/views/external_users/index.html.slim +++ b/app/views/external_users/index.html.slim @@ -10,7 +10,7 @@ h1 = ExternalUser.model_name.human(count: 2) tbody - @users.each do |user| tr - td = user.name + td = user.displayname td = link_to(user.consumer, user.consumer) td = link_to(t('shared.show'), user) diff --git a/app/views/external_users/show.html.slim b/app/views/external_users/show.html.slim index c68a91f5..e5aee19e 100644 --- a/app/views/external_users/show.html.slim +++ b/app/views/external_users/show.html.slim @@ -1,6 +1,6 @@ -h1 = @user.name +h1 = @user.displayname -= row(label: 'external_user.name', value: @user.real_name) += row(label: 'external_user.name', value: @user.name) //= row(label: 'external_user.email', value: @user.email) = row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer)) diff --git a/app/views/user_exercise_feedbacks/index.html.slim b/app/views/user_exercise_feedbacks/index.html.slim index 52249229..a9e6b33b 100644 --- a/app/views/user_exercise_feedbacks/index.html.slim +++ b/app/views/user_exercise_feedbacks/index.html.slim @@ -19,7 +19,7 @@ h1 = UserExerciseFeedback.model_name.human(count: 2) - @uefs.each do |uef| tr td = uef.user.id - td = uef.user.name + td = uef.user.displayname td = link_to(uef.exercise.title, uef.exercise) td = link_to(t('shared.show'), uef) td = link_to(t('shared.destroy'), uef, data: {confirm: t('shared.confirm_destroy')}, method: :delete) diff --git a/app/views/user_mailer/send_thank_you_note.slim b/app/views/user_mailer/send_thank_you_note.slim index fcdcdf8d..465e3083 100644 --- a/app/views/user_mailer/send_thank_you_note.slim +++ b/app/views/user_mailer/send_thank_you_note.slim @@ -1 +1 @@ -== t('mailers.user_mailer.send_thank_you_note.body', receiver_displayname: @receiver_displayname, link_to_comment: link_to(@rfc_link, @rfc_link), author: @author, thank_you_note: @thank_you_note ) +== t('mailers.user_mailer.send_thank_you_note.body', receiver_displayname: @receiver_displayname, link_to_comment: link_to(@rfc_link, @rfc_link), author: @author.displayname, thank_you_note: @thank_you_note ) diff --git a/spec/features/authentication_spec.rb b/spec/features/authentication_spec.rb index 2a777dab..1cd08727 100644 --- a/spec/features/authentication_spec.rb +++ b/spec/features/authentication_spec.rb @@ -38,8 +38,8 @@ describe 'Authentication' do visit(root_path) end - it "displays the user's name" do - expect(page).to have_content(user.name) + it "displays the user's displayname" do + expect(page).to have_content(user.displayname) end it 'displays a sign out link' do From f0a462e3b8f3b58952720109f1d364e43e9aca5b Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Thu, 13 Dec 2018 14:30:21 +0100 Subject: [PATCH 9/9] Ensure that migration will always succeed --- db/migrate/20181119161514_add_user_to_proxy_exercise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20181119161514_add_user_to_proxy_exercise.rb b/db/migrate/20181119161514_add_user_to_proxy_exercise.rb index d250117b..983ce778 100644 --- a/db/migrate/20181119161514_add_user_to_proxy_exercise.rb +++ b/db/migrate/20181119161514_add_user_to_proxy_exercise.rb @@ -3,7 +3,7 @@ class AddUserToProxyExercise < ActiveRecord::Migration[5.2] 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) + 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