Add submission deadline to exercises and allow teachers to view their submissions

This commit is contained in:
Sebastian Serth
2020-05-07 17:45:53 +02:00
parent 4c571c4fb2
commit 914eeb6035
21 changed files with 456 additions and 245 deletions

View File

@ -7,6 +7,13 @@
width: 100% !important;
}
.chosen-inline {
.chosen-container {
min-width: unset !important;
width: unset !important;
}
}
.code-field {
font-family: monospace;
}

View File

@ -62,6 +62,18 @@ tr.highlight {
border-top: 2px solid rgba(222,0,0,1);
}
.before_deadline {
background-color: #DAF7A6;
}
.within_grace_period {
background-color: #F7DC6F;
}
.after_late_deadline {
background-color: #EC7063;
}
/////////////////////////////////////////////////////////////////////////////////////////////
// StatisticsController:

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ExercisesController < ApplicationController
include CommonBehavior
include Lti
@ -5,16 +7,16 @@ class ExercisesController < ApplicationController
include SubmissionScoring
include TimeHelper
before_action :handle_file_uploads, only: [:create, :update]
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :requests_for_comments, :study_group_dashboard, :export_external_check, :export_external_confirm]
before_action :handle_file_uploads, only: %i[create update]
before_action :set_execution_environments, only: %i[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]
before_action :set_external_user_and_authorize, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update]
before_action :set_file_types, only: %i[create edit new update]
before_action :set_course_token, only: [:implement]
skip_before_action :verify_authenticity_token, only: [:import_exercise, :import_uuid_check, :export_external_confirm]
skip_after_action :verify_authorized, only: [:import_exercise, :import_uuid_check, :export_external_confirm]
skip_after_action :verify_policy_scoped, only: [:import_exercise, :import_uuid_check, :export_external_confirm], raise: false
skip_before_action :verify_authenticity_token, only: %i[import_exercise import_uuid_check export_external_confirm]
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check export_external_confirm]
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check export_external_confirm], raise: false
def authorize!
authorize(@exercise || @exercises)
@ -31,13 +33,13 @@ class ExercisesController < ApplicationController
def experimental_courses
{
java17: "702cbd2a-c84c-4b37-923a-692d7d1532d0",
java1: "0ea88ea9-979a-44a3-b0e4-84ba58e5a05e"
java17: '702cbd2a-c84c-4b37-923a-692d7d1532d0',
java1: '0ea88ea9-979a-44a3-b0e4-84ba58e5a05e'
}
end
def experimental_course?(course_token)
experimental_courses.has_value?(course_token)
experimental_courses.value?(course_token)
end
def batch_update
@ -76,18 +78,18 @@ class ExercisesController < ApplicationController
def create
@exercise = Exercise.new(exercise_params)
collect_set_and_unset_exercise_tags
myparam = exercise_params.present? ? exercise_params : { }
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 }
myparam = exercise_params.present? ? exercise_params : {}
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 }
for et in checked_exercise_tags
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
removed_exercise_tags.map {|et| et.destroy}
removed_exercise_tags.map(&:destroy)
authorize!
create_and_respond(object: @exercise)
@ -112,12 +114,12 @@ class ExercisesController < ApplicationController
def requests_for_comments
authorize!
@search = RequestForComment
.with_last_activity
.where(exercise: @exercise)
.ransack(params[:q])
.with_last_activity
.where(exercise: @exercise)
.ransack(params[:q])
@request_for_comments = @search.result
.order('last_comment DESC')
.paginate(page: params[:page])
.order('last_comment DESC')
.paginate(page: params[:page])
render 'request_for_comments/index'
end
@ -210,7 +212,7 @@ class ExercisesController < ApplicationController
private :user_by_codeharbor_token
def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :unpublished, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) 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, files_attributes: file_attributes, tag_ids: []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present?
end
private :exercise_params
@ -232,56 +234,55 @@ class ExercisesController < ApplicationController
private :handle_file_uploads
def implement
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO TESTESTEST
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO: TESTESTEST
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
user_solved_exercise = @exercise.has_user_solved(current_user)
count_interventions_today = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count
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 or user_got_intervention_in_exercise
(user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day) || user_got_intervention_in_exercise
unless @embed_options[:disable_interventions]
@show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s
if @embed_options[:disable_interventions]
@show_rfc_interventions = false
@show_break_interventions = false
else
@show_rfc_interventions = false
@show_rfc_interventions = (!user_solved_exercise && !user_got_enough_interventions).to_s
@show_break_interventions = false
end
@hide_rfc_button = @embed_options[:disable_rfc]
@search = Search.new
@search.exercise = @exercise
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
@paths = collect_paths(@files)
if current_user.respond_to? :external_id
@user_id = current_user.external_id
else
@user_id = current_user.id
end
@user_id = if current_user.respond_to? :external_id
current_user.external_id
else
current_user.id
end
end
def set_course_token
lti_parameters = LtiParameter.where(external_users_id: current_user.id,
exercises_id: @exercise.id).last
exercises_id: @exercise.id).last
if lti_parameters
lti_json = lti_parameters.lti_parameters["launch_presentation_return_url"]
lti_json = lti_parameters.lti_parameters['launch_presentation_return_url']
@course_token =
unless lti_json.nil?
if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/)
match.captures.first
else
""
end
if lti_json.nil?
''
else
if match = lti_json.match(%r{^.*courses/([a-z0-9\-]+)/sections})
match.captures.first
else
""
''
end
end
else
# no consumer, therefore implementation with internal user
@course_token = "702cbd2a-c84c-4b37-923a-692d7d1532d0"
@course_token = '702cbd2a-c84c-4b37-923a-692d7d1532d0'
end
end
private :set_course_token
@ -294,14 +295,15 @@ class ExercisesController < ApplicationController
def intervention
intervention = Intervention.find_by_name(params[:intervention_type])
unless intervention.nil?
if intervention.nil?
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
else
uei = UserExerciseIntervention.new(
user: current_user, exercise: @exercise, intervention: intervention,
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user))
user: current_user, exercise: @exercise, intervention: intervention,
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user)
)
uei.save
render(json: {success: 'true'})
else
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
end
end
@ -310,9 +312,9 @@ class ExercisesController < ApplicationController
search = Search.new(user: current_user, exercise: @exercise, search: search_text)
begin search.save
render(json: {success: 'true'})
rescue
render(json: {success: 'false', error: "could not save search: #{$!}"})
render(json: {success: 'true'})
rescue StandardError
render(json: {success: 'false', error: "could not save search: #{$ERROR_INFO}"})
end
end
@ -386,9 +388,9 @@ class ExercisesController < ApplicationController
@search = policy_scope(Tag).ransack(params[:q])
@tags = @search.result.order(:name)
checked_exercise_tags = @exercise.exercise_tags
checked_tags = checked_exercise_tags.collect{|e| e.tag}.to_set
checked_tags = checked_exercise_tags.collect(&:tag).to_set
unchecked_tags = Tag.all.to_set.subtract checked_tags
@exercise_tags = checked_exercise_tags + unchecked_tags.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)}
@exercise_tags = checked_exercise_tags + unchecked_tags.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag) }
end
private :collect_set_and_unset_exercise_tags
@ -401,27 +403,39 @@ class ExercisesController < ApplicationController
end
def statistics
if(@external_user)
if @external_user
authorize(@external_user, :statistics?)
@submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
interventions = UserExerciseIntervention.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)
@all_events = (@submissions + interventions).sort_by { |a| a.created_at }
@deltas = @all_events.map.with_index do |item, index|
delta = item.created_at - @all_events[index - 1].created_at if index > 0
if delta == nil or delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS then 0 else delta end
end
@working_times_until = []
@all_events.each_with_index do |_, index|
@working_times_until.push((format_time_difference(@deltas[0..index].inject(:+)) if index > 0))
if policy(@exercise).detailed_statistics?
@submissions = Submission.where(user: @external_user, exercise_id: @exercise.id).in_study_group_of(current_user).order('created_at')
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 > 0
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].inject(:+)) if index > 0))
end
else
latest_submissions = Submission.where(user: @external_user, exercise_id: @exercise.id).in_study_group_of(current_user).final.latest
relevant_submissions = latest_submissions.before_deadline.or(latest_submissions.within_grace_period).or(latest_submissions.after_late_deadline)
@submissions = relevant_submissions.sort_by(&:created_at)
@all_events = @submissions
end
render 'exercises/external_users/statistics'
else
user_statistics = {}
additional_filter = if policy(@exercise).detailed_statistics?
''
else
"AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"
end
query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs
FROM submissions WHERE exercise_id = #{@exercise.id} GROUP BY
FROM submissions WHERE exercise_id = #{@exercise.id} #{additional_filter} GROUP BY
user_id;"
ApplicationRecord.connection.execute(query).each do |tuple|
user_statistics[tuple["user_id"].to_i] = tuple
user_statistics[tuple['user_id'].to_i] = tuple
end
render locals: {
user_statistics: user_statistics
@ -441,7 +455,7 @@ class ExercisesController < ApplicationController
end
def transmit_lti_score
::NewRelic::Agent.add_custom_attributes({ submission: @submission.id, normalized_score: @submission.normalized_score })
::NewRelic::Agent.add_custom_attributes({submission: @submission.id, normalized_score: @submission.normalized_score})
response = send_score(@submission.exercise_id, @submission.normalized_score, @submission.user_id)
if response[:status] == 'success'
@ -458,17 +472,17 @@ class ExercisesController < ApplicationController
def update
collect_set_and_unset_exercise_tags
myparam = exercise_params
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 = @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 }
for et in checked_exercise_tags
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
removed_exercise_tags.map {|et| et.destroy}
removed_exercise_tags.map(&:destroy)
update_and_respond(object: @exercise, params: myparam)
end
@ -511,21 +525,21 @@ class ExercisesController < ApplicationController
clear_lti_session_data(@submission.exercise_id, @submission.user_id, session[:consumer_id])
respond_to do |format|
format.html {redirect_to(rfc)}
format.json {render(json: {redirect: url_for(rfc)})}
format.html { redirect_to(rfc) }
format.json { render(json: {redirect: url_for(rfc)}) }
end
return
end
end
else
# redirect to feedback page if score is less than 100 percent
if @exercise.needs_more_feedback? && !@embed_options[:disable_redirect_to_feedback]
clear_lti_session_data(@submission.exercise_id, @submission.user_id, session[:consumer_id])
redirect_to_user_feedback
else
redirect_to_lti_return_path
end
return
if @exercise.needs_more_feedback? && !@embed_options[:disable_redirect_to_feedback]
clear_lti_session_data(@submission.exercise_id, @submission.user_id, session[:consumer_id])
redirect_to_user_feedback
else
redirect_to_lti_return_path
end
return
end
redirect_to_lti_return_path
end
@ -547,12 +561,11 @@ class ExercisesController < ApplicationController
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)
@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

