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
This commit is contained in:
Sebastian Serth
2018-12-10 16:53:43 +01:00
parent 4fd128b31b
commit a0d8b30ef2
25 changed files with 178 additions and 90 deletions

View File

@ -11,7 +11,7 @@ window.CodeOcean = {
var ANIMATION_DURATION = 500; var ANIMATION_DURATION = 500;
$.isController = function(name) { $.isController = function(name) {
return $('.container[data-controller="' + name + '"]').isPresent(); return $('div[data-controller="' + name + '"]').isPresent();
}; };
$.fn.isPresent = function() { $.fn.isPresent = function() {

View File

@ -110,6 +110,10 @@ configureEditors: function () {
// The event ready.jstree is fired too early and thus doesn't work. // The event ready.jstree is fired too early and thus doesn't work.
selectFileInJsTree: function(filetree, file_id) { selectFileInJsTree: function(filetree, file_id) {
if (!filetree.is(':visible'))
// The left sidebar is not shown and thus the filetree is not rendered.
return;
if (!filetree.hasClass('jstree-loading')) { if (!filetree.hasClass('jstree-loading')) {
filetree.jstree("deselect_all"); filetree.jstree("deselect_all");
filetree.jstree().select_node(file_id); filetree.jstree().select_node(file_id);
@ -224,6 +228,11 @@ configureEditors: function () {
// remove last (empty) that is there by default line // remove last (empty) that is there by default line
document.removeLines(document.getLength() - 1, document.getLength() - 1); document.removeLines(document.getLength() - 1, document.getLength() - 1);
editor.setReadOnly($(element).data('read-only') !== undefined); editor.setReadOnly($(element).data('read-only') !== undefined);
if (editor.getReadOnly()) {
editor.setHighlightActiveLine(false);
editor.setHighlightGutterLine(false);
editor.renderer.$cursorLayer.element.style.opacity = 0;
}
editor.setShowPrintMargin(false); editor.setShowPrintMargin(false);
editor.setTheme(this.THEME); editor.setTheme(this.THEME);
@ -458,7 +467,7 @@ configureEditors: function () {
var editor = this.editor_for_file.get(file); var editor = this.editor_for_file.get(file);
editor.gotoLine(line, 0); editor.gotoLine(line, 0);
event.preventDefault();
}, },
augmentStacktraceInOutput: function () { augmentStacktraceInOutput: function () {
@ -722,6 +731,7 @@ configureEditors: function () {
this.initPrompt(); this.initPrompt();
this.renderScore(); this.renderScore();
this.showFirstFile(); this.showFirstFile();
this.resizeAceEditors();
$(window).on("beforeunload", this.unloadAutoSave.bind(this)); $(window).on("beforeunload", this.unloadAutoSave.bind(this));
// create autosave when the editor is opened the first time // create autosave when the editor is opened the first time

View File

@ -27,8 +27,11 @@ CodeOceanEditorEvaluation = {
printScoringResult: function (result, index) { printScoringResult: function (result, index) {
$('#results').show(); $('#results').show();
var card = $('#dummies').children().first().clone(); var card = $('#dummies').children().first().clone();
this.populateCard(card, result, index); if (card.isPresent()) {
$('#results ul').first().append(card); // the card won't be present if @embed_options[::hide_test_results] == true
this.populateCard(card, result, index);
$('#results ul').first().append(card);
}
}, },
printScoringResults: function (response) { printScoringResults: function (response) {
@ -141,14 +144,19 @@ CodeOceanEditorEvaluation = {
}, },
printOutput: function (output, colorize, index) { printOutput: function (output, colorize, index) {
if (output.stderr === undefined && output.stdout === undefined) {
// Prevent empty element with no text at all
return;
}
var element = this.findOrCreateOutputElement(index); var element = this.findOrCreateOutputElement(index);
if (!colorize) { if (!colorize) {
if (output.stdout != undefined && output.stdout != '') { if (output.stdout !== undefined && output.stdout !== '') {
//element.append(output.stdout) //element.append(output.stdout)
element.text(element.text() + output.stdout) element.text(element.text() + output.stdout)
} }
if (output.stderr != undefined && output.stderr != '') { if (output.stderr !== undefined && output.stderr !== '') {
//element.append('StdErr: ' + output.stderr); //element.append('StdErr: ' + output.stderr);
element.text('StdErr: ' + element.text() + output.stderr); element.text('StdErr: ' + element.text() + output.stderr);
} }

View File

@ -172,8 +172,9 @@ CodeOceanEditorRequestForComments = {
this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this)); this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
$('#comment-modal').modal('hide'); $('#comment-modal').modal('hide');
$('#question').val('');
// we disabled the button to prevent that the user spams RFCs, but decided against this now. // we disabled the button to prevent that the user spams RFCs, but decided against this now.
//var button = $('#requestComments'); //var button = $('#requestComments');
//button.prop('disabled', true); //button.prop('disabled', true);
}, }
}; };

View File

@ -10,6 +10,7 @@ CodeOceanEditorSubmissions = {
*/ */
createSubmission: function (initiator, filter, callback) { createSubmission: function (initiator, filter, callback) {
this.showSpinner(initiator); this.showSpinner(initiator);
var url = $(initiator).data('url') || $('#editor').data('submissions-url');
var jqxhr = this.ajax({ var jqxhr = this.ajax({
data: { data: {
submission: { submission: {
@ -21,7 +22,7 @@ CodeOceanEditorSubmissions = {
}, },
dataType: 'json', dataType: 'json',
method: 'POST', method: 'POST',
url: $(initiator).data('url') || $('#editor').data('submissions-url') url: url + '.json'
}); });
jqxhr.always(this.hideSpinner.bind(this)); jqxhr.always(this.hideSpinner.bind(this));
jqxhr.done(this.createSubmissionCallback.bind(this)); jqxhr.done(this.createSubmissionCallback.bind(this));

View File

@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
MEMBER_ACTIONS = [:destroy, :edit, :show, :update] MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
after_action :verify_authorized, except: [:help, :welcome] after_action :verify_authorized, except: [:help, :welcome]
before_action :set_locale, :allow_iframe_requests before_action :set_locale, :allow_iframe_requests, :load_embed_options
protect_from_forgery(with: :exception, prepend: true) protect_from_forgery(with: :exception, prepend: true)
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
@ -38,4 +38,14 @@ class ApplicationController < ActionController::Base
def allow_iframe_requests def allow_iframe_requests
response.headers.delete('X-Frame-Options') response.headers.delete('X-Frame-Options')
end end
def load_embed_options
if session[:embed_options].present? && session[:embed_options].is_a?(Hash)
@embed_options = session[:embed_options].symbolize_keys
else
@embed_options = {}
end
@embed_options
end
private :load_embed_options
end end

View File

@ -22,6 +22,7 @@ module Lti
if (exercise_id.nil?) if (exercise_id.nil?)
session.delete(:consumer_id) session.delete(:consumer_id)
session.delete(:external_user_id) session.delete(:external_user_id)
session.delete(:embed_options)
else else
LtiParameter.where(consumers_id: consumer_id, LtiParameter.where(consumers_id: consumer_id,
external_users_id: user_id, external_users_id: user_id,
@ -133,6 +134,26 @@ module Lti
end end
private :set_current_user private :set_current_user
def set_embedding_options
@embed_options = {}
[:hide_navbar,
:hide_exercise_description,
:disable_run,
:disable_score,
:disable_rfc,
:disable_interventions,
:hide_sidebar,
:read_only,
:hide_test_results,
:disable_hints].each do |option|
value = params["custom_embed_options_#{option}".to_sym] == 'true'
# Optimize storage and save only those that are true, the session cookie is limited to 4KB
@embed_options[option] = value if value.present?
end
session[:embed_options] = @embed_options
end
private :set_embedding_options
def store_lti_session_data(options = {}) def store_lti_session_data(options = {})
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id, lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
external_users_id: @current_user.id, external_users_id: @current_user.id,

View File

@ -55,6 +55,11 @@ module SubmissionScoring
} }
end end
end end
if @embed_options.present? && @embed_options[:hide_test_results] && outputs.present?
outputs.each do |output|
output.except!(:error_messages, :count, :failed, :filename, :message, :passed, :stderr, :stdout)
end
end
outputs outputs
end end
end end

View File

@ -188,7 +188,15 @@ class ExercisesController < ApplicationController
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise
user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day or user_got_intervention_in_exercise user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day or user_got_intervention_in_exercise
@show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s unless @embed_options[:disable_interventions]
@show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s
@show_break_interventions = false
else
@show_rfc_interventions = false
@show_break_interventions = false
end
@hide_rfc_button = @embed_options[:disable_rfc]
@search = Search.new @search = Search.new

View File

@ -8,7 +8,7 @@ class FlowrController < ApplicationController
.order('testruns.created_at DESC').first .order('testruns.created_at DESC').first
# Return if no submission was found # Return if no submission was found
if submission.blank? if submission.blank? || @embed_options[:disable_hints] || @embed_options[:hide_test_results]
skip_authorization skip_authorization
render json: [], status: :ok render json: [], status: :ok
return return

View File

@ -96,6 +96,10 @@ class RequestForCommentsController < ApplicationController
# POST /request_for_comments # POST /request_for_comments
# POST /request_for_comments.json # POST /request_for_comments.json
def create def create
# Consider all requests as JSON
request.format = 'json'
raise Pundit::NotAuthorizedError if @embed_options[:disable_rfc]
@request_for_comment = RequestForComment.new(request_for_comment_params) @request_for_comment = RequestForComment.new(request_for_comment_params)
respond_to do |format| respond_to do |format|
if @request_for_comment.save if @request_for_comment.save

View File

@ -1,7 +1,7 @@
class SessionsController < ApplicationController class SessionsController < ApplicationController
include Lti include Lti
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token].each do |method_name| [:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token, :set_embedding_options].each do |method_name|
before_action(method_name, only: :create_through_lti) before_action(method_name, only: :create_through_lti)
end end

View File

@ -129,6 +129,11 @@ class SubmissionsController < ApplicationController
# end # end
hijack do |tubesock| hijack do |tubesock|
if @embed_options[:disable_run]
kill_socket(tubesock)
return
end
# probably add: # probably add:
# ensure # ensure
# #guarantee that the thread is releasing the DB connection after it is done # #guarantee that the thread is releasing the DB connection after it is done
@ -291,6 +296,11 @@ class SubmissionsController < ApplicationController
def score def score
hijack do |tubesock| hijack do |tubesock|
if @embed_options[:disable_score]
kill_socket(tubesock)
return
end
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
# tubesock is the socket to the client # tubesock is the socket to the client
@ -308,6 +318,7 @@ class SubmissionsController < ApplicationController
end end
def send_hints(tubesock, errors) def send_hints(tubesock, errors)
return if @embed_options[:disable_hints]
errors = errors.to_a.uniq { |e| e.hint} errors = errors.to_a.uniq { |e| e.hint}
errors.each do | error | errors.each do | error |
tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description}) tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description})

View File

@ -1,19 +1,20 @@
- if current_user.try(:internal_user?) - if current_user.try(:internal_user?)
ul.breadcrumb .container
- if model = Kernel.const_get(controller_path.classify) rescue nil ul.breadcrumb
- object = model.find_by(id: params[:id]) - if model = Kernel.const_get(controller_path.classify) rescue nil
- if model.try(:nested_resource?) - object = model.find_by(id: params[:id])
li.breadcrumb-item = model.model_name.human(count: 2) - if model.try(:nested_resource?)
- if object li.breadcrumb-item = model.model_name.human(count: 2)
li.breadcrumb-item = object - if object
- else li.breadcrumb-item = object
li.breadcrumb-item = link_to(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 - else
= t("#{controller_name}.index.#{params[:action]}") li.breadcrumb-item = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
- else - if object
li.breadcrumb-item.active = t("breadcrumbs.#{controller_name}.#{params[:action]}") li.breadcrumb-item = link_to(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]}")

View File

@ -1,4 +1,4 @@
#flash-container #flash-container.container
#flash.container.fixed_error_messages data-message-failure=t('shared.message_failure') #flash.container.fixed_error_messages data-message-failure=t('shared.message_failure')
- %w[alert danger info notice success warning].each do |severity| - %w[alert danger info notice success warning].each do |severity|
div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)} alert-dismissible fade show" div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)} alert-dismissible fade show"

View File

@ -5,26 +5,29 @@
- show_rfc_interventions = @show_rfc_interventions || "false" - show_rfc_interventions = @show_rfc_interventions || "false"
- hide_rfc_button = @hide_rfc_button || false - hide_rfc_button = @hide_rfc_button || false
#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path(@exercise) #editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path(@exercise)
div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) - unless @embed_options[:hide_sidebar]
div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
div.editor-col.col.p-0 id='frames' div.editor-col.col.p-0 id='frames'
#editor-buttons.btn-group.enforce-bottom-margin #editor-buttons.btn-group.enforce-bottom-margin
= render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy')) = render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy'))
= render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render')) = render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render')) unless @embed_options[:hide_run_button]
= render('editor_button', data: {:'data-message-failure' => t('exercises.editor.run_failure'), :'data-message-network' => t('exercises.editor.network'), :'data-message-success' => t('exercises.editor.run_success'), :'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-play', id: 'run', label: t('exercises.editor.run'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r')) = render('editor_button', data: {:'data-message-failure' => t('exercises.editor.run_failure'), :'data-message-network' => t('exercises.editor.network'), :'data-message-success' => t('exercises.editor.run_success'), :'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-play', id: 'run', label: t('exercises.editor.run'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r')) unless @embed_options[:disable_run]
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-stop', id: 'stop', label: t('exercises.editor.stop'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r')) = render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-stop', id: 'stop', label: t('exercises.editor.stop'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r')) unless @embed_options[:disable_run]
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-rocket', id: 'test', label: t('exercises.editor.test'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + t')) = render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-rocket', id: 'test', label: t('exercises.editor.test'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + t')) unless @embed_options[:disable_run]
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) = render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) unless @embed_options[:disable_score]
// todo: check this // todo: check this
- if not hide_rfc_button - unless hide_rfc_button
= render('editor_button', icon: 'fa fa-comment', id: 'requestComments', label: t('exercises.editor.requestComments'), title: t('exercises.editor.requestCommentsTooltip')) = render('editor_button', icon: 'fa fa-comment', id: 'requestComments', label: t('exercises.editor.requestComments'), title: t('exercises.editor.requestCommentsTooltip'))
- @files.each do |file| - @files.each do |file|
- file.read_only = true if @embed_options[:read_only]
= render('editor_frame', exercise: exercise, file: file) = render('editor_frame', exercise: exercise, file: file)
#autosave-label #autosave-label
= t('exercises.editor.lastsaved') = t('exercises.editor.lastsaved')
span span
button style="display:none" id="autosave" button style="display:none" id="autosave"
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) - unless @embed_options[:disable_run] && @embed_options[:disable_score]
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') unless @embed_options[:disable_rfc]
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal') = render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal') unless @embed_options[:disable_interventions]

