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;
$.isController = function(name) {
return $('.container[data-controller="' + name + '"]').isPresent();
return $('div[data-controller="' + name + '"]').isPresent();
};
$.fn.isPresent = function() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')}:&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'
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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