View File

@ -6,7 +6,7 @@ class ExternalUsersController < ApplicationController
def index
@search = ExternalUser.ransack(params[:q])
@users = @search.result.includes(:consumer).paginate(page: params[:page])
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page])
authorize!
end
@ -41,6 +41,7 @@ class ExternalUsersController < ApplicationController
FROM submissions
WHERE user_id = #{@user.id}
AND user_type = 'ExternalUser'
#{!current_user.admin? ? "AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'" : ''}
GROUP BY exercise_id,
user_id,
id

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'nokogiri'
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
require File.expand_path('../../lib/active_model/validations/boolean_presence_validator', __dir__)
class Exercise < ApplicationRecord
include Context
@ -25,11 +27,12 @@ class Exercise < ApplicationRecord
has_many :external_users, source: :user, source_type: 'ExternalUser', through: :submissions
has_many :internal_users, source: :user, source_type: 'InternalUser', through: :submissions
alias_method :users, :external_users
alias users external_users
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
validate :valid_main_file?
validate :valid_submission_deadlines?
validates :description, presence: true
validates :execution_environment, presence: true, if: -> { !unpublished? }
validates :public, boolean_presence: true
@ -44,7 +47,7 @@ class Exercise < ApplicationRecord
MAX_EXERCISE_FEEDBACKS = 20
def average_percentage
if average_score and maximum_score != 0.0 and submissions.exists?(cause: 'submit')
if average_score && (maximum_score != 0.0) && submissions.exists?(cause: 'submit')
(average_score / maximum_score * 100).round(2)
else
0
@ -68,11 +71,13 @@ class Exercise < ApplicationRecord
def average_number_of_submissions
user_count = internal_users.distinct.count + external_users.distinct.count
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
user_count == 0 ? 0 : submissions.count / user_count.to_f
end
def time_maximum_score(user)
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC, created_at ASC").first.created_at rescue Time.zone.at(0)
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC, created_at ASC').first.created_at
rescue StandardError
Time.zone.at(0)
end
def user_working_time_query
@ -100,7 +105,7 @@ class Exercise < ApplicationRecord
end
def study_group_working_time_query(exercise_id, study_group_id, additional_filter)
"""
''"
WITH working_time_between_submissions AS (
SELECT submissions.user_id,
submissions.user_type,
@ -193,7 +198,7 @@ class Exercise < ApplicationRecord
FROM working_times_with_index
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
ORDER BY index, score ASC;
"""
"''
end
def get_working_times_for_study_group(study_group_id, user = nil)
@ -202,36 +207,34 @@ class Exercise < ApplicationRecord
max_bucket = 100
maximum_score = self.maximum_score
if user.blank?
additional_filter = ''
else
additional_filter = "AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
end
additional_filter = if user.blank?
''
else
"AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
end
results = self.class.connection.execute(study_group_working_time_query(id, study_group_id, additional_filter)).each do |tuple|
if maximum_score > 0.0 && tuple['score'] <= maximum_score
bucket = (tuple['score'] / maximum_score * max_bucket).round
else
bucket = max_bucket # maximum_score / maximum_score will always be 1
end
bucket = if maximum_score > 0.0 && tuple['score'] <= maximum_score
(tuple['score'] / maximum_score * max_bucket).round
else
max_bucket # maximum_score / maximum_score will always be 1
end
user_progress[bucket] ||= []
additional_user_data[bucket] ||= []
additional_user_data[max_bucket + 1] ||= []
user_progress[bucket][tuple['index']] = tuple["working_time_per_score"]
additional_user_data[bucket][tuple['index']] = {start_time: tuple["start_time"], score: tuple["score"]}
user_progress[bucket][tuple['index']] = tuple['working_time_per_score']
additional_user_data[bucket][tuple['index']] = {start_time: tuple['start_time'], score: tuple['score']}
additional_user_data[max_bucket + 1][tuple['index']] = {id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
end
if results.ntuples > 0
first_index = results[0]['index']
last_index = results[results.ntuples-1]['index']
last_index = results[results.ntuples - 1]['index']
buckets = last_index - first_index
user_progress.each do |timings_array|
if timings_array.present? && timings_array.length != buckets + 1
timings_array[buckets] = nil
end
timings_array[buckets] = nil if timings_array.present? && timings_array.length != buckets + 1
end
end
@ -239,8 +242,8 @@ class Exercise < ApplicationRecord
end
def get_quantiles(quantiles)
quantiles_str = "[" + quantiles.join(",") + "]"
result = self.class.connection.execute("""
quantiles_str = '[' + quantiles.join(',') + ']'
result = self.class.connection.execute(''"
WITH working_time AS
(
SELECT user_id,
@ -346,13 +349,12 @@ class Exercise < ApplicationRecord
exercise_id )
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
FROM result
""")
"'')
if result.count > 0
quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight}
quantiles.each_with_index.map { |_q, i| Time.parse(result[i]['unnest']).seconds_since_midnight }
else
quantiles.map{|q| 0}
quantiles.map { |_q| 0 }
end
end
def retrieve_working_time_statistics
@ -363,72 +365,74 @@ class Exercise < ApplicationRecord
end
def average_working_time
self.class.connection.execute("""
self.class.connection.execute(''"
SELECT avg(working_time) as average_time
FROM
(#{user_working_time_query}) AS baz;
""").first['average_time']
"'').first['average_time']
end
def average_working_time_for(user_id)
if @working_time_statistics == nil
retrieve_working_time_statistics()
end
@working_time_statistics[user_id]["working_time"]
retrieve_working_time_statistics if @working_time_statistics.nil?
@working_time_statistics[user_id]['working_time']
end
def accumulated_working_time_for_only(user)
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
Time.parse(self.class.connection.execute("""
WITH WORKING_TIME AS
(SELECT user_id,
id,
exercise_id,
max(score) AS max_score,
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
ORDER BY created_at)) AS working_time
FROM submissions
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
GROUP BY user_id, id, exercise_id),
MAX_POINTS AS
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
begin
Time.parse(self.class.connection.execute(''"
WITH WORKING_TIME AS
(SELECT user_id,
id,
exercise_id,
max(score) AS max_score,
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
ORDER BY created_at)) AS working_time
FROM submissions
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
GROUP BY user_id, id, exercise_id),
MAX_POINTS AS
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
-- filter for rows containing max points
TIME_MAX_SCORE AS
(SELECT *
FROM WORKING_TIME W1, MAX_POINTS MS
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
-- filter for rows containing max points
TIME_MAX_SCORE AS
(SELECT *
FROM WORKING_TIME W1, MAX_POINTS MS
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
-- find row containing the first time max points
FIRST_TIME_MAX_SCORE AS
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
FROM (
SELECT id,USER_id,exercise_id,max_score,working_time,
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
FROM TIME_MAX_SCORE) T
WHERE rn = 1),
-- find row containing the first time max points
FIRST_TIME_MAX_SCORE AS
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
FROM (
SELECT id,USER_id,exercise_id,max_score,working_time,
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
FROM TIME_MAX_SCORE) T
WHERE rn = 1),
TIMES_UNTIL_MAX_POINTS AS (
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
TIMES_UNTIL_MAX_POINTS AS (
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
-- if user never makes it to max points, take all times
ALL_WORKING_TIMES_UNTIL_MAX AS
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
UNION ALL
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
-- if user never makes it to max points, take all times
ALL_WORKING_TIMES_UNTIL_MAX AS
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
UNION ALL
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
FILTERED_TIMES_UNTIL_MAX AS
(
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
FROM ALL_WORKING_TIMES_UNTIL_MAX
)
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
""").first["working_time"]).seconds_since_midnight rescue 0
FILTERED_TIMES_UNTIL_MAX AS
(
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
FROM ALL_WORKING_TIMES_UNTIL_MAX
)
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
"'').first['working_time']).seconds_since_midnight
rescue StandardError
0
end
end
def duplicate(attributes = {})
@ -453,7 +457,8 @@ class Exercise < ApplicationRecord
return 'main_file'
elsif (file_class == 'internal') && (comment == 'main')
end
return 'regular_file'
'regular_file'
end
def from_proforma_xml(xml_string)
@ -467,23 +472,23 @@ class Exercise < ApplicationRecord
description: description,
instructions: description
}
task_node.xpath('p:files/p:file').all? { |file|
task_node.xpath('p:files/p:file').all? do |file|
file_name_split = file.xpath('@filename').first.value.split('.')
file_class = file.xpath('@class').first.value
role = determine_file_role_from_proforma_file(task_node, file)
feedback_message_nodes = task_node.xpath("p:tests/p:test/p:test-configuration/c:feedback-message/text()")
feedback_message_nodes = task_node.xpath('p:tests/p:test/p:test-configuration/c:feedback-message/text()')
files.build({
name: file_name_split.first,
content: file.xpath('text()').first.content,
read_only: false,
hidden: file_class == 'internal',
role: role,
feedback_message: (role == 'teacher_defined_test') ? feedback_message_nodes.first.content : nil,
file_type: FileType.where(
file_extension: ".#{file_name_split.second}"
).take
})
}
name: file_name_split.first,
content: file.xpath('text()').first.content,
read_only: false,
hidden: file_class == 'internal',
role: role,
feedback_message: role == 'teacher_defined_test' ? feedback_message_nodes.first.content : nil,
file_type: FileType.where(
file_extension: ".#{file_name_split.second}"
).take
})
end
self.execution_environment_id = 1
end
@ -495,7 +500,11 @@ class Exercise < ApplicationRecord
def maximum_score(user = nil)
if user
# FIXME: where(user: user) will not work here!
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0
begin
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC').first.score || 0
rescue StandardError
0
end
else
files.teacher_defined_tests.sum(:weight)
end
@ -510,7 +519,7 @@ class Exercise < ApplicationRecord
end
def finishers
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w(submit assess)}).distinct
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w[submit assess]}).distinct
end
def set_default_values
@ -523,12 +532,22 @@ class Exercise < ApplicationRecord
end
def valid_main_file?
if files.main_files.count > 1
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
end
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file')) if files.main_files.count > 1
end
private :valid_main_file?
def valid_submission_deadlines?
return unless submission_deadline.present? || late_submission_deadline.present?
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_alone')) if late_submission_deadline.present? && submission_deadline.blank?
if submission_deadline.present? && late_submission_deadline.present? &&
late_submission_deadline < submission_deadline
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_before_submission_deadline'))
end
end
private :valid_submission_deadlines?
def needs_more_feedback?
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
end
@ -543,5 +562,4 @@ class Exercise < ApplicationRecord
WHERE exercise_id = #{id}
) AS t ON t.fv = submissions.id").distinct
end
end