View File

@ -10,17 +10,18 @@ div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom
#results #results
h2 = t('exercises.implement.results') h2 = t('exercises.implement.results')
p.test-count == t('exercises.implement.test_count', count: 0) p.test-count == t('exercises.implement.test_count', count: 0)
ul.list-unstyled - unless @embed_options[:hide_test_results]
ul#dummies.d-none.list-unstyled ul.list-unstyled
li.card.mt-2 ul#dummies.d-none.list-unstyled
.card-header.py-2 li.card.mt-2
h5.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0) .card-header.py-2
.card-body.bg-white.text-dark h5.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0)
= row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) .card-body.bg-white.text-dark
= row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) = row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: 'exercises.implement.feedback') = row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: 'exercises.implement.error_messages') = row(label: 'exercises.implement.feedback')
/= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#')) = row(label: 'exercises.implement.error_messages')
/= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#'))
#score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score) #score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score)
h4 h4
span == "#{t('activerecord.attributes.submission.score')}:&nbsp;" span == "#{t('activerecord.attributes.submission.score')}:&nbsp;"
@ -44,12 +45,13 @@ div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom
input#prompt-input.form-control type='text' input#prompt-input.form-control type='text'
span.input-group-btn span.input-group-btn
button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send') button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')
#error-hints - unless @embed_options[:disable_hints]
.heading = t('exercises.implement.error_hints.heading') #error-hints
ul.body .heading = t('exercises.implement.error_hints.heading')
ul.body
#output.mt-2 #output.mt-2
pre = t('exercises.implement.no_output_yet') pre = t('exercises.implement.no_output_yet')
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] && !@embed_options[:disable_hints] && !@embed_options[:hide_test_results]
#flowrHint.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' #flowrHint.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
.card-header = t('exercises.implement.flowr.heading') .card-header = t('exercises.implement.flowr.heading')
.card-body.text-dark.bg-white .card-body.text-dark.bg-white

