
* Ensure only one `pp_work_alone` event is stored. * Disable Turbolinks for Programming Groups Work Alone, so that the implement page is requested normally. Otherwise, Turbolinks would load the page first, just to notice that it needs to reload the page afterwards to include Highlight.js for the tips.
613 lines
22 KiB
Ruby
613 lines
22 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ExercisesController < ApplicationController
|
|
include CommonBehavior
|
|
include RedirectBehavior
|
|
include Lti
|
|
include SubmissionParameters
|
|
include TimeHelper
|
|
|
|
before_action :handle_file_uploads, only: %i[create update]
|
|
before_action :set_execution_environments, only: %i[index create edit new update]
|
|
before_action :set_exercise_and_authorize,
|
|
only: MEMBER_ACTIONS + %i[clone implement working_times intervention search run statistics submit reload feedback
|
|
requests_for_comments study_group_dashboard export_external_check export_external_confirm
|
|
external_user_statistics]
|
|
before_action :collect_set_and_unset_exercise_tags, only: MEMBER_ACTIONS
|
|
before_action :set_external_user_and_authorize, only: [:external_user_statistics]
|
|
before_action :set_file_types, only: %i[create edit new update]
|
|
before_action :set_available_tips, only: %i[implement show new edit]
|
|
|
|
skip_before_action :verify_authenticity_token, only: %i[import_task import_uuid_check]
|
|
skip_after_action :verify_authorized, only: %i[import_task import_uuid_check]
|
|
skip_after_action :verify_policy_scoped, only: %i[import_task import_uuid_check], raise: false
|
|
|
|
rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise
|
|
|
|
def authorize!
|
|
authorize(@exercise || @exercises)
|
|
end
|
|
|
|
private :authorize!
|
|
|
|
def max_intervention_count_per_day
|
|
3
|
|
end
|
|
|
|
def max_intervention_count_per_exercise
|
|
1
|
|
end
|
|
|
|
def batch_update
|
|
@exercises = Exercise.all
|
|
authorize!
|
|
@exercises = params[:exercises].values.map do |exercise_params|
|
|
exercise = Exercise.find(exercise_params.delete(:id))
|
|
exercise.update(exercise_params)
|
|
exercise
|
|
end
|
|
render(json: {exercises: @exercises})
|
|
end
|
|
|
|
def clone
|
|
exercise = @exercise.duplicate(public: false, token: nil, user: current_user)
|
|
exercise.send(:generate_token)
|
|
if exercise.save
|
|
redirect_to(exercise_path(exercise), notice: t('shared.object_cloned', model: Exercise.model_name.human))
|
|
else
|
|
flash[:danger] = t('shared.message_failure')
|
|
redirect_to(@exercise)
|
|
end
|
|
end
|
|
|
|
def collect_paths(files)
|
|
unique_paths = files.map(&:path).compact_blank.uniq
|
|
subpaths = unique_paths.map do |path|
|
|
Array.new((path.split('/').length + 1)) do |n|
|
|
path.split('/').shift(n).join('/')
|
|
end
|
|
end
|
|
subpaths.flatten.uniq
|
|
end
|
|
|
|
private :collect_paths
|
|
|
|
def index
|
|
@search = policy_scope(Exercise).ransack(params[:q])
|
|
@exercises = @search.result.includes(:execution_environment, :user, :files, :exercise_tags).order(:title).paginate(page: params[:page], per_page: per_page_param)
|
|
authorize!
|
|
end
|
|
|
|
def show
|
|
# Show exercise details for teachers and admins
|
|
end
|
|
|
|
def new
|
|
@exercise = Exercise.new
|
|
authorize!
|
|
collect_set_and_unset_exercise_tags
|
|
end
|
|
|
|
def feedback
|
|
authorize!
|
|
@feedbacks = @exercise
|
|
.user_exercise_feedbacks
|
|
.includes(:exercise, user: [:programming_groups])
|
|
.paginate(page: params[:page], per_page: per_page_param)
|
|
@submissions = @feedbacks.map do |feedback|
|
|
feedback.exercise.final_submission(feedback.user.programming_groups.select {|pg| pg.exercise = @exercise }.presence || feedback.user)
|
|
end
|
|
end
|
|
|
|
def export_external_check
|
|
codeharbor_check = ExerciseService::CheckExternal.call(uuid: @exercise.uuid,
|
|
codeharbor_link: current_user.codeharbor_link)
|
|
render json: {
|
|
message: codeharbor_check[:message],
|
|
actions: render_to_string(
|
|
partial: 'export_actions',
|
|
locals: {
|
|
exercise: @exercise,
|
|
uuid_found: codeharbor_check[:uuid_found],
|
|
update_right: codeharbor_check[:update_right],
|
|
error: codeharbor_check[:error],
|
|
exported: false,
|
|
}
|
|
),
|
|
}, status: :ok
|
|
end
|
|
|
|
def export_external_confirm
|
|
authorize!
|
|
@exercise.uuid = SecureRandom.uuid if @exercise.uuid.nil?
|
|
|
|
error = ExerciseService::PushExternal.call(
|
|
zip: ProformaService::ExportTask.call(exercise: @exercise),
|
|
codeharbor_link: current_user.codeharbor_link
|
|
)
|
|
if error.nil?
|
|
render json: {
|
|
status: 'success',
|
|
message: t('exercises.export_codeharbor.successfully_exported', id: @exercise.id, title: @exercise.title),
|
|
actions: render_to_string(partial: 'export_actions',
|
|
locals: {exercise: @exercise, exported: true, error:}),
|
|
}
|
|
@exercise.save
|
|
else
|
|
render json: {
|
|
status: 'fail',
|
|
message: t('exercises.export_codeharbor.export_failed', id: @exercise.id, title: @exercise.title, error:),
|
|
actions: render_to_string(partial: 'export_actions',
|
|
locals: {exercise: @exercise, exported: true, error:}),
|
|
}
|
|
end
|
|
end
|
|
|
|
def import_uuid_check
|
|
user = user_from_api_key
|
|
return render json: {}, status: :unauthorized if user.nil?
|
|
|
|
uuid = params[:uuid]
|
|
exercise = Exercise.find_by(uuid:)
|
|
|
|
return render json: {uuid_found: false} if exercise.nil?
|
|
return render json: {uuid_found: true, update_right: false} unless ExercisePolicy.new(user, exercise).update?
|
|
|
|
render json: {uuid_found: true, update_right: true}
|
|
end
|
|
|
|
def import_task
|
|
tempfile = Tempfile.new('codeharbor_import.zip')
|
|
tempfile.write request.body.read.force_encoding('UTF-8')
|
|
tempfile.rewind
|
|
|
|
user = user_from_api_key
|
|
return render json: {}, status: :unauthorized if user.nil?
|
|
|
|
ActiveRecord::Base.transaction do
|
|
exercise = ::ProformaService::Import.call(zip: tempfile, user:)
|
|
exercise.save!
|
|
render json: {}, status: :created
|
|
end
|
|
rescue ProformaXML::ExerciseNotOwned
|
|
render json: {}, status: :unauthorized
|
|
rescue ProformaXML::ProformaError
|
|
render json: t('exercises.import_codeharbor.import_errors.invalid'), status: :bad_request
|
|
rescue StandardError => e
|
|
Sentry.capture_exception(e)
|
|
render json: t('exercises.import_codeharbor.import_errors.internal_error'), status: :internal_server_error
|
|
end
|
|
|
|
def user_from_api_key
|
|
authorization_header = request.headers['Authorization']
|
|
api_key = authorization_header&.split(' ')&.second
|
|
user_by_codeharbor_token(api_key)
|
|
end
|
|
|
|
private :user_from_api_key
|
|
|
|
def user_by_codeharbor_token(api_key)
|
|
link = CodeharborLink.find_by(api_key:)
|
|
link&.user
|
|
end
|
|
|
|
private :user_by_codeharbor_token
|
|
|
|
def exercise_params
|
|
@exercise_params ||= if params[:exercise].present?
|
|
params[:exercise].permit(
|
|
:description,
|
|
:execution_environment_id,
|
|
:file_id,
|
|
:instructions,
|
|
:submission_deadline,
|
|
:late_submission_deadline,
|
|
:public,
|
|
:unpublished,
|
|
:hide_file_tree,
|
|
:allow_file_creation,
|
|
:allow_auto_completion,
|
|
:title,
|
|
:expected_difficulty,
|
|
:tips,
|
|
files_attributes: file_attributes,
|
|
tag_ids: []
|
|
).merge(
|
|
user: current_user
|
|
)
|
|
end
|
|
end
|
|
|
|
private :exercise_params
|
|
|
|
def exercise_params_with_tags
|
|
myparam = exercise_params.presence || {}
|
|
checked_exercise_tags = @exercise_tags.select {|et| myparam[:tag_ids]&.include? et.tag.id.to_s }
|
|
removed_exercise_tags = @exercise_tags.reject {|et| myparam[:tag_ids]&.include? et.tag.id.to_s }
|
|
|
|
checked_exercise_tags.each do |et|
|
|
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
|
et.exercise = @exercise
|
|
end
|
|
|
|
myparam[:exercise_tags] = checked_exercise_tags
|
|
myparam.delete :tag_ids
|
|
myparam.delete :tips
|
|
removed_exercise_tags.map(&:destroy)
|
|
myparam
|
|
end
|
|
private :exercise_params_with_tags
|
|
|
|
def handle_file_uploads
|
|
if exercise_params
|
|
exercise_params[:files_attributes].try(:each) do |_index, file_attributes|
|
|
if file_attributes[:content].respond_to?(:read)
|
|
if FileType.find_by(id: file_attributes[:file_type_id]).try(:binary?)
|
|
file_attributes[:native_file] = file_attributes[:content]
|
|
file_attributes[:content] = nil
|
|
else
|
|
file_attributes[:content] = file_attributes[:content].read.detect_encoding!.encode.delete("\x00")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
private :handle_file_uploads
|
|
|
|
def handle_exercise_tips(tips_params)
|
|
return unless tips_params
|
|
|
|
begin
|
|
exercise_tips = JSON.parse(tips_params)
|
|
# Order is important to ensure no foreign key restraints are violated during delete
|
|
previous_exercise_tips = ExerciseTip.where(exercise: @exercise).select(:id).order(rank: :desc).ids
|
|
remaining_exercise_tips = update_exercise_tips exercise_tips, nil, 1
|
|
# Destroy initializes each object and then calls a *single* SQL DELETE
|
|
ExerciseTip.destroy(previous_exercise_tips - remaining_exercise_tips)
|
|
rescue JSON::ParserError => e
|
|
flash[:danger] = "JSON error: #{e.message}"
|
|
redirect_to(edit_exercise_path(@exercise))
|
|
end
|
|
end
|
|
|
|
private :handle_exercise_tips
|
|
|
|
def update_exercise_tips(exercise_tips, parent_exercise_tip_id, rank)
|
|
result = []
|
|
exercise_tips.each do |exercise_tip|
|
|
exercise_tip.symbolize_keys!
|
|
current_exercise_tip = ExerciseTip.find_or_initialize_by(id: exercise_tip[:id],
|
|
exercise: @exercise,
|
|
tip_id: exercise_tip[:tip_id])
|
|
current_exercise_tip.parent_exercise_tip_id = parent_exercise_tip_id
|
|
current_exercise_tip.rank = rank
|
|
rank += 1
|
|
unless current_exercise_tip.save
|
|
flash[:danger] = current_exercise_tip.errors.full_messages.join('. ')
|
|
redirect_to(edit_exercise_path(@exercise)) and break
|
|
end
|
|
|
|
children = update_exercise_tips exercise_tip[:children], current_exercise_tip.id, rank
|
|
rank += children.length
|
|
|
|
result << current_exercise_tip.id
|
|
result += children
|
|
end
|
|
result
|
|
end
|
|
|
|
private :update_exercise_tips
|
|
|
|
def implement # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
if session[:pg_id] && current_contributor.exercise != @exercise
|
|
# we are acting on behalf of a programming group
|
|
if current_user.admin?
|
|
session.delete(:pg_id)
|
|
@current_contributor = current_user
|
|
else
|
|
return redirect_back(
|
|
fallback_location: implement_exercise_path(current_contributor.exercise),
|
|
alert: t('exercises.implement.existing_programming_group', exercise: current_contributor.exercise.title)
|
|
)
|
|
end
|
|
elsif session[:pg_id].blank? && (pg = current_user.programming_groups.find_by(exercise: @exercise)) && pg.submissions.where(study_group_id: current_user.current_study_group_id).any?
|
|
# we are just acting on behalf of a single user who has already worked on this exercise as part of a programming group **in the context of the current study group**
|
|
session[:pg_id] = pg.id
|
|
@current_contributor = pg
|
|
elsif session[:pg_id].blank? && PairProgramming23Study.participate?(current_user, @exercise) && current_user.submissions.where(study_group_id: current_user.current_study_group_id, exercise: @exercise).none?
|
|
Event.find_or_create_by(category: 'pp_work_alone', user: current_user, exercise: @exercise, data: nil, file_id: nil)
|
|
current_user.pair_programming_waiting_users&.find_by(exercise: @exercise)&.update(status: :worked_alone)
|
|
end
|
|
|
|
user_solved_exercise = @exercise.solved_by?(current_contributor)
|
|
count_interventions_today = UserExerciseIntervention.where(user: current_user).where(created_at: Time.zone.now.beginning_of_day..).count
|
|
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) || user_got_intervention_in_exercise
|
|
|
|
if @embed_options[:disable_interventions]
|
|
@show_rfc_interventions = false
|
|
@show_break_interventions = false
|
|
@show_tips_interventions = false
|
|
else
|
|
show_intervention = (!user_solved_exercise && !user_got_enough_interventions).to_s
|
|
if @tips.present? && Java21Study.show_tips_intervention?(current_user, @exercise)
|
|
@show_tips_interventions = show_intervention
|
|
@show_break_interventions = false
|
|
@show_rfc_interventions = false
|
|
elsif Java21Study.show_break_intervention?(current_user, @exercise)
|
|
@show_tips_interventions = false
|
|
@show_break_interventions = show_intervention
|
|
@show_rfc_interventions = false
|
|
else
|
|
@show_tips_interventions = false
|
|
@show_break_interventions = false
|
|
@show_rfc_interventions = show_intervention
|
|
end
|
|
end
|
|
|
|
@embed_options[:disable_score] = true unless @exercise.teacher_defined_assessment?
|
|
|
|
@hide_rfc_button = @embed_options[:disable_rfc]
|
|
|
|
@submission = current_contributor.submissions.order(created_at: :desc).find_by(exercise: @exercise)
|
|
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:filepath)
|
|
@paths = collect_paths(@files)
|
|
end
|
|
|
|
def set_available_tips
|
|
# Order of elements is important and will be kept
|
|
available_tips = ExerciseTip.where(exercise: @exercise).order(rank: :asc).includes(:tip)
|
|
|
|
# Transform result set in a hash and prepare (temporary) children array.
|
|
# The children array will contain the sorted list of nested tips,
|
|
# shown for learners in the output sidebar with cards.
|
|
# Hash - Key: exercise_tip.id, value: exercise_tip Object loaded from database
|
|
nested_tips = available_tips.each_with_object({}) do |exercise_tip, hash|
|
|
exercise_tip.children = []
|
|
hash[exercise_tip.id] = exercise_tip
|
|
end
|
|
|
|
available_tips.each do |tip|
|
|
# A tip without a parent cannot be a children
|
|
next if tip.parent_exercise_tip_id.blank?
|
|
|
|
# Link tips if they are related
|
|
nested_tips[tip.parent_exercise_tip_id].children << tip
|
|
end
|
|
|
|
# Return an array with top-level tips
|
|
@tips = nested_tips.values.select {|tip| tip.parent_exercise_tip_id.nil? }
|
|
end
|
|
|
|
private :set_available_tips
|
|
|
|
def working_times
|
|
working_time_accumulated = @exercise.accumulated_working_time_for_only(current_contributor)
|
|
working_time_75_percentile = @exercise.get_quantiles([0.75]).first
|
|
render(json: {working_time_75_percentile:,
|
|
working_time_accumulated:})
|
|
end
|
|
|
|
def intervention
|
|
intervention = Intervention.find_by(name: params[:intervention_type])
|
|
if intervention.nil?
|
|
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
|
|
else
|
|
uei = UserExerciseIntervention.new(
|
|
user: current_contributor, exercise: @exercise, intervention:,
|
|
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_contributor)
|
|
)
|
|
uei.save
|
|
render(json: {success: 'true'})
|
|
end
|
|
end
|
|
|
|
def edit; end
|
|
|
|
def create
|
|
@exercise = Exercise.new(exercise_params&.except(:tips))
|
|
authorize!
|
|
collect_set_and_unset_exercise_tags
|
|
tips_params = exercise_params&.dig(:tips)
|
|
return if performed?
|
|
|
|
create_and_respond(object: @exercise, params: exercise_params_with_tags) do
|
|
# We first need to create the exercise before handling tips
|
|
handle_exercise_tips tips_params
|
|
end
|
|
end
|
|
|
|
def not_authorized_for_exercise(_exception)
|
|
return render_not_authorized unless current_user
|
|
return render_not_authorized unless %w[implement working_times intervention reload].include?(action_name)
|
|
|
|
if current_user.admin? || current_user.teacher?
|
|
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished?
|
|
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
|
redirect_to(@exercise, alert: t('exercises.implement.no_execution_environment')) if @exercise.execution_environment.blank?
|
|
else
|
|
render_not_authorized
|
|
end
|
|
end
|
|
private :not_authorized_for_exercise
|
|
|
|
def set_execution_environments
|
|
@execution_environments = ExecutionEnvironment.order(:name)
|
|
end
|
|
|
|
private :set_execution_environments
|
|
|
|
def set_exercise_and_authorize
|
|
@exercise = Exercise.includes(:exercise_tips, files: [:file_type]).find(params[:id])
|
|
authorize!
|
|
end
|
|
|
|
private :set_exercise_and_authorize
|
|
|
|
def set_external_user_and_authorize
|
|
if params[:external_user_id]
|
|
@external_user = ExternalUser.find(params[:external_user_id])
|
|
authorize!
|
|
end
|
|
end
|
|
|
|
private :set_external_user_and_authorize
|
|
|
|
def set_file_types
|
|
@file_types = FileType.order(:name)
|
|
end
|
|
|
|
private :set_file_types
|
|
|
|
def collect_set_and_unset_exercise_tags
|
|
@tags = policy_scope(Tag)
|
|
checked_exercise_tags = @exercise.exercise_tags
|
|
checked_tags = checked_exercise_tags.to_set(&:tag)
|
|
unchecked_tags = Tag.all.to_set.subtract checked_tags
|
|
@exercise_tags = checked_exercise_tags + unchecked_tags.collect do |tag|
|
|
ExerciseTag.new(exercise: @exercise, tag:)
|
|
end
|
|
end
|
|
|
|
private :collect_set_and_unset_exercise_tags
|
|
|
|
def update
|
|
handle_exercise_tips exercise_params&.dig(:tips)
|
|
return if performed?
|
|
|
|
update_and_respond(object: @exercise, params: exercise_params_with_tags)
|
|
end
|
|
|
|
def reload
|
|
# Returns JSON with original file content
|
|
end
|
|
|
|
def statistics
|
|
# Show general statistic page for specific exercise
|
|
contributor_statistics = {'InternalUser' => {}, 'ExternalUser' => {}, 'ProgrammingGroup' => {}}
|
|
|
|
query = Submission.select('contributor_id, contributor_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
|
|
.where(exercise_id: @exercise.id)
|
|
.group('contributor_id, contributor_type')
|
|
|
|
query = if policy(@exercise).detailed_statistics?
|
|
query
|
|
elsif !policy(@exercise).detailed_statistics? && current_user.study_groups.count.positive?
|
|
query.where(study_groups: current_user.study_groups.pluck(:id), cause: 'submit')
|
|
else
|
|
# e.g. internal user without any study groups, show no submissions
|
|
query.where('false')
|
|
end
|
|
|
|
query.each do |tuple|
|
|
contributor_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
|
|
end
|
|
|
|
render locals: {
|
|
contributor_statistics:,
|
|
}
|
|
end
|
|
|
|
def external_user_statistics
|
|
# Render statistics page for one specific external user
|
|
|
|
if policy(@exercise).detailed_statistics?
|
|
submissions = Submission.where(contributor: @external_user, exercise: @exercise)
|
|
.in_study_group_of(current_user)
|
|
.order(:created_at)
|
|
@show_autosaves = params[:show_autosaves] == 'true' || submissions.none? {|s| s.cause != 'autosave' }
|
|
submissions = submissions.where.not(cause: 'autosave') unless @show_autosaves
|
|
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id,
|
|
@exercise.id)
|
|
@all_events = (submissions + interventions).sort_by(&:created_at)
|
|
@deltas = @all_events.map.with_index do |item, index|
|
|
delta = item.created_at - @all_events[index - 1].created_at if index.positive?
|
|
delta.nil? || (delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS) ? 0 : delta
|
|
end
|
|
@working_times_until = []
|
|
@all_events.each_with_index do |_, index|
|
|
@working_times_until.push((format_time_difference(@deltas[0..index].sum) if index.positive?))
|
|
end
|
|
else
|
|
final_submissions = Submission.where(contributor: @external_user,
|
|
exercise_id: @exercise.id).in_study_group_of(current_user).final
|
|
submissions = []
|
|
%i[before_deadline within_grace_period after_late_deadline].each do |filter|
|
|
relevant_submission = final_submissions.send(filter).latest
|
|
submissions.push relevant_submission if relevant_submission.present?
|
|
end
|
|
@all_events = submissions
|
|
end
|
|
|
|
render 'exercises/external_users/statistics'
|
|
end
|
|
|
|
def submit
|
|
@submission = Submission.create(submission_params)
|
|
@submission.calculate_score(current_user)
|
|
|
|
if @submission.users.map {|user| lti_outcome_service?(@submission.exercise, user, @submission.study_group_id) }.any?
|
|
transmit_lti_score
|
|
else
|
|
redirect_after_submit
|
|
end
|
|
rescue Runner::Error => e
|
|
Rails.logger.debug { "Runner error while submitting submission #{@submission.id}: #{e.message}" }
|
|
respond_to do |format|
|
|
format.html { redirect_to(implement_exercise_path(@submission.exercise)) }
|
|
format.json { render(json: {danger: I18n.t('exercises.editor.depleted'), status: :container_depleted}) }
|
|
end
|
|
end
|
|
|
|
def transmit_lti_score
|
|
responses = send_scores(@submission)
|
|
messages = {}
|
|
failed_users = []
|
|
|
|
responses.each do |response|
|
|
if Lti::ERROR_STATUS.include? response[:status]
|
|
failed_users << response[:user]
|
|
elsif response[:score_sent] != @submission.normalized_score # the score was sent successfully, but received too late
|
|
messages[:warning] = I18n.t('exercises.submit.too_late')
|
|
end
|
|
end
|
|
|
|
if failed_users.size == responses.size # all submissions failed
|
|
messages[:danger] = I18n.t('exercises.submit.failure')
|
|
elsif failed_users.size.positive? # at least one submission failed
|
|
messages[:warning] = [[messages[:warning]], I18n.t('exercises.submit.warning_not_for_all_users_submitted', user: failed_users.join(', '))].join('<br><br>')
|
|
messages[:warning] = "#{messages[:warning]}\n\n#{I18n.t('exercises.submit.warning_not_for_all_users_submitted', user: failed_users.join(', '))}".strip
|
|
else
|
|
messages.each do |type, message_text|
|
|
flash.now[type] = message_text
|
|
flash.keep(type)
|
|
end
|
|
return redirect_after_submit
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.html { redirect_to(implement_exercise_path(@submission.exercise), **messages) }
|
|
format.json { render(json: messages) } # We must not change the HTTP status code here, since otherwise the custom message is not displayed.
|
|
end
|
|
end
|
|
|
|
private :transmit_lti_score
|
|
|
|
def destroy
|
|
destroy_and_respond(object: @exercise)
|
|
end
|
|
|
|
def study_group_dashboard
|
|
authorize!
|
|
@study_group_id = params[:study_group_id]
|
|
@request_for_comments = RequestForComment
|
|
.where(exercise: @exercise).includes(:submission)
|
|
.where(submissions: {study_group_id: @study_group_id})
|
|
.order(created_at: :desc)
|
|
|
|
@graph_data = @exercise.get_working_times_for_study_group(@study_group_id)
|
|
end
|
|
end
|