View File

@ -15,11 +15,22 @@ class Submission < ApplicationRecord
has_many :structured_errors
has_many :comments, through: :files
belongs_to :external_users, -> { where(submissions: {user_type: 'ExternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'ExternalUser', optional: true
belongs_to :internal_users, -> { where(submissions: {user_type: 'InternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'InternalUser', optional: true
delegate :execution_environment, to: :exercise
scope :final, -> { where(cause: 'submit') }
scope :intermediate, -> { where.not(cause: 'submit') }
scope :before_deadline, -> { joins(:exercise).where('submissions.updated_at <= exercises.submission_deadline OR exercises.submission_deadline IS NULL') }
scope :within_grace_period, -> { joins(:exercise).where('(submissions.updated_at > exercises.submission_deadline) AND (submissions.updated_at <= exercises.late_submission_deadline OR exercises.late_submission_deadline IS NULL)') }
scope :after_late_deadline, -> { joins(:exercise).where('submissions.updated_at > exercises.late_submission_deadline') }
scope :latest, -> { order(updated_at: :desc).limit(1) }
scope :in_study_group_of, ->(user) { where(study_group_id: user.study_groups) unless user.admin? }
validates :cause, inclusion: {in: CAUSES}
validates :exercise_id, presence: true
@ -29,6 +40,7 @@ class Submission < ApplicationRecord
def build_files_hash(files, attribute)
files.map(&attribute.to_proc).zip(files).to_h
end
private :build_files_hash
def collect_files
@ -42,7 +54,7 @@ class Submission < ApplicationRecord
end
def normalized_score
::NewRelic::Agent.add_custom_attributes({ unnormalized_score: score })
::NewRelic::Agent.add_custom_attributes({unnormalized_score: score})
if !score.nil? && !exercise.maximum_score.nil? && (exercise.maximum_score > 0)
score / exercise.maximum_score
else
@ -62,6 +74,32 @@ class Submission < ApplicationRecord
Submission.model_name.human
end
def before_deadline?
if exercise.submission_deadline.present?
updated_at <= exercise.submission_deadline
else
false
end
end
def within_grace_period?
if exercise.submission_deadline.present? && exercise.late_submission_deadline.present?
updated_at > exercise.submission_deadline && updated_at <= exercise.late_submission_deadline
else
false
end
end
def after_late_deadline?
if exercise.late_submission_deadline.present?
updated_at > exercise.late_submission_deadline
elsif exercise.submission_deadline.present?
updated_at > exercise.submission_deadline
else
false
end
end
def redirect_to_feedback?
((user_id + exercise.created_at.to_i) % 10 == 1) && exercise.needs_more_feedback?
end
@ -71,6 +109,6 @@ class Submission < ApplicationRecord
end
def unsolved_rfc
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order("RANDOM()").find { | rfc_element |( (rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && (!rfc_element.question.empty?)) }
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order("RANDOM()").find { |rfc_element| ((rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && (!rfc_element.question.empty?)) }
end
end

View File

@ -19,6 +19,8 @@ class User < ApplicationRecord
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
scope :in_study_group_of, ->(user) { joins(:study_group_memberships).where(study_group_memberships: {study_group_id: user.study_groups}) unless user.admin? }
ROLES.each do |role|
define_method("#{role}?") { try(:role) == role }
end

View File

@ -33,9 +33,14 @@ class ApplicationPolicy
users_in_same_study_group = study_group.users
elsif @record.respond_to? :users # e.g. study_group
users_in_same_study_group = @record.users
else # e.g. exercise
elsif @record.respond_to? :user # e.g. exercise
study_groups = @record.user.study_groups
users_in_same_study_group = study_groups.collect(&:users).flatten
elsif @record.respond_to? :study_groups # e.g. user
study_groups = @record.study_groups
users_in_same_study_group = study_groups.collect(&:users).flatten
else
return false
end
users_in_same_study_group.include? @user

View File

@ -11,6 +11,14 @@ class ExercisePolicy < AdminOrAuthorPolicy
admin? || teacher_in_study_group?
end
def submission_statistics?
admin? || teacher_in_study_group?
end
def detailed_statistics?
admin?
end
[:clone?, :destroy?, :edit?, :update?, :export_external_check?, :export_external_confirm?].each do |action|
define_method(action) { admin? || teacher_in_study_group? || author? }
end

View File

@ -1,6 +1,14 @@
class ExternalUserPolicy < AdminOnlyPolicy
def index?
admin? || teacher?
end
def show?
admin? || teacher_in_study_group?
end
def statistics?
admin?
admin? || teacher_in_study_group?
end
def tag_statistics?

View File

@ -16,6 +16,16 @@
= f.label(:instructions)
= f.hidden_field(:instructions)
.form-control.markdown
.form-group
= f.label(:submission_deadline)
.chosen-inline
= f.datetime_select(:submission_deadline, include_blank: true)
.help-block.form-text == t('.hints.submission_deadline')
.form-group
= f.label(:late_submission_deadline)
.chosen-inline
= f.datetime_select(:late_submission_deadline, include_blank: true)
.help-block.form-text == t('.hints.late_submission_deadline')
.form-check
label.form-check-label
= f.check_box(:public, class: 'form-check-input')

View File

@ -1,4 +1,4 @@
h1 = "#{@exercise} (external user #{link_to_if(policy(@external_user).show?, @external_user.displayname, @external_user)})"
h1 = "#{@exercise} (external user #{link_to_if(policy(@external_user).show?, @external_user.displayname, @external_user)})".html_safe
- current_submission = @submissions.first
- if current_submission
- initial_files = current_submission.files.to_a
@ -36,32 +36,54 @@ h1 = "#{@exercise} (external user #{link_to_if(policy(@external_user).show?, @ex
table.table
thead
tr
- ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
th.header = t(title)
th.header = t('.time')
th.header = t('.cause')
th.header = t('.score')
th.header = t('.tests')
th.header = t('.time_difference') if policy(@exercise).detailed_statistics?
tbody
- @all_events.each_with_index do |this, index|
- highlight = (index > 0 and @deltas[index] == 0 and this.created_at.to_s != @all_events[index - 1].created_at.to_s)
tr data-id=this.id class=('highlight' if highlight)
- row_classes = ''
- row_classes += ' highlight' if highlight
- row_classes += ' before_deadline' if this.is_a?(Submission) && this.before_deadline?
- row_classes += ' within_grace_period' if this.is_a?(Submission) && this.within_grace_period?
- row_classes += ' after_late_deadline' if this.is_a?(Submission) && this.after_late_deadline?
tr data-id=this.id class=row_classes
td.clickable = this.created_at.strftime("%F %T")
- if this.is_a?(Submission)
td = this.cause
td = this.score
td
td.align-middle
-this.testruns.each do |run|
- if run.passed
.unit-test-result.positive-result title=run.output
- else
.unit-test-result.unknown-result title=run.output
td = @working_times_until[index] if index > 0
td = @working_times_until[index] if index > 0 if policy(@exercise).detailed_statistics?
- elsif this.is_a? UserExerciseIntervention
td = this.intervention.name
td =
td =
td = @working_times_until[index] if index > 0
p = t('.addendum', delta: StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS / 60)
.d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
div#progress_chart.col-lg-12
.graph-functions-2
td = @working_times_until[index] if index > 0 if policy(@exercise).detailed_statistics?
small
b
= t('.legend')
.container.px-0.border
.row.w-100.mx-0
.col-sm-3.py-2
= t('.no_deadline')
.col-sm-3.before_deadline.py-2
= t('.before_deadline')
.col-sm-3.within_grace_period.py-2
= t('.within_grace_period')
.col-sm-3.after_late_deadline.py-2
= t('.after_late_deadline')
- if current_user.try(:admin?)
p = t('.addendum', delta: StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS / 60)
.d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
div#progress_chart.col-lg-12
.graph-functions-2
- else
p = t('.no_data_available')

View File

@ -25,6 +25,8 @@ h1
= row(label: 'exercise.execution_environment', value: link_to_if(@exercise.execution_environment && policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment))
/= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions))
= row(label: 'exercise.maximum_score', value: @exercise.maximum_score)
= row(label: 'exercise.submission_deadline', value: @exercise.submission_deadline)
= row(label: 'exercise.late_submission_deadline', value: @exercise.late_submission_deadline)
= row(label: 'exercise.public', value: @exercise.public?)
= row(label: 'exercise.unpublished', value: @exercise.unpublished?)
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)

View File

@ -25,12 +25,13 @@ h1 = @exercise
p = @exercise.average_working_time
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label|
strong = label
-if symbol==:external_users
-working_time_array = []
- if symbol==:internal_users && current_user.admin?
strong = label
- if symbol==:external_users
- working_time_array = []
- @exercise.send(symbol).distinct().each do |user|
-working_time = @exercise.average_working_time_for(user.id) or 0
-working_time_array.push working_time
- working_time = @exercise.average_working_time_for(user.id) or 0
- working_time_array.push working_time
hr
.d-none#data data-working-time=ActiveSupport::JSON.encode(working_time_array)
.working-time-graphs
@ -38,19 +39,26 @@ h1 = @exercise
hr
div#chart_2
hr
- if current_user.admin?
- submissions = Submission.where(user: @exercise.send(symbol).distinct, exercise: @exercise).in_study_group_of(current_user)
- if !policy(@exercise).detailed_statistics?
- submissions = submissions.final
- any_viewable_submission = submissions.first
- if any_viewable_submission
.table-responsive
table.table.table-striped.sortable
thead
tr
- ['.user', '.score', '.runs', '.worktime'].each do |title|
th.header = t(title)
th.header = t('.user')
th.header = t('.score')
th.header = t('.runs') if policy(@exercise).detailed_statistics?
th.header = t('.worktime') if policy(@exercise).detailed_statistics?
tbody
- @exercise.send(symbol).distinct().each do |user|
- users = symbol.to_s.classify.constantize.where(id: submissions.joins(symbol).group(:user_id).select(:user_id).distinct)
- users.each do |user|
- if user_statistics[user.id] then us = user_statistics[user.id] else us = {"maximum_score" => nil, "runs" => nil}
- label = "#{user.displayname}"
tr
td = link_to_if symbol==:external_users && policy(user).statistics?, label, {controller: "exercises", action: "statistics", external_user_id: user.id, id: @exercise.id}
td = us['maximum_score'] or 0
td = us['runs']
td = @exercise.average_working_time_for(user.id) or 0
td = us['runs'] if policy(@exercise).detailed_statistics?
td = @exercise.average_working_time_for(user.id) or 0 if policy(@exercise).detailed_statistics?

View File

@ -1,7 +1,7 @@
h1 = @user.displayname
= row(label: 'external_user.name', value: @user.name)
= row(label: 'external_user.email', value: @user.email)
= row(label: 'external_user.email', value: @user.email) if current_user.admin?
= row(label: 'external_user.external_id') do
code
= @user.external_id
@ -10,10 +10,11 @@ h1 = @user.displayname
h4.mt-4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user)) if policy(@user).statistics?
h4.mt-4 = t('.tag_statistics')
#loading
.spinner
= t('.loading_tag_statistics')
#no-elements
= t('.empty_tag_statistics')
#tag-grid
- if current_user.admin?
h4.mt-4 = t('.tag_statistics')
#loading
.spinner
= t('.loading_tag_statistics')
#no-elements
= t('.empty_tag_statistics')
#tag-grid

View File

@ -1,19 +1,30 @@
h1 = t('.title')
- exercises = Exercise.where(:id => @user.submissions.group(:exercise_id).select(:exercise_id).distinct)
- submissions = Submission.where(user: @user).in_study_group_of(current_user)
- exercises = Exercise.where(id: submissions.joins(:exercise).group(:exercise_id).select(:exercise_id).distinct)
- if !policy(exercises.first).detailed_statistics?
- submissions = submissions.final
- any_viewable_submission = submissions.first
.table-responsive
table.table.table-striped.sortable
thead
tr
- ['.exercise', '.score', '.runs', '.worktime'].each do |title|
th.header = t(title)
tbody
- exercises.each do |exercise|
- if statistics[exercise.id]
- stats = statistics[exercise.id]
tr
td = link_to_if policy(exercise).show?, exercise, controller: "exercises", action: "statistics", external_user_id: @user.id, id: exercise.id
td = stats["maximum_score"] or 0
td = stats["runs"] or 0
td = stats["working_time"] or 0
- if any_viewable_submission && policy(any_viewable_submission).show_study_group?
.table-responsive
table.table.table-striped.sortable
thead
tr
th.header = t('.exercise')
th.header = t('.score')
th.header = t('.runs') if policy(exercises.first).detailed_statistics?
th.header = t('.worktime') if policy(exercises.first).detailed_statistics?
tbody
- exercises.each do |exercise|
// Grab any submission in context of study group (or all if admin). Then check for permission
- any_submission = submissions.where(exercise: exercise).first
- if any_submission && policy(any_submission).show_study_group? && statistics[exercise.id]
- stats = statistics[exercise.id]
tr
td = link_to exercise, controller: "exercises", action: "statistics", external_user_id: @user.id, id: exercise.id
td = stats["maximum_score"] or 0
td = stats["runs"] or 0 if policy(exercises.first).detailed_statistics?
td = stats["working_time"] or 0 if policy(exercises.first).detailed_statistics?
- else
= t('exercises.external_users.statistics.no_data_available')

View File

@ -35,6 +35,8 @@ de:
hide_file_tree: Dateibaum verstecken
instructions: Anweisungen
maximum_score: Erreichbare Punktzahl
submission_deadline: Abgabefrist
late_submission_deadline: Verspätete Abgabefrist
number_of_users: "# Nutzer"
public: Öffentlich
selection: Ausgewählt
@ -216,6 +218,8 @@ de:
models:
exercise:
at_most_one_main_file: dürfen höchstens eine Hauptdatei enthalten
late_submission_deadline_not_alone: darf nicht ohne eine reguläre Abgabefrist verwendet werden
late_submission_deadline_not_before_submission_deadline: darf nicht vor der reguläre Abgabefrist liegen
admin:
dashboard:
show:
@ -347,6 +351,9 @@ de:
unpublish_warning: Mit dieser Aktion wird die Aufgabe deaktiviert. Jeder Student, der versucht sie zu implementieren wird eine Fehlermeldung bekommen, bis die Aufgabe wieder aktiviert wurde.
no_execution_environment_selected: Bitte eine Ausführungsumgebung auswählen, bevor die Aufgabe aktiviert wird.
none: Keine
hints:
submission_deadline: Ein Zeitpunkt in UTC, zu dem die Abgabe geschlossen wird. Einreichungen nach der Abgabefrist werden als verspätet gekennzeichnet.
late_submission_deadline: Eine Gnadenfrist für Abgaben in UTC. Die verlängerte Abgabefrist soll nicht vor der eigentlichen Abgabefrist liegen. Nachdem die Gnadenfrist verstichen ist, werden keine neuen Einreichungen mehr akzeptiert.
implement:
alert:
text: 'Ihr Browser unterstützt nicht alle Funktionalitäten, die %{application_name} benötigt. Bitte nutzen Sie einen modernen Browser, um %{application_name} zu besuchen.'
@ -406,7 +413,7 @@ de:
participants: Bearbeitende Nutzer
users: '%{count} verschiedene Nutzer'
user: Nutzer
score: Punktzahl
score: Maximale Punktzahl
runs: Versuche
worktime: Arbeitszeit
average_worktime: Durchschnittliche Arbeitszeit
@ -426,12 +433,17 @@ de:
no_data_yet: Bisher sind keine Daten verfügbar
external_users:
statistics:
no_data_available: Keine Daten verfügbar.
no_data_available: Keine Daten verfügbar oder fehlende Berechtigungen.
time: Zeit
cause: Grund
score: Punktzahl
tests: Unit Tests
time_difference: 'Arbeitszeit bis hier*'
legend: 'Legende:'
no_deadline: Keine Abgabefrist
before_deadline: Abgabe rechtzeitig
within_grace_period: Abgabe innerhalb der Gnadenfrist
after_late_deadline: Verspätete Abgabe
addendum: '* Differenzen von mehr als %{delta} Minuten werden ignoriert.'
proxy_exercises:
index:
@ -440,7 +452,7 @@ de:
statistics:
title: Statistiken für Externe Benutzer
exercise: Übung
score: Bewertung
score: Maximale Punktzahl
runs: Abgaben
worktime: Arbeitszeit
show:

View File

@ -35,6 +35,8 @@ en:
hide_file_tree: Hide File Tree
instructions: Instructions
maximum_score: Maximum Score
submission_deadline: Submission Deadline
late_submission_deadline: Late Submission Deadline
number_of_users: "# Users"
public: Public
selection: Selected
@ -216,6 +218,8 @@ en:
models:
exercise:
at_most_one_main_file: must include at most one main file
late_submission_deadline_not_alone: must not be used without a regular submission deadline
late_submission_deadline_not_before_submission_deadline: must not be before the submission deadline passed
admin:
dashboard:
show:
@ -347,6 +351,9 @@ en:
unpublish_warning: This will unpublish the exercise. Any student trying to implement it will get an error message, until it is published again.
no_execution_environment_selected: Select an execution environment before publishing the exercise.
none: None
hints:
submission_deadline: A date and time in UTC to close the submission. Any submission obtained after the deadline will be considered late.
late_submission_deadline: A grace period for submissions in UTC. The late submission deadline should not be set or any timestamp before the original submission deadline. After the late submission deadline passed, any new submissions are prevented.
implement:
alert:
text: 'Your browser does not support features required for using %{application_name}. Please access %{application_name} using a modern browser.'
@ -406,7 +413,7 @@ en:
participants: Participating Users
users: '%{count} distinct users'
user: User
score: Score
score: Maximum Score
runs: Runs
worktime: Working Time
average_worktime: Average Working Time
@ -426,12 +433,17 @@ en:
no_data_yet: No data available yet
external_users:
statistics:
no_data_available: No data available.
no_data_available: No data available or insufficient permissions
time: Time
cause: Cause
score: Score
tests: Unit Test Results
time_difference: 'Working Time until here*'
legend: 'Legend:'
no_deadline: No Deadline
before_deadline: On Time
within_grace_period: Within Grace Period
after_late_deadline: Too Late
addendum: "* Deltas longer than %{delta} minutes are ignored."
proxy_exercises:
index:
@ -440,7 +452,7 @@ en:
statistics:
title: External User Statistics
exercise: Exercise
score: Score
score: Maximum Score
runs: Submissions
worktime: Working Time
show:

View File

@ -0,0 +1,6 @@
class AddDeadlineToExercises < ActiveRecord::Migration[5.2]
def change
add_column :exercises, :submission_deadline, :datetime, null: true, default: nil
add_column :exercises, :late_submission_deadline, :datetime, null: true, default: nil
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_03_26_115249) do
ActiveRecord::Schema.define(version: 2020_05_06_093054) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -154,6 +154,8 @@ ActiveRecord::Schema.define(version: 2020_03_26_115249) do
t.integer "expected_difficulty", default: 1
t.uuid "uuid"
t.boolean "unpublished", default: false
t.datetime "submission_deadline"
t.datetime "late_submission_deadline"
t.index ["id"], name: "index_exercises_on_id"
end
@ -423,5 +425,6 @@ ActiveRecord::Schema.define(version: 2020_03_26_115249) do
t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id"
end
add_foreign_key "request_for_comments", "submissions", name: "request_for_comments_submissions_id_fk"
add_foreign_key "submissions", "study_groups"
end

View File

@ -3,7 +3,7 @@ require 'rails_helper'
describe ExternalUserPolicy do
subject { described_class }
[:create?, :destroy?, :edit?, :index?, :new?, :show?, :update?].each do |action|
[:create?, :destroy?, :edit?, :new?, :show?, :update?].each do |action|
permissions(action) do
it 'grants access to admins only' do
expect(subject).to permit(FactoryBot.build(:admin), ExternalUser.new)
@ -13,4 +13,16 @@ describe ExternalUserPolicy do
end
end
end
[:index?].each do |action|
permissions(action) do
it 'grants access to admins and teachers only' do
expect(subject).to permit(FactoryBot.build(:admin), ExternalUser.new)
expect(subject).to permit(FactoryBot.build(:teacher), ExternalUser.new)
[:external_user].each do |factory_name|
expect(subject).not_to permit(FactoryBot.build(factory_name), ExternalUser.new)
end
end
end
end
end