From a0d8b30ef2f2f91a41855adf88ccb73947011022 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Mon, 10 Dec 2018 16:53:43 +0100 Subject: [PATCH 1/4] 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 2/4] 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 d45a68a1230daa3b8d05b9a8e378181277313a97 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Thu, 13 Dec 2018 12:57:49 +0100 Subject: [PATCH 3/4] 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 4/4] 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