Add submission deadline to exercises and allow teachers to view their submissions
This commit is contained in:
@ -7,6 +7,13 @@
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.chosen-inline {
|
||||
.chosen-container {
|
||||
min-width: unset !important;
|
||||
width: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.code-field {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
@ -80,14 +82,14 @@ class ExercisesController < ApplicationController
|
||||
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)
|
||||
@ -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,34 +234,33 @@ 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
|
||||
@user_id = if current_user.respond_to? :external_id
|
||||
current_user.external_id
|
||||
else
|
||||
@user_id = current_user.id
|
||||
current_user.id
|
||||
end
|
||||
end
|
||||
|
||||
@ -267,21 +268,21 @@ class ExercisesController < ApplicationController
|
||||
lti_parameters = LtiParameter.where(external_users_id: current_user.id,
|
||||
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/)
|
||||
if lti_json.nil?
|
||||
''
|
||||
else
|
||||
if match = lti_json.match(%r{^.*courses/([a-z0-9\-]+)/sections})
|
||||
match.captures.first
|
||||
else
|
||||
""
|
||||
''
|
||||
end
|
||||
else
|
||||
""
|
||||
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))
|
||||
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
|
||||
|
||||
@ -311,8 +313,8 @@ class ExercisesController < ApplicationController
|
||||
|
||||
begin search.save
|
||||
render(json: {success: 'true'})
|
||||
rescue
|
||||
render(json: {success: 'false', error: "could not save search: #{$!}"})
|
||||
rescue StandardError
|
||||
render(json: {success: 'false', error: "could not save search: #{$ERROR_INFO}"})
|
||||
end
|
||||
end
|
||||
|
||||
@ -386,7 +388,7 @@ 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) }
|
||||
end
|
||||
@ -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 }
|
||||
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
|
||||
if delta == nil or delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS then 0 else delta end
|
||||
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
|
||||
@ -461,14 +475,14 @@ class ExercisesController < ApplicationController
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,25 +207,25 @@ class Exercise < ApplicationRecord
|
||||
max_bucket = 100
|
||||
maximum_score = self.maximum_score
|
||||
|
||||
if user.blank?
|
||||
additional_filter = ''
|
||||
additional_filter = if user.blank?
|
||||
''
|
||||
else
|
||||
additional_filter = "AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
|
||||
"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
|
||||
bucket = if maximum_score > 0.0 && tuple['score'] <= maximum_score
|
||||
(tuple['score'] / maximum_score * max_bucket).round
|
||||
else
|
||||
bucket = max_bucket # maximum_score / maximum_score will always be 1
|
||||
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
|
||||
|
||||
@ -229,9 +234,7 @@ class Exercise < ApplicationRecord
|
||||
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,23 +365,22 @@ 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("""
|
||||
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
|
||||
begin
|
||||
Time.parse(self.class.connection.execute(''"
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
@ -428,7 +429,10 @@ class Exercise < ApplicationRecord
|
||||
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
|
||||
"'').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,
|
||||
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
|
||||
|
@ -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
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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')
|
||||
|
@ -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,28 +36,50 @@ 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
|
||||
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
|
||||
|
@ -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?)
|
||||
|
@ -25,6 +25,7 @@ h1 = @exercise
|
||||
p = @exercise.average_working_time
|
||||
|
||||
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label|
|
||||
- if symbol==:internal_users && current_user.admin?
|
||||
strong = label
|
||||
- if symbol==:external_users
|
||||
- working_time_array = []
|
||||
@ -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?
|
||||
|
@ -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,6 +10,7 @@ h1 = @user.displayname
|
||||
|
||||
h4.mt-4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user)) if policy(@user).statistics?
|
||||
|
||||
- if current_user.admin?
|
||||
h4.mt-4 = t('.tag_statistics')
|
||||
#loading
|
||||
.spinner
|
||||
|
@ -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
|
||||
|
||||
- if any_viewable_submission && policy(any_viewable_submission).show_study_group?
|
||||
.table-responsive
|
||||
table.table.table-striped.sortable
|
||||
thead
|
||||
tr
|
||||
- ['.exercise', '.score', '.runs', '.worktime'].each do |title|
|
||||
th.header = t(title)
|
||||
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|
|
||||
- if statistics[exercise.id]
|
||||
// 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_if policy(exercise).show?, exercise, controller: "exercises", action: "statistics", external_user_id: @user.id, id: exercise.id
|
||||
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
|
||||
td = stats["working_time"] 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')
|
@ -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:
|
||||
|
@ -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:
|
||||
|
6
db/migrate/20200506093054_add_deadline_to_exercises.rb
Normal file
6
db/migrate/20200506093054_add_deadline_to_exercises.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user