View File

@ -1,17 +1,18 @@
.row .row
#editor-column.col-md-12 #editor-column.col-md-12
.exercise.clearfix - unless @embed_options[:hide_exercise_description]
div .exercise.clearfix
span.badge.badge-pill.badge-primary.float-right.score div
span.badge.badge-pill.badge-primary.float-right.score
h1 id="exercise-headline" h1 id="exercise-headline"
i class="fa fa-chevron-down" id="description-symbol" i class="fa fa-chevron-down" id="description-symbol"
= @exercise.title = @exercise.title
#description-card.lead.description-card #description-card.lead.description-card
= render_markdown(@exercise.description) = render_markdown(@exercise.description)
a#toggle href="#" data-show=t('shared.show') data-hide=t('shared.hide') = t('shared.hide') a#toggle href="#" data-show=t('shared.show') data-hide=t('shared.hide') = t('shared.hide')
#alert.alert.alert-danger role='alert' #alert.alert.alert-danger role='alert'
h4 = t('.alert.title') h4 = t('.alert.title')

View File

@ -13,22 +13,23 @@ html lang='en'
= yield(:head) = yield(:head)
= csrf_meta_tags = csrf_meta_tags
body body
nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation' - unless @embed_options[:hide_navbar]
.container nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation'
.navbar-brand .container
i.fa.fa-code .navbar-brand
= application_name i.fa.fa-code
button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation' = application_name
span.navbar-toggler-icon button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation'
#navbar-collapse.collapse.navbar-collapse span.navbar-toggler-icon
= render('navigation', cached: true) #navbar-collapse.collapse.navbar-collapse
ul.nav.navbar-nav.ml-auto = render('navigation', cached: true)
= render('locale_selector', cached: true) ul.nav.navbar-nav.ml-auto
li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link') = render('locale_selector', cached: true)
= render('session') li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link')
.container data-controller=controller_name = render('session')
div data-controller=controller_name
= render('flash') = render('flash')
= render('breadcrumbs') if current_user.try(:internal_user?) = render('breadcrumbs') if current_user.try(:internal_user?) && !@embed_options[:hide_navbar]
- if (controller_name == "exercises" && action_name == "implement") - if (controller_name == "exercises" && action_name == "implement")
.container-fluid .container-fluid
= yield = yield

