Add submission deadline to exercises and allow teachers to view their submissions
This commit is contained in:
@ -7,6 +7,13 @@
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chosen-inline {
|
||||||
|
.chosen-container {
|
||||||
|
min-width: unset !important;
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.code-field {
|
.code-field {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,18 @@ tr.highlight {
|
|||||||
border-top: 2px solid rgba(222,0,0,1);
|
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:
|
// StatisticsController:
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ExercisesController < ApplicationController
|
class ExercisesController < ApplicationController
|
||||||
include CommonBehavior
|
include CommonBehavior
|
||||||
include Lti
|
include Lti
|
||||||
@ -5,16 +7,16 @@ class ExercisesController < ApplicationController
|
|||||||
include SubmissionScoring
|
include SubmissionScoring
|
||||||
include TimeHelper
|
include TimeHelper
|
||||||
|
|
||||||
before_action :handle_file_uploads, only: [:create, :update]
|
before_action :handle_file_uploads, only: %i[create update]
|
||||||
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
|
before_action :set_execution_environments, only: %i[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 :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_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]
|
before_action :set_course_token, only: [:implement]
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token, only: [:import_exercise, :import_uuid_check, :export_external_confirm]
|
skip_before_action :verify_authenticity_token, only: %i[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_authorized, only: %i[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_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check export_external_confirm], raise: false
|
||||||
|
|
||||||
def authorize!
|
def authorize!
|
||||||
authorize(@exercise || @exercises)
|
authorize(@exercise || @exercises)
|
||||||
@ -31,13 +33,13 @@ class ExercisesController < ApplicationController
|
|||||||
|
|
||||||
def experimental_courses
|
def experimental_courses
|
||||||
{
|
{
|
||||||
java17: "702cbd2a-c84c-4b37-923a-692d7d1532d0",
|
java17: '702cbd2a-c84c-4b37-923a-692d7d1532d0',
|
||||||
java1: "0ea88ea9-979a-44a3-b0e4-84ba58e5a05e"
|
java1: '0ea88ea9-979a-44a3-b0e4-84ba58e5a05e'
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def experimental_course?(course_token)
|
def experimental_course?(course_token)
|
||||||
experimental_courses.has_value?(course_token)
|
experimental_courses.value?(course_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
def batch_update
|
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 }
|
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 }
|
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.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||||
et.exercise = @exercise
|
et.exercise = @exercise
|
||||||
end
|
end
|
||||||
|
|
||||||
myparam[:exercise_tags] = checked_exercise_tags
|
myparam[:exercise_tags] = checked_exercise_tags
|
||||||
myparam.delete :tag_ids
|
myparam.delete :tag_ids
|
||||||
removed_exercise_tags.map {|et| et.destroy}
|
removed_exercise_tags.map(&:destroy)
|
||||||
|
|
||||||
authorize!
|
authorize!
|
||||||
create_and_respond(object: @exercise)
|
create_and_respond(object: @exercise)
|
||||||
@ -210,7 +212,7 @@ class ExercisesController < ApplicationController
|
|||||||
private :user_by_codeharbor_token
|
private :user_by_codeharbor_token
|
||||||
|
|
||||||
def exercise_params
|
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
|
end
|
||||||
private :exercise_params
|
private :exercise_params
|
||||||
|
|
||||||
@ -232,34 +234,33 @@ class ExercisesController < ApplicationController
|
|||||||
private :handle_file_uploads
|
private :handle_file_uploads
|
||||||
|
|
||||||
def implement
|
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?
|
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||||
user_solved_exercise = @exercise.has_user_solved(current_user)
|
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_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]
|
if @embed_options[:disable_interventions]
|
||||||
@show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s
|
@show_rfc_interventions = false
|
||||||
@show_break_interventions = false
|
@show_break_interventions = false
|
||||||
else
|
else
|
||||||
@show_rfc_interventions = false
|
@show_rfc_interventions = (!user_solved_exercise && !user_got_enough_interventions).to_s
|
||||||
@show_break_interventions = false
|
@show_break_interventions = false
|
||||||
end
|
end
|
||||||
|
|
||||||
@hide_rfc_button = @embed_options[:disable_rfc]
|
@hide_rfc_button = @embed_options[:disable_rfc]
|
||||||
|
|
||||||
|
|
||||||
@search = Search.new
|
@search = Search.new
|
||||||
@search.exercise = @exercise
|
@search.exercise = @exercise
|
||||||
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
@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)
|
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
|
||||||
@paths = collect_paths(@files)
|
@paths = collect_paths(@files)
|
||||||
|
|
||||||
if current_user.respond_to? :external_id
|
@user_id = if current_user.respond_to? :external_id
|
||||||
@user_id = current_user.external_id
|
current_user.external_id
|
||||||
else
|
else
|
||||||
@user_id = current_user.id
|
current_user.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -267,21 +268,21 @@ class ExercisesController < ApplicationController
|
|||||||
lti_parameters = LtiParameter.where(external_users_id: current_user.id,
|
lti_parameters = LtiParameter.where(external_users_id: current_user.id,
|
||||||
exercises_id: @exercise.id).last
|
exercises_id: @exercise.id).last
|
||||||
if lti_parameters
|
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 =
|
@course_token =
|
||||||
unless lti_json.nil?
|
if lti_json.nil?
|
||||||
if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/)
|
''
|
||||||
|
else
|
||||||
|
if match = lti_json.match(%r{^.*courses/([a-z0-9\-]+)/sections})
|
||||||
match.captures.first
|
match.captures.first
|
||||||
else
|
else
|
||||||
""
|
''
|
||||||
end
|
end
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# no consumer, therefore implementation with internal user
|
# no consumer, therefore implementation with internal user
|
||||||
@course_token = "702cbd2a-c84c-4b37-923a-692d7d1532d0"
|
@course_token = '702cbd2a-c84c-4b37-923a-692d7d1532d0'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
private :set_course_token
|
private :set_course_token
|
||||||
@ -294,14 +295,15 @@ class ExercisesController < ApplicationController
|
|||||||
|
|
||||||
def intervention
|
def intervention
|
||||||
intervention = Intervention.find_by_name(params[:intervention_type])
|
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(
|
uei = UserExerciseIntervention.new(
|
||||||
user: current_user, exercise: @exercise, intervention: intervention,
|
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
|
uei.save
|
||||||
render(json: {success: 'true'})
|
render(json: {success: 'true'})
|
||||||
else
|
|
||||||
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -311,8 +313,8 @@ class ExercisesController < ApplicationController
|
|||||||
|
|
||||||
begin search.save
|
begin search.save
|
||||||
render(json: {success: 'true'})
|
render(json: {success: 'true'})
|
||||||
rescue
|
rescue StandardError
|
||||||
render(json: {success: 'false', error: "could not save search: #{$!}"})
|
render(json: {success: 'false', error: "could not save search: #{$ERROR_INFO}"})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -386,7 +388,7 @@ class ExercisesController < ApplicationController
|
|||||||
@search = policy_scope(Tag).ransack(params[:q])
|
@search = policy_scope(Tag).ransack(params[:q])
|
||||||
@tags = @search.result.order(:name)
|
@tags = @search.result.order(:name)
|
||||||
checked_exercise_tags = @exercise.exercise_tags
|
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
|
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
|
end
|
||||||
@ -401,27 +403,39 @@ class ExercisesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def statistics
|
def statistics
|
||||||
if(@external_user)
|
if @external_user
|
||||||
authorize(@external_user, :statistics?)
|
authorize(@external_user, :statistics?)
|
||||||
@submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
|
if policy(@exercise).detailed_statistics?
|
||||||
interventions = UserExerciseIntervention.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)
|
@submissions = Submission.where(user: @external_user, exercise_id: @exercise.id).in_study_group_of(current_user).order('created_at')
|
||||||
@all_events = (@submissions + interventions).sort_by { |a| a.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|
|
@deltas = @all_events.map.with_index do |item, index|
|
||||||
delta = item.created_at - @all_events[index - 1].created_at if index > 0
|
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
|
end
|
||||||
@working_times_until = []
|
@working_times_until = []
|
||||||
@all_events.each_with_index do |_, index|
|
@all_events.each_with_index do |_, index|
|
||||||
@working_times_until.push((format_time_difference(@deltas[0..index].inject(:+)) if index > 0))
|
@working_times_until.push((format_time_difference(@deltas[0..index].inject(:+)) if index > 0))
|
||||||
end
|
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'
|
render 'exercises/external_users/statistics'
|
||||||
else
|
else
|
||||||
user_statistics = {}
|
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
|
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;"
|
user_id;"
|
||||||
ApplicationRecord.connection.execute(query).each do |tuple|
|
ApplicationRecord.connection.execute(query).each do |tuple|
|
||||||
user_statistics[tuple["user_id"].to_i] = tuple
|
user_statistics[tuple['user_id'].to_i] = tuple
|
||||||
end
|
end
|
||||||
render locals: {
|
render locals: {
|
||||||
user_statistics: user_statistics
|
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 }
|
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 }
|
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.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||||
et.exercise = @exercise
|
et.exercise = @exercise
|
||||||
end
|
end
|
||||||
|
|
||||||
myparam[:exercise_tags] = checked_exercise_tags
|
myparam[:exercise_tags] = checked_exercise_tags
|
||||||
myparam.delete :tag_ids
|
myparam.delete :tag_ids
|
||||||
removed_exercise_tags.map {|et| et.destroy}
|
removed_exercise_tags.map(&:destroy)
|
||||||
update_and_respond(object: @exercise, params: myparam)
|
update_and_respond(object: @exercise, params: myparam)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -547,12 +561,11 @@ class ExercisesController < ApplicationController
|
|||||||
def study_group_dashboard
|
def study_group_dashboard
|
||||||
authorize!
|
authorize!
|
||||||
@study_group_id = params[:study_group_id]
|
@study_group_id = params[:study_group_id]
|
||||||
@request_for_comments = RequestForComment.
|
@request_for_comments = RequestForComment
|
||||||
where(exercise: @exercise).includes(:submission).
|
.where(exercise: @exercise).includes(:submission)
|
||||||
where(submissions: {study_group_id: @study_group_id}).
|
.where(submissions: {study_group_id: @study_group_id})
|
||||||
order(created_at: :desc)
|
.order(created_at: :desc)
|
||||||
|
|
||||||
@graph_data = @exercise.get_working_times_for_study_group(@study_group_id)
|
@graph_data = @exercise.get_working_times_for_study_group(@study_group_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -6,7 +6,7 @@ class ExternalUsersController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@search = ExternalUser.ransack(params[:q])
|
@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!
|
authorize!
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ class ExternalUsersController < ApplicationController
|
|||||||
FROM submissions
|
FROM submissions
|
||||||
WHERE user_id = #{@user.id}
|
WHERE user_id = #{@user.id}
|
||||||
AND user_type = 'ExternalUser'
|
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,
|
GROUP BY exercise_id,
|
||||||
user_id,
|
user_id,
|
||||||
id
|
id
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'nokogiri'
|
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
|
class Exercise < ApplicationRecord
|
||||||
include Context
|
include Context
|
||||||
@ -25,11 +27,12 @@ class Exercise < ApplicationRecord
|
|||||||
|
|
||||||
has_many :external_users, source: :user, source_type: 'ExternalUser', through: :submissions
|
has_many :external_users, source: :user, source_type: 'ExternalUser', through: :submissions
|
||||||
has_many :internal_users, source: :user, source_type: 'InternalUser', 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)') }
|
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
|
||||||
|
|
||||||
validate :valid_main_file?
|
validate :valid_main_file?
|
||||||
|
validate :valid_submission_deadlines?
|
||||||
validates :description, presence: true
|
validates :description, presence: true
|
||||||
validates :execution_environment, presence: true, if: -> { !unpublished? }
|
validates :execution_environment, presence: true, if: -> { !unpublished? }
|
||||||
validates :public, boolean_presence: true
|
validates :public, boolean_presence: true
|
||||||
@ -44,7 +47,7 @@ class Exercise < ApplicationRecord
|
|||||||
MAX_EXERCISE_FEEDBACKS = 20
|
MAX_EXERCISE_FEEDBACKS = 20
|
||||||
|
|
||||||
def average_percentage
|
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)
|
(average_score / maximum_score * 100).round(2)
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
@ -68,11 +71,13 @@ class Exercise < ApplicationRecord
|
|||||||
|
|
||||||
def average_number_of_submissions
|
def average_number_of_submissions
|
||||||
user_count = internal_users.distinct.count + external_users.distinct.count
|
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
|
end
|
||||||
|
|
||||||
def time_maximum_score(user)
|
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
|
end
|
||||||
|
|
||||||
def user_working_time_query
|
def user_working_time_query
|
||||||
@ -100,7 +105,7 @@ class Exercise < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def study_group_working_time_query(exercise_id, study_group_id, additional_filter)
|
def study_group_working_time_query(exercise_id, study_group_id, additional_filter)
|
||||||
"""
|
''"
|
||||||
WITH working_time_between_submissions AS (
|
WITH working_time_between_submissions AS (
|
||||||
SELECT submissions.user_id,
|
SELECT submissions.user_id,
|
||||||
submissions.user_type,
|
submissions.user_type,
|
||||||
@ -193,7 +198,7 @@ class Exercise < ApplicationRecord
|
|||||||
FROM working_times_with_index
|
FROM working_times_with_index
|
||||||
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
|
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
|
||||||
ORDER BY index, score ASC;
|
ORDER BY index, score ASC;
|
||||||
"""
|
"''
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_working_times_for_study_group(study_group_id, user = nil)
|
def get_working_times_for_study_group(study_group_id, user = nil)
|
||||||
@ -202,25 +207,25 @@ class Exercise < ApplicationRecord
|
|||||||
max_bucket = 100
|
max_bucket = 100
|
||||||
maximum_score = self.maximum_score
|
maximum_score = self.maximum_score
|
||||||
|
|
||||||
if user.blank?
|
additional_filter = if user.blank?
|
||||||
additional_filter = ''
|
''
|
||||||
else
|
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
|
end
|
||||||
|
|
||||||
results = self.class.connection.execute(study_group_working_time_query(id, study_group_id, additional_filter)).each do |tuple|
|
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 = if maximum_score > 0.0 && tuple['score'] <= maximum_score
|
||||||
bucket = (tuple['score'] / maximum_score * max_bucket).round
|
(tuple['score'] / maximum_score * max_bucket).round
|
||||||
else
|
else
|
||||||
bucket = max_bucket # maximum_score / maximum_score will always be 1
|
max_bucket # maximum_score / maximum_score will always be 1
|
||||||
end
|
end
|
||||||
|
|
||||||
user_progress[bucket] ||= []
|
user_progress[bucket] ||= []
|
||||||
additional_user_data[bucket] ||= []
|
additional_user_data[bucket] ||= []
|
||||||
additional_user_data[max_bucket + 1] ||= []
|
additional_user_data[max_bucket + 1] ||= []
|
||||||
|
|
||||||
user_progress[bucket][tuple['index']] = tuple["working_time_per_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[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']}
|
additional_user_data[max_bucket + 1][tuple['index']] = {id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -229,9 +234,7 @@ class Exercise < ApplicationRecord
|
|||||||
last_index = results[results.ntuples - 1]['index']
|
last_index = results[results.ntuples - 1]['index']
|
||||||
buckets = last_index - first_index
|
buckets = last_index - first_index
|
||||||
user_progress.each do |timings_array|
|
user_progress.each do |timings_array|
|
||||||
if timings_array.present? && timings_array.length != buckets + 1
|
timings_array[buckets] = nil if timings_array.present? && timings_array.length != buckets + 1
|
||||||
timings_array[buckets] = nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -239,8 +242,8 @@ class Exercise < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_quantiles(quantiles)
|
def get_quantiles(quantiles)
|
||||||
quantiles_str = "[" + quantiles.join(",") + "]"
|
quantiles_str = '[' + quantiles.join(',') + ']'
|
||||||
result = self.class.connection.execute("""
|
result = self.class.connection.execute(''"
|
||||||
WITH working_time AS
|
WITH working_time AS
|
||||||
(
|
(
|
||||||
SELECT user_id,
|
SELECT user_id,
|
||||||
@ -346,13 +349,12 @@ class Exercise < ApplicationRecord
|
|||||||
exercise_id )
|
exercise_id )
|
||||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||||
FROM result
|
FROM result
|
||||||
""")
|
"'')
|
||||||
if result.count > 0
|
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
|
else
|
||||||
quantiles.map{|q| 0}
|
quantiles.map { |_q| 0 }
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def retrieve_working_time_statistics
|
def retrieve_working_time_statistics
|
||||||
@ -363,23 +365,22 @@ class Exercise < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def average_working_time
|
def average_working_time
|
||||||
self.class.connection.execute("""
|
self.class.connection.execute(''"
|
||||||
SELECT avg(working_time) as average_time
|
SELECT avg(working_time) as average_time
|
||||||
FROM
|
FROM
|
||||||
(#{user_working_time_query}) AS baz;
|
(#{user_working_time_query}) AS baz;
|
||||||
""").first['average_time']
|
"'').first['average_time']
|
||||||
end
|
end
|
||||||
|
|
||||||
def average_working_time_for(user_id)
|
def average_working_time_for(user_id)
|
||||||
if @working_time_statistics == nil
|
retrieve_working_time_statistics if @working_time_statistics.nil?
|
||||||
retrieve_working_time_statistics()
|
@working_time_statistics[user_id]['working_time']
|
||||||
end
|
|
||||||
@working_time_statistics[user_id]["working_time"]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def accumulated_working_time_for_only(user)
|
def accumulated_working_time_for_only(user)
|
||||||
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
|
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
|
||||||
Time.parse(self.class.connection.execute("""
|
begin
|
||||||
|
Time.parse(self.class.connection.execute(''"
|
||||||
WITH WORKING_TIME AS
|
WITH WORKING_TIME AS
|
||||||
(SELECT user_id,
|
(SELECT user_id,
|
||||||
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
|
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
|
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
|
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
|
end
|
||||||
|
|
||||||
def duplicate(attributes = {})
|
def duplicate(attributes = {})
|
||||||
@ -453,7 +457,8 @@ class Exercise < ApplicationRecord
|
|||||||
return 'main_file'
|
return 'main_file'
|
||||||
elsif (file_class == 'internal') && (comment == 'main')
|
elsif (file_class == 'internal') && (comment == 'main')
|
||||||
end
|
end
|
||||||
return 'regular_file'
|
|
||||||
|
'regular_file'
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_proforma_xml(xml_string)
|
def from_proforma_xml(xml_string)
|
||||||
@ -467,23 +472,23 @@ class Exercise < ApplicationRecord
|
|||||||
description: description,
|
description: description,
|
||||||
instructions: 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_name_split = file.xpath('@filename').first.value.split('.')
|
||||||
file_class = file.xpath('@class').first.value
|
file_class = file.xpath('@class').first.value
|
||||||
role = determine_file_role_from_proforma_file(task_node, file)
|
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({
|
files.build({
|
||||||
name: file_name_split.first,
|
name: file_name_split.first,
|
||||||
content: file.xpath('text()').first.content,
|
content: file.xpath('text()').first.content,
|
||||||
read_only: false,
|
read_only: false,
|
||||||
hidden: file_class == 'internal',
|
hidden: file_class == 'internal',
|
||||||
role: role,
|
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_type: FileType.where(
|
||||||
file_extension: ".#{file_name_split.second}"
|
file_extension: ".#{file_name_split.second}"
|
||||||
).take
|
).take
|
||||||
})
|
})
|
||||||
}
|
end
|
||||||
self.execution_environment_id = 1
|
self.execution_environment_id = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -495,7 +500,11 @@ class Exercise < ApplicationRecord
|
|||||||
def maximum_score(user = nil)
|
def maximum_score(user = nil)
|
||||||
if user
|
if user
|
||||||
# FIXME: where(user: user) will not work here!
|
# 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
|
else
|
||||||
files.teacher_defined_tests.sum(:weight)
|
files.teacher_defined_tests.sum(:weight)
|
||||||
end
|
end
|
||||||
@ -510,7 +519,7 @@ class Exercise < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def finishers
|
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
|
end
|
||||||
|
|
||||||
def set_default_values
|
def set_default_values
|
||||||
@ -523,12 +532,22 @@ class Exercise < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def valid_main_file?
|
def valid_main_file?
|
||||||
if files.main_files.count > 1
|
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file')) if files.main_files.count > 1
|
||||||
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
private :valid_main_file?
|
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?
|
def needs_more_feedback?
|
||||||
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
|
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
|
||||||
end
|
end
|
||||||
@ -543,5 +562,4 @@ class Exercise < ApplicationRecord
|
|||||||
WHERE exercise_id = #{id}
|
WHERE exercise_id = #{id}
|
||||||
) AS t ON t.fv = submissions.id").distinct
|
) AS t ON t.fv = submissions.id").distinct
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -15,11 +15,22 @@ class Submission < ApplicationRecord
|
|||||||
has_many :structured_errors
|
has_many :structured_errors
|
||||||
has_many :comments, through: :files
|
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
|
delegate :execution_environment, to: :exercise
|
||||||
|
|
||||||
scope :final, -> { where(cause: 'submit') }
|
scope :final, -> { where(cause: 'submit') }
|
||||||
scope :intermediate, -> { where.not(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 :cause, inclusion: {in: CAUSES}
|
||||||
validates :exercise_id, presence: true
|
validates :exercise_id, presence: true
|
||||||
|
|
||||||
@ -29,6 +40,7 @@ class Submission < ApplicationRecord
|
|||||||
def build_files_hash(files, attribute)
|
def build_files_hash(files, attribute)
|
||||||
files.map(&attribute.to_proc).zip(files).to_h
|
files.map(&attribute.to_proc).zip(files).to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
private :build_files_hash
|
private :build_files_hash
|
||||||
|
|
||||||
def collect_files
|
def collect_files
|
||||||
@ -62,6 +74,32 @@ class Submission < ApplicationRecord
|
|||||||
Submission.model_name.human
|
Submission.model_name.human
|
||||||
end
|
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?
|
def redirect_to_feedback?
|
||||||
((user_id + exercise.created_at.to_i) % 10 == 1) && exercise.needs_more_feedback?
|
((user_id + exercise.created_at.to_i) % 10 == 1) && exercise.needs_more_feedback?
|
||||||
end
|
end
|
||||||
|
@ -19,6 +19,8 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
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|
|
ROLES.each do |role|
|
||||||
define_method("#{role}?") { try(:role) == role }
|
define_method("#{role}?") { try(:role) == role }
|
||||||
end
|
end
|
||||||
|
@ -33,9 +33,14 @@ class ApplicationPolicy
|
|||||||
users_in_same_study_group = study_group.users
|
users_in_same_study_group = study_group.users
|
||||||
elsif @record.respond_to? :users # e.g. study_group
|
elsif @record.respond_to? :users # e.g. study_group
|
||||||
users_in_same_study_group = @record.users
|
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
|
study_groups = @record.user.study_groups
|
||||||
users_in_same_study_group = study_groups.collect(&:users).flatten
|
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
|
end
|
||||||
|
|
||||||
users_in_same_study_group.include? @user
|
users_in_same_study_group.include? @user
|
||||||
|
@ -11,6 +11,14 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
|||||||
admin? || teacher_in_study_group?
|
admin? || teacher_in_study_group?
|
||||||
end
|
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|
|
[:clone?, :destroy?, :edit?, :update?, :export_external_check?, :export_external_confirm?].each do |action|
|
||||||
define_method(action) { admin? || teacher_in_study_group? || author? }
|
define_method(action) { admin? || teacher_in_study_group? || author? }
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
class ExternalUserPolicy < AdminOnlyPolicy
|
class ExternalUserPolicy < AdminOnlyPolicy
|
||||||
|
def index?
|
||||||
|
admin? || teacher?
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
admin? || teacher_in_study_group?
|
||||||
|
end
|
||||||
|
|
||||||
def statistics?
|
def statistics?
|
||||||
admin?
|
admin? || teacher_in_study_group?
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_statistics?
|
def tag_statistics?
|
||||||
|
@ -16,6 +16,16 @@
|
|||||||
= f.label(:instructions)
|
= f.label(:instructions)
|
||||||
= f.hidden_field(:instructions)
|
= f.hidden_field(:instructions)
|
||||||
.form-control.markdown
|
.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
|
.form-check
|
||||||
label.form-check-label
|
label.form-check-label
|
||||||
= f.check_box(:public, class: 'form-check-input')
|
= 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
|
- current_submission = @submissions.first
|
||||||
- if current_submission
|
- if current_submission
|
||||||
- initial_files = current_submission.files.to_a
|
- 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
|
table.table
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
- ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
|
th.header = t('.time')
|
||||||
th.header = t(title)
|
th.header = t('.cause')
|
||||||
|
th.header = t('.score')
|
||||||
|
th.header = t('.tests')
|
||||||
|
th.header = t('.time_difference') if policy(@exercise).detailed_statistics?
|
||||||
tbody
|
tbody
|
||||||
- @all_events.each_with_index do |this, index|
|
- @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)
|
- 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")
|
td.clickable = this.created_at.strftime("%F %T")
|
||||||
- if this.is_a?(Submission)
|
- if this.is_a?(Submission)
|
||||||
td = this.cause
|
td = this.cause
|
||||||
td = this.score
|
td = this.score
|
||||||
td
|
td.align-middle
|
||||||
-this.testruns.each do |run|
|
-this.testruns.each do |run|
|
||||||
- if run.passed
|
- if run.passed
|
||||||
.unit-test-result.positive-result title=run.output
|
.unit-test-result.positive-result title=run.output
|
||||||
- else
|
- else
|
||||||
.unit-test-result.unknown-result title=run.output
|
.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
|
- elsif this.is_a? UserExerciseIntervention
|
||||||
td = this.intervention.name
|
td = this.intervention.name
|
||||||
td =
|
td =
|
||||||
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)
|
p = t('.addendum', delta: StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS / 60)
|
||||||
.d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
|
.d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
|
||||||
div#progress_chart.col-lg-12
|
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.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.instructions', value: render_markdown(@exercise.instructions))
|
||||||
= row(label: 'exercise.maximum_score', value: @exercise.maximum_score)
|
= 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.public', value: @exercise.public?)
|
||||||
= row(label: 'exercise.unpublished', value: @exercise.unpublished?)
|
= row(label: 'exercise.unpublished', value: @exercise.unpublished?)
|
||||||
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)
|
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)
|
||||||
|
@ -25,6 +25,7 @@ h1 = @exercise
|
|||||||
p = @exercise.average_working_time
|
p = @exercise.average_working_time
|
||||||
|
|
||||||
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label|
|
- 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
|
strong = label
|
||||||
- if symbol==:external_users
|
- if symbol==:external_users
|
||||||
- working_time_array = []
|
- working_time_array = []
|
||||||
@ -38,19 +39,26 @@ h1 = @exercise
|
|||||||
hr
|
hr
|
||||||
div#chart_2
|
div#chart_2
|
||||||
hr
|
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-responsive
|
||||||
table.table.table-striped.sortable
|
table.table.table-striped.sortable
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
- ['.user', '.score', '.runs', '.worktime'].each do |title|
|
th.header = t('.user')
|
||||||
th.header = t(title)
|
th.header = t('.score')
|
||||||
|
th.header = t('.runs') if policy(@exercise).detailed_statistics?
|
||||||
|
th.header = t('.worktime') if policy(@exercise).detailed_statistics?
|
||||||
tbody
|
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}
|
- if user_statistics[user.id] then us = user_statistics[user.id] else us = {"maximum_score" => nil, "runs" => nil}
|
||||||
- label = "#{user.displayname}"
|
- label = "#{user.displayname}"
|
||||||
tr
|
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 = 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['maximum_score'] or 0
|
||||||
td = us['runs']
|
td = us['runs'] if policy(@exercise).detailed_statistics?
|
||||||
td = @exercise.average_working_time_for(user.id) or 0
|
td = @exercise.average_working_time_for(user.id) or 0 if policy(@exercise).detailed_statistics?
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
h1 = @user.displayname
|
h1 = @user.displayname
|
||||||
|
|
||||||
= row(label: 'external_user.name', value: @user.name)
|
= 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
|
= row(label: 'external_user.external_id') do
|
||||||
code
|
code
|
||||||
= @user.external_id
|
= @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?
|
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')
|
h4.mt-4 = t('.tag_statistics')
|
||||||
#loading
|
#loading
|
||||||
.spinner
|
.spinner
|
||||||
|
@ -1,19 +1,30 @@
|
|||||||
h1 = t('.title')
|
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-responsive
|
||||||
table.table.table-striped.sortable
|
table.table.table-striped.sortable
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
- ['.exercise', '.score', '.runs', '.worktime'].each do |title|
|
th.header = t('.exercise')
|
||||||
th.header = t(title)
|
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
|
tbody
|
||||||
- exercises.each do |exercise|
|
- 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]
|
- stats = statistics[exercise.id]
|
||||||
tr
|
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["maximum_score"] or 0
|
||||||
td = stats["runs"] or 0
|
td = stats["runs"] or 0 if policy(exercises.first).detailed_statistics?
|
||||||
td = stats["working_time"] or 0
|
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
|
hide_file_tree: Dateibaum verstecken
|
||||||
instructions: Anweisungen
|
instructions: Anweisungen
|
||||||
maximum_score: Erreichbare Punktzahl
|
maximum_score: Erreichbare Punktzahl
|
||||||
|
submission_deadline: Abgabefrist
|
||||||
|
late_submission_deadline: Verspätete Abgabefrist
|
||||||
number_of_users: "# Nutzer"
|
number_of_users: "# Nutzer"
|
||||||
public: Öffentlich
|
public: Öffentlich
|
||||||
selection: Ausgewählt
|
selection: Ausgewählt
|
||||||
@ -216,6 +218,8 @@ de:
|
|||||||
models:
|
models:
|
||||||
exercise:
|
exercise:
|
||||||
at_most_one_main_file: dürfen höchstens eine Hauptdatei enthalten
|
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:
|
admin:
|
||||||
dashboard:
|
dashboard:
|
||||||
show:
|
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.
|
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.
|
no_execution_environment_selected: Bitte eine Ausführungsumgebung auswählen, bevor die Aufgabe aktiviert wird.
|
||||||
none: Keine
|
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:
|
implement:
|
||||||
alert:
|
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.'
|
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
|
participants: Bearbeitende Nutzer
|
||||||
users: '%{count} verschiedene Nutzer'
|
users: '%{count} verschiedene Nutzer'
|
||||||
user: Nutzer
|
user: Nutzer
|
||||||
score: Punktzahl
|
score: Maximale Punktzahl
|
||||||
runs: Versuche
|
runs: Versuche
|
||||||
worktime: Arbeitszeit
|
worktime: Arbeitszeit
|
||||||
average_worktime: Durchschnittliche Arbeitszeit
|
average_worktime: Durchschnittliche Arbeitszeit
|
||||||
@ -426,12 +433,17 @@ de:
|
|||||||
no_data_yet: Bisher sind keine Daten verfügbar
|
no_data_yet: Bisher sind keine Daten verfügbar
|
||||||
external_users:
|
external_users:
|
||||||
statistics:
|
statistics:
|
||||||
no_data_available: Keine Daten verfügbar.
|
no_data_available: Keine Daten verfügbar oder fehlende Berechtigungen.
|
||||||
time: Zeit
|
time: Zeit
|
||||||
cause: Grund
|
cause: Grund
|
||||||
score: Punktzahl
|
score: Punktzahl
|
||||||
tests: Unit Tests
|
tests: Unit Tests
|
||||||
time_difference: 'Arbeitszeit bis hier*'
|
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.'
|
addendum: '* Differenzen von mehr als %{delta} Minuten werden ignoriert.'
|
||||||
proxy_exercises:
|
proxy_exercises:
|
||||||
index:
|
index:
|
||||||
@ -440,7 +452,7 @@ de:
|
|||||||
statistics:
|
statistics:
|
||||||
title: Statistiken für Externe Benutzer
|
title: Statistiken für Externe Benutzer
|
||||||
exercise: Übung
|
exercise: Übung
|
||||||
score: Bewertung
|
score: Maximale Punktzahl
|
||||||
runs: Abgaben
|
runs: Abgaben
|
||||||
worktime: Arbeitszeit
|
worktime: Arbeitszeit
|
||||||
show:
|
show:
|
||||||
|
@ -35,6 +35,8 @@ en:
|
|||||||
hide_file_tree: Hide File Tree
|
hide_file_tree: Hide File Tree
|
||||||
instructions: Instructions
|
instructions: Instructions
|
||||||
maximum_score: Maximum Score
|
maximum_score: Maximum Score
|
||||||
|
submission_deadline: Submission Deadline
|
||||||
|
late_submission_deadline: Late Submission Deadline
|
||||||
number_of_users: "# Users"
|
number_of_users: "# Users"
|
||||||
public: Public
|
public: Public
|
||||||
selection: Selected
|
selection: Selected
|
||||||
@ -216,6 +218,8 @@ en:
|
|||||||
models:
|
models:
|
||||||
exercise:
|
exercise:
|
||||||
at_most_one_main_file: must include at most one main file
|
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:
|
admin:
|
||||||
dashboard:
|
dashboard:
|
||||||
show:
|
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.
|
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.
|
no_execution_environment_selected: Select an execution environment before publishing the exercise.
|
||||||
none: None
|
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:
|
implement:
|
||||||
alert:
|
alert:
|
||||||
text: 'Your browser does not support features required for using %{application_name}. Please access %{application_name} using a modern browser.'
|
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
|
participants: Participating Users
|
||||||
users: '%{count} distinct users'
|
users: '%{count} distinct users'
|
||||||
user: User
|
user: User
|
||||||
score: Score
|
score: Maximum Score
|
||||||
runs: Runs
|
runs: Runs
|
||||||
worktime: Working Time
|
worktime: Working Time
|
||||||
average_worktime: Average Working Time
|
average_worktime: Average Working Time
|
||||||
@ -426,12 +433,17 @@ en:
|
|||||||
no_data_yet: No data available yet
|
no_data_yet: No data available yet
|
||||||
external_users:
|
external_users:
|
||||||
statistics:
|
statistics:
|
||||||
no_data_available: No data available.
|
no_data_available: No data available or insufficient permissions
|
||||||
time: Time
|
time: Time
|
||||||
cause: Cause
|
cause: Cause
|
||||||
score: Score
|
score: Score
|
||||||
tests: Unit Test Results
|
tests: Unit Test Results
|
||||||
time_difference: 'Working Time until here*'
|
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."
|
addendum: "* Deltas longer than %{delta} minutes are ignored."
|
||||||
proxy_exercises:
|
proxy_exercises:
|
||||||
index:
|
index:
|
||||||
@ -440,7 +452,7 @@ en:
|
|||||||
statistics:
|
statistics:
|
||||||
title: External User Statistics
|
title: External User Statistics
|
||||||
exercise: Exercise
|
exercise: Exercise
|
||||||
score: Score
|
score: Maximum Score
|
||||||
runs: Submissions
|
runs: Submissions
|
||||||
worktime: Working Time
|
worktime: Working Time
|
||||||
show:
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -154,6 +154,8 @@ ActiveRecord::Schema.define(version: 2020_03_26_115249) do
|
|||||||
t.integer "expected_difficulty", default: 1
|
t.integer "expected_difficulty", default: 1
|
||||||
t.uuid "uuid"
|
t.uuid "uuid"
|
||||||
t.boolean "unpublished", default: false
|
t.boolean "unpublished", default: false
|
||||||
|
t.datetime "submission_deadline"
|
||||||
|
t.datetime "late_submission_deadline"
|
||||||
t.index ["id"], name: "index_exercises_on_id"
|
t.index ["id"], name: "index_exercises_on_id"
|
||||||
end
|
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"
|
t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_foreign_key "request_for_comments", "submissions", name: "request_for_comments_submissions_id_fk"
|
||||||
add_foreign_key "submissions", "study_groups"
|
add_foreign_key "submissions", "study_groups"
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,7 @@ require 'rails_helper'
|
|||||||
describe ExternalUserPolicy do
|
describe ExternalUserPolicy do
|
||||||
subject { described_class }
|
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
|
permissions(action) do
|
||||||
it 'grants access to admins only' do
|
it 'grants access to admins only' do
|
||||||
expect(subject).to permit(FactoryBot.build(:admin), ExternalUser.new)
|
expect(subject).to permit(FactoryBot.build(:admin), ExternalUser.new)
|
||||||
@ -13,4 +13,16 @@ describe ExternalUserPolicy do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
Reference in New Issue
Block a user