View File

@ -1,8 +1,8 @@
json.extract! @submission, :id, :files json.extract! @submission, :id, :files
json.download_url download_submission_path(@submission) json.download_url download_submission_path(@submission, format: :json)
json.score_url score_submission_path(@submission) json.score_url score_submission_path(@submission, format: :json)
json.stop_url stop_submission_path(@submission) json.stop_url stop_submission_path(@submission, format: :json)
json.download_file_url download_file_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') json.download_file_url download_file_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')
json.render_url render_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') json.render_url render_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')
json.run_url run_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') json.run_url run_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')
json.test_url test_submission_path(@submission, 'a.').gsub(/a\.$/, '{filename}') json.test_url test_submission_path(@submission, 'a.', format: :json).gsub(/a\.\.json$/, '{filename}.json')

View File

@ -1,5 +1,5 @@
$( document ).on('turbolinks:load', function() { $( document ).on('turbolinks:load', function() {
var DURATION = 10000; var DURATION = 5000;
var SEVERITIES = ['danger', 'info', 'success', 'warning']; var SEVERITIES = ['danger', 'info', 'success', 'warning'];
var buildFlash = function(options) { var buildFlash = function(options) {

View File

@ -1,6 +1,5 @@
#flash-container { #flash-container {
position: relative; position: relative;
top: -21px;
} }
.flash { .flash {

View File

@ -19,6 +19,7 @@ describe Lti do
it 'clears the session' do it 'clears the session' do
expect(controller.session).to receive(:delete).with(:consumer_id) expect(controller.session).to receive(:delete).with(:consumer_id)
expect(controller.session).to receive(:delete).with(:external_user_id) expect(controller.session).to receive(:delete).with(:external_user_id)
expect(controller.session).to receive(:delete).with(:embed_options)
controller.send(:clear_lti_session_data) controller.send(:clear_lti_session_data)
end end
end end

View File

@ -186,7 +186,7 @@ describe SubmissionsController do
end end
it 'ends with a placeholder' do it 'ends with a placeholder' do
expect(url).to end_with(Submission::FILENAME_URL_PLACEHOLDER) expect(url).to end_with(Submission::FILENAME_URL_PLACEHOLDER + '.json')
end end
end end
end end
@ -196,7 +196,7 @@ describe SubmissionsController do
let(:url) { JSON.parse(response.body).with_indifferent_access.fetch("#{action}_url") } let(:url) { JSON.parse(response.body).with_indifferent_access.fetch("#{action}_url") }
it "corresponds to the #{action} path" do it "corresponds to the #{action} path" do
expect(url).to eq(Rails.application.routes.url_helpers.send(:"#{action}_submission_path", submission)) expect(url).to eq(Rails.application.routes.url_helpers.send(:"#{action}_submission_path", submission, format: :json))
end end
end end
end end

View File

@ -10,6 +10,7 @@ describe 'exercises/implement.html.slim' do
assign(:exercise, exercise) assign(:exercise, exercise)
assign(:files, files) assign(:files, files)
assign(:paths, []) assign(:paths, [])
assign(:embed_options, {})
render render
end end