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
|
||||
@@ -76,18 +78,18 @@ class ExercisesController < ApplicationController
|
||||
def create
|
||||
@exercise = Exercise.new(exercise_params)
|
||||
collect_set_and_unset_exercise_tags
|
||||
myparam = exercise_params.present? ? exercise_params : { }
|
||||
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
myparam = exercise_params.present? ? exercise_params : {}
|
||||
checked_exercise_tags = @exercise_tags.select { |et| myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { |et| myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
|
||||
for et in checked_exercise_tags
|
||||
checked_exercise_tags.each do |et|
|
||||
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||
et.exercise = @exercise
|
||||
end
|
||||
|
||||
myparam[:exercise_tags] = checked_exercise_tags
|
||||
myparam.delete :tag_ids
|
||||
removed_exercise_tags.map {|et| et.destroy}
|
||||
removed_exercise_tags.map(&:destroy)
|
||||
|
||||
authorize!
|
||||
create_and_respond(object: @exercise)
|
||||
@@ -112,12 +114,12 @@ class ExercisesController < ApplicationController
|
||||
def requests_for_comments
|
||||
authorize!
|
||||
@search = RequestForComment
|
||||
.with_last_activity
|
||||
.where(exercise: @exercise)
|
||||
.ransack(params[:q])
|
||||
.with_last_activity
|
||||
.where(exercise: @exercise)
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('last_comment DESC')
|
||||
.paginate(page: params[:page])
|
||||
.order('last_comment DESC')
|
||||
.paginate(page: params[:page])
|
||||
render 'request_for_comments/index'
|
||||
end
|
||||
|
||||
@@ -210,7 +212,7 @@ class ExercisesController < ApplicationController
|
||||
private :user_by_codeharbor_token
|
||||
|
||||
def exercise_params
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :unpublished, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present?
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :submission_deadline, :late_submission_deadline, :public, :unpublished, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, tag_ids: []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present?
|
||||
end
|
||||
private :exercise_params
|
||||
|
||||
@@ -232,56 +234,55 @@ class ExercisesController < ApplicationController
|
||||
private :handle_file_uploads
|
||||
|
||||
def implement
|
||||
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO TESTESTEST
|
||||
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO: TESTESTEST
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
user_solved_exercise = @exercise.has_user_solved(current_user)
|
||||
count_interventions_today = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count
|
||||
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?', Time.zone.now.beginning_of_day).count
|
||||
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise
|
||||
user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day or user_got_intervention_in_exercise
|
||||
(user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day) || user_got_intervention_in_exercise
|
||||
|
||||
unless @embed_options[:disable_interventions]
|
||||
@show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s
|
||||
if @embed_options[:disable_interventions]
|
||||
@show_rfc_interventions = false
|
||||
@show_break_interventions = false
|
||||
else
|
||||
@show_rfc_interventions = false
|
||||
@show_rfc_interventions = (!user_solved_exercise && !user_got_enough_interventions).to_s
|
||||
@show_break_interventions = false
|
||||
end
|
||||
|
||||
@hide_rfc_button = @embed_options[:disable_rfc]
|
||||
|
||||
|
||||
@search = Search.new
|
||||
@search.exercise = @exercise
|
||||
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
|
||||
@paths = collect_paths(@files)
|
||||
|
||||
if current_user.respond_to? :external_id
|
||||
@user_id = current_user.external_id
|
||||
else
|
||||
@user_id = current_user.id
|
||||
end
|
||||
@user_id = if current_user.respond_to? :external_id
|
||||
current_user.external_id
|
||||
else
|
||||
current_user.id
|
||||
end
|
||||
end
|
||||
|
||||
def set_course_token
|
||||
lti_parameters = LtiParameter.where(external_users_id: current_user.id,
|
||||
exercises_id: @exercise.id).last
|
||||
exercises_id: @exercise.id).last
|
||||
if lti_parameters
|
||||
lti_json = lti_parameters.lti_parameters["launch_presentation_return_url"]
|
||||
lti_json = lti_parameters.lti_parameters['launch_presentation_return_url']
|
||||
|
||||
@course_token =
|
||||
unless lti_json.nil?
|
||||
if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/)
|
||||
match.captures.first
|
||||
else
|
||||
""
|
||||
end
|
||||
if lti_json.nil?
|
||||
''
|
||||
else
|
||||
if match = lti_json.match(%r{^.*courses/([a-z0-9\-]+)/sections})
|
||||
match.captures.first
|
||||
else
|
||||
""
|
||||
''
|
||||
end
|
||||
end
|
||||
else
|
||||
# no consumer, therefore implementation with internal user
|
||||
@course_token = "702cbd2a-c84c-4b37-923a-692d7d1532d0"
|
||||
@course_token = '702cbd2a-c84c-4b37-923a-692d7d1532d0'
|
||||
end
|
||||
end
|
||||
private :set_course_token
|
||||
@@ -294,14 +295,15 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def intervention
|
||||
intervention = Intervention.find_by_name(params[:intervention_type])
|
||||
unless intervention.nil?
|
||||
if intervention.nil?
|
||||
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
|
||||
else
|
||||
uei = UserExerciseIntervention.new(
|
||||
user: current_user, exercise: @exercise, intervention: intervention,
|
||||
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user))
|
||||
user: current_user, exercise: @exercise, intervention: intervention,
|
||||
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user)
|
||||
)
|
||||
uei.save
|
||||
render(json: {success: 'true'})
|
||||
else
|
||||
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -310,9 +312,9 @@ class ExercisesController < ApplicationController
|
||||
search = Search.new(user: current_user, exercise: @exercise, search: search_text)
|
||||
|
||||
begin search.save
|
||||
render(json: {success: 'true'})
|
||||
rescue
|
||||
render(json: {success: 'false', error: "could not save search: #{$!}"})
|
||||
render(json: {success: 'true'})
|
||||
rescue StandardError
|
||||
render(json: {success: 'false', error: "could not save search: #{$ERROR_INFO}"})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -386,9 +388,9 @@ class ExercisesController < ApplicationController
|
||||
@search = policy_scope(Tag).ransack(params[:q])
|
||||
@tags = @search.result.order(:name)
|
||||
checked_exercise_tags = @exercise.exercise_tags
|
||||
checked_tags = checked_exercise_tags.collect{|e| e.tag}.to_set
|
||||
checked_tags = checked_exercise_tags.collect(&:tag).to_set
|
||||
unchecked_tags = Tag.all.to_set.subtract checked_tags
|
||||
@exercise_tags = checked_exercise_tags + unchecked_tags.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)}
|
||||
@exercise_tags = checked_exercise_tags + unchecked_tags.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag) }
|
||||
end
|
||||
private :collect_set_and_unset_exercise_tags
|
||||
|
||||
@@ -401,27 +403,39 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def statistics
|
||||
if(@external_user)
|
||||
if @external_user
|
||||
authorize(@external_user, :statistics?)
|
||||
@submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
|
||||
interventions = UserExerciseIntervention.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)
|
||||
@all_events = (@submissions + interventions).sort_by { |a| a.created_at }
|
||||
@deltas = @all_events.map.with_index do |item, index|
|
||||
delta = item.created_at - @all_events[index - 1].created_at if index > 0
|
||||
if delta == nil or delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS then 0 else delta end
|
||||
end
|
||||
@working_times_until = []
|
||||
@all_events.each_with_index do |_, index|
|
||||
@working_times_until.push((format_time_difference(@deltas[0..index].inject(:+)) if index > 0))
|
||||
if policy(@exercise).detailed_statistics?
|
||||
@submissions = Submission.where(user: @external_user, exercise_id: @exercise.id).in_study_group_of(current_user).order('created_at')
|
||||
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id, @exercise.id)
|
||||
@all_events = (@submissions + interventions).sort_by(&:created_at)
|
||||
@deltas = @all_events.map.with_index do |item, index|
|
||||
delta = item.created_at - @all_events[index - 1].created_at if index > 0
|
||||
delta.nil? || (delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS) ? 0 : delta
|
||||
end
|
||||
@working_times_until = []
|
||||
@all_events.each_with_index do |_, index|
|
||||
@working_times_until.push((format_time_difference(@deltas[0..index].inject(:+)) if index > 0))
|
||||
end
|
||||
else
|
||||
latest_submissions = Submission.where(user: @external_user, exercise_id: @exercise.id).in_study_group_of(current_user).final.latest
|
||||
relevant_submissions = latest_submissions.before_deadline.or(latest_submissions.within_grace_period).or(latest_submissions.after_late_deadline)
|
||||
@submissions = relevant_submissions.sort_by(&:created_at)
|
||||
@all_events = @submissions
|
||||
end
|
||||
render 'exercises/external_users/statistics'
|
||||
else
|
||||
user_statistics = {}
|
||||
additional_filter = if policy(@exercise).detailed_statistics?
|
||||
''
|
||||
else
|
||||
"AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"
|
||||
end
|
||||
query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs
|
||||
FROM submissions WHERE exercise_id = #{@exercise.id} GROUP BY
|
||||
FROM submissions WHERE exercise_id = #{@exercise.id} #{additional_filter} GROUP BY
|
||||
user_id;"
|
||||
ApplicationRecord.connection.execute(query).each do |tuple|
|
||||
user_statistics[tuple["user_id"].to_i] = tuple
|
||||
user_statistics[tuple['user_id'].to_i] = tuple
|
||||
end
|
||||
render locals: {
|
||||
user_statistics: user_statistics
|
||||
@@ -441,7 +455,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def transmit_lti_score
|
||||
::NewRelic::Agent.add_custom_attributes({ submission: @submission.id, normalized_score: @submission.normalized_score })
|
||||
::NewRelic::Agent.add_custom_attributes({submission: @submission.id, normalized_score: @submission.normalized_score})
|
||||
response = send_score(@submission.exercise_id, @submission.normalized_score, @submission.user_id)
|
||||
|
||||
if response[:status] == 'success'
|
||||
@@ -458,17 +472,17 @@ class ExercisesController < ApplicationController
|
||||
def update
|
||||
collect_set_and_unset_exercise_tags
|
||||
myparam = exercise_params
|
||||
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
checked_exercise_tags = @exercise_tags.select { |et| myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { |et| myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
|
||||
for et in checked_exercise_tags
|
||||
checked_exercise_tags.each do |et|
|
||||
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||
et.exercise = @exercise
|
||||
end
|
||||
|
||||
myparam[:exercise_tags] = checked_exercise_tags
|
||||
myparam.delete :tag_ids
|
||||
removed_exercise_tags.map {|et| et.destroy}
|
||||
removed_exercise_tags.map(&:destroy)
|
||||
update_and_respond(object: @exercise, params: myparam)
|
||||
end
|
||||
|
||||
@@ -511,21 +525,21 @@ class ExercisesController < ApplicationController
|
||||
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id, session[:consumer_id])
|
||||
respond_to do |format|
|
||||
format.html {redirect_to(rfc)}
|
||||
format.json {render(json: {redirect: url_for(rfc)})}
|
||||
format.html { redirect_to(rfc) }
|
||||
format.json { render(json: {redirect: url_for(rfc)}) }
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
# redirect to feedback page if score is less than 100 percent
|
||||
if @exercise.needs_more_feedback? && !@embed_options[:disable_redirect_to_feedback]
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id, session[:consumer_id])
|
||||
redirect_to_user_feedback
|
||||
else
|
||||
redirect_to_lti_return_path
|
||||
end
|
||||
return
|
||||
if @exercise.needs_more_feedback? && !@embed_options[:disable_redirect_to_feedback]
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id, session[:consumer_id])
|
||||
redirect_to_user_feedback
|
||||
else
|
||||
redirect_to_lti_return_path
|
||||
end
|
||||
return
|
||||
end
|
||||
redirect_to_lti_return_path
|
||||
end
|
||||
@@ -547,12 +561,11 @@ class ExercisesController < ApplicationController
|
||||
def study_group_dashboard
|
||||
authorize!
|
||||
@study_group_id = params[:study_group_id]
|
||||
@request_for_comments = RequestForComment.
|
||||
where(exercise: @exercise).includes(:submission).
|
||||
where(submissions: {study_group_id: @study_group_id}).
|
||||
order(created_at: :desc)
|
||||
@request_for_comments = RequestForComment
|
||||
.where(exercise: @exercise).includes(:submission)
|
||||
.where(submissions: {study_group_id: @study_group_id})
|
||||
.order(created_at: :desc)
|
||||
|
||||
@graph_data = @exercise.get_working_times_for_study_group(@study_group_id)
|
||||
end
|
||||
|
||||
end
|
||||
|
@@ -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,36 +207,34 @@ class Exercise < ApplicationRecord
|
||||
max_bucket = 100
|
||||
maximum_score = self.maximum_score
|
||||
|
||||
if user.blank?
|
||||
additional_filter = ''
|
||||
else
|
||||
additional_filter = "AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
|
||||
end
|
||||
additional_filter = if user.blank?
|
||||
''
|
||||
else
|
||||
"AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
|
||||
end
|
||||
|
||||
results = self.class.connection.execute(study_group_working_time_query(id, study_group_id, additional_filter)).each do |tuple|
|
||||
if maximum_score > 0.0 && tuple['score'] <= maximum_score
|
||||
bucket = (tuple['score'] / maximum_score * max_bucket).round
|
||||
else
|
||||
bucket = max_bucket # maximum_score / maximum_score will always be 1
|
||||
end
|
||||
bucket = if maximum_score > 0.0 && tuple['score'] <= maximum_score
|
||||
(tuple['score'] / maximum_score * max_bucket).round
|
||||
else
|
||||
max_bucket # maximum_score / maximum_score will always be 1
|
||||
end
|
||||
|
||||
user_progress[bucket] ||= []
|
||||
additional_user_data[bucket] ||= []
|
||||
additional_user_data[max_bucket + 1] ||= []
|
||||
|
||||
user_progress[bucket][tuple['index']] = tuple["working_time_per_score"]
|
||||
additional_user_data[bucket][tuple['index']] = {start_time: tuple["start_time"], score: tuple["score"]}
|
||||
user_progress[bucket][tuple['index']] = tuple['working_time_per_score']
|
||||
additional_user_data[bucket][tuple['index']] = {start_time: tuple['start_time'], score: tuple['score']}
|
||||
additional_user_data[max_bucket + 1][tuple['index']] = {id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
|
||||
end
|
||||
|
||||
if results.ntuples > 0
|
||||
first_index = results[0]['index']
|
||||
last_index = results[results.ntuples-1]['index']
|
||||
last_index = results[results.ntuples - 1]['index']
|
||||
buckets = last_index - first_index
|
||||
user_progress.each do |timings_array|
|
||||
if timings_array.present? && timings_array.length != buckets + 1
|
||||
timings_array[buckets] = nil
|
||||
end
|
||||
timings_array[buckets] = nil if timings_array.present? && timings_array.length != buckets + 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -239,8 +242,8 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = "[" + quantiles.join(",") + "]"
|
||||
result = self.class.connection.execute("""
|
||||
quantiles_str = '[' + quantiles.join(',') + ']'
|
||||
result = self.class.connection.execute(''"
|
||||
WITH working_time AS
|
||||
(
|
||||
SELECT user_id,
|
||||
@@ -346,13 +349,12 @@ class Exercise < ApplicationRecord
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
""")
|
||||
"'')
|
||||
if result.count > 0
|
||||
quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight}
|
||||
quantiles.each_with_index.map { |_q, i| Time.parse(result[i]['unnest']).seconds_since_midnight }
|
||||
else
|
||||
quantiles.map{|q| 0}
|
||||
quantiles.map { |_q| 0 }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def retrieve_working_time_statistics
|
||||
@@ -363,72 +365,74 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def average_working_time
|
||||
self.class.connection.execute("""
|
||||
self.class.connection.execute(''"
|
||||
SELECT avg(working_time) as average_time
|
||||
FROM
|
||||
(#{user_working_time_query}) AS baz;
|
||||
""").first['average_time']
|
||||
"'').first['average_time']
|
||||
end
|
||||
|
||||
def average_working_time_for(user_id)
|
||||
if @working_time_statistics == nil
|
||||
retrieve_working_time_statistics()
|
||||
end
|
||||
@working_time_statistics[user_id]["working_time"]
|
||||
retrieve_working_time_statistics if @working_time_statistics.nil?
|
||||
@working_time_statistics[user_id]['working_time']
|
||||
end
|
||||
|
||||
def accumulated_working_time_for_only(user)
|
||||
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
|
||||
Time.parse(self.class.connection.execute("""
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
|
||||
GROUP BY user_id, id, exercise_id),
|
||||
MAX_POINTS AS
|
||||
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
|
||||
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
|
||||
begin
|
||||
Time.parse(self.class.connection.execute(''"
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
|
||||
GROUP BY user_id, id, exercise_id),
|
||||
MAX_POINTS AS
|
||||
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
|
||||
|
||||
-- filter for rows containing max points
|
||||
TIME_MAX_SCORE AS
|
||||
(SELECT *
|
||||
FROM WORKING_TIME W1, MAX_POINTS MS
|
||||
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
|
||||
-- filter for rows containing max points
|
||||
TIME_MAX_SCORE AS
|
||||
(SELECT *
|
||||
FROM WORKING_TIME W1, MAX_POINTS MS
|
||||
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
|
||||
|
||||
-- find row containing the first time max points
|
||||
FIRST_TIME_MAX_SCORE AS
|
||||
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
|
||||
FROM (
|
||||
SELECT id,USER_id,exercise_id,max_score,working_time,
|
||||
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM TIME_MAX_SCORE) T
|
||||
WHERE rn = 1),
|
||||
-- find row containing the first time max points
|
||||
FIRST_TIME_MAX_SCORE AS
|
||||
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
|
||||
FROM (
|
||||
SELECT id,USER_id,exercise_id,max_score,working_time,
|
||||
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM TIME_MAX_SCORE) T
|
||||
WHERE rn = 1),
|
||||
|
||||
TIMES_UNTIL_MAX_POINTS AS (
|
||||
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
|
||||
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
|
||||
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
|
||||
TIMES_UNTIL_MAX_POINTS AS (
|
||||
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
|
||||
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
|
||||
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
|
||||
|
||||
-- if user never makes it to max points, take all times
|
||||
ALL_WORKING_TIMES_UNTIL_MAX AS
|
||||
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
|
||||
UNION ALL
|
||||
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
|
||||
-- if user never makes it to max points, take all times
|
||||
ALL_WORKING_TIMES_UNTIL_MAX AS
|
||||
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
|
||||
UNION ALL
|
||||
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
|
||||
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM ALL_WORKING_TIMES_UNTIL_MAX
|
||||
)
|
||||
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
|
||||
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
|
||||
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
|
||||
""").first["working_time"]).seconds_since_midnight rescue 0
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM ALL_WORKING_TIMES_UNTIL_MAX
|
||||
)
|
||||
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
|
||||
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
|
||||
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
|
||||
"'').first['working_time']).seconds_since_midnight
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def duplicate(attributes = {})
|
||||
@@ -453,7 +457,8 @@ class Exercise < ApplicationRecord
|
||||
return 'main_file'
|
||||
elsif (file_class == 'internal') && (comment == 'main')
|
||||
end
|
||||
return 'regular_file'
|
||||
|
||||
'regular_file'
|
||||
end
|
||||
|
||||
def from_proforma_xml(xml_string)
|
||||
@@ -467,23 +472,23 @@ class Exercise < ApplicationRecord
|
||||
description: description,
|
||||
instructions: description
|
||||
}
|
||||
task_node.xpath('p:files/p:file').all? { |file|
|
||||
task_node.xpath('p:files/p:file').all? do |file|
|
||||
file_name_split = file.xpath('@filename').first.value.split('.')
|
||||
file_class = file.xpath('@class').first.value
|
||||
role = determine_file_role_from_proforma_file(task_node, file)
|
||||
feedback_message_nodes = task_node.xpath("p:tests/p:test/p:test-configuration/c:feedback-message/text()")
|
||||
feedback_message_nodes = task_node.xpath('p:tests/p:test/p:test-configuration/c:feedback-message/text()')
|
||||
files.build({
|
||||
name: file_name_split.first,
|
||||
content: file.xpath('text()').first.content,
|
||||
read_only: false,
|
||||
hidden: file_class == 'internal',
|
||||
role: role,
|
||||
feedback_message: (role == 'teacher_defined_test') ? feedback_message_nodes.first.content : nil,
|
||||
file_type: FileType.where(
|
||||
file_extension: ".#{file_name_split.second}"
|
||||
).take
|
||||
})
|
||||
}
|
||||
name: file_name_split.first,
|
||||
content: file.xpath('text()').first.content,
|
||||
read_only: false,
|
||||
hidden: file_class == 'internal',
|
||||
role: role,
|
||||
feedback_message: role == 'teacher_defined_test' ? feedback_message_nodes.first.content : nil,
|
||||
file_type: FileType.where(
|
||||
file_extension: ".#{file_name_split.second}"
|
||||
).take
|
||||
})
|
||||
end
|
||||
self.execution_environment_id = 1
|
||||
end
|
||||
|
||||
@@ -495,7 +500,11 @@ class Exercise < ApplicationRecord
|
||||
def maximum_score(user = nil)
|
||||
if user
|
||||
# FIXME: where(user: user) will not work here!
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0
|
||||
begin
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC').first.score || 0
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
else
|
||||
files.teacher_defined_tests.sum(:weight)
|
||||
end
|
||||
@@ -510,7 +519,7 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def finishers
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w(submit assess)}).distinct
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w[submit assess]}).distinct
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
@@ -523,12 +532,22 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def valid_main_file?
|
||||
if files.main_files.count > 1
|
||||
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
|
||||
end
|
||||
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file')) if files.main_files.count > 1
|
||||
end
|
||||
private :valid_main_file?
|
||||
|
||||
def valid_submission_deadlines?
|
||||
return unless submission_deadline.present? || late_submission_deadline.present?
|
||||
|
||||
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_alone')) if late_submission_deadline.present? && submission_deadline.blank?
|
||||
|
||||
if submission_deadline.present? && late_submission_deadline.present? &&
|
||||
late_submission_deadline < submission_deadline
|
||||
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_before_submission_deadline'))
|
||||
end
|
||||
end
|
||||
private :valid_submission_deadlines?
|
||||
|
||||
def needs_more_feedback?
|
||||
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
|
||||
end
|
||||
@@ -543,5 +562,4 @@ class Exercise < ApplicationRecord
|
||||
WHERE exercise_id = #{id}
|
||||
) AS t ON t.fv = submissions.id").distinct
|
||||
end
|
||||
|
||||
end
|
||||
|
@@ -15,11 +15,22 @@ class Submission < ApplicationRecord
|
||||
has_many :structured_errors
|
||||
has_many :comments, through: :files
|
||||
|
||||
belongs_to :external_users, -> { where(submissions: {user_type: 'ExternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'ExternalUser', optional: true
|
||||
belongs_to :internal_users, -> { where(submissions: {user_type: 'InternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'InternalUser', optional: true
|
||||
|
||||
delegate :execution_environment, to: :exercise
|
||||
|
||||
scope :final, -> { where(cause: 'submit') }
|
||||
scope :intermediate, -> { where.not(cause: 'submit') }
|
||||
|
||||
scope :before_deadline, -> { joins(:exercise).where('submissions.updated_at <= exercises.submission_deadline OR exercises.submission_deadline IS NULL') }
|
||||
scope :within_grace_period, -> { joins(:exercise).where('(submissions.updated_at > exercises.submission_deadline) AND (submissions.updated_at <= exercises.late_submission_deadline OR exercises.late_submission_deadline IS NULL)') }
|
||||
scope :after_late_deadline, -> { joins(:exercise).where('submissions.updated_at > exercises.late_submission_deadline') }
|
||||
|
||||
scope :latest, -> { order(updated_at: :desc).limit(1) }
|
||||
|
||||
scope :in_study_group_of, ->(user) { where(study_group_id: user.study_groups) unless user.admin? }
|
||||
|
||||
validates :cause, inclusion: {in: CAUSES}
|
||||
validates :exercise_id, presence: true
|
||||
|
||||
@@ -29,6 +40,7 @@ class Submission < ApplicationRecord
|
||||
def build_files_hash(files, attribute)
|
||||
files.map(&attribute.to_proc).zip(files).to_h
|
||||
end
|
||||
|
||||
private :build_files_hash
|
||||
|
||||
def collect_files
|
||||
@@ -42,7 +54,7 @@ class Submission < ApplicationRecord
|
||||
end
|
||||
|
||||
def normalized_score
|
||||
::NewRelic::Agent.add_custom_attributes({ unnormalized_score: score })
|
||||
::NewRelic::Agent.add_custom_attributes({unnormalized_score: score})
|
||||
if !score.nil? && !exercise.maximum_score.nil? && (exercise.maximum_score > 0)
|
||||
score / exercise.maximum_score
|
||||
else
|
||||
@@ -62,6 +74,32 @@ class Submission < ApplicationRecord
|
||||
Submission.model_name.human
|
||||
end
|
||||
|
||||
def before_deadline?
|
||||
if exercise.submission_deadline.present?
|
||||
updated_at <= exercise.submission_deadline
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def within_grace_period?
|
||||
if exercise.submission_deadline.present? && exercise.late_submission_deadline.present?
|
||||
updated_at > exercise.submission_deadline && updated_at <= exercise.late_submission_deadline
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def after_late_deadline?
|
||||
if exercise.late_submission_deadline.present?
|
||||
updated_at > exercise.late_submission_deadline
|
||||
elsif exercise.submission_deadline.present?
|
||||
updated_at > exercise.submission_deadline
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_feedback?
|
||||
((user_id + exercise.created_at.to_i) % 10 == 1) && exercise.needs_more_feedback?
|
||||
end
|
||||
@@ -71,6 +109,6 @@ class Submission < ApplicationRecord
|
||||
end
|
||||
|
||||
def unsolved_rfc
|
||||
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order("RANDOM()").find { | rfc_element |( (rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && (!rfc_element.question.empty?)) }
|
||||
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order("RANDOM()").find { |rfc_element| ((rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && (!rfc_element.question.empty?)) }
|
||||
end
|
||||
end
|
||||
|
@@ -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,32 +36,54 @@ h1 = "#{@exercise} (external user #{link_to_if(policy(@external_user).show?, @ex
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
- ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
|
||||
th.header = t(title)
|
||||
th.header = t('.time')
|
||||
th.header = t('.cause')
|
||||
th.header = t('.score')
|
||||
th.header = t('.tests')
|
||||
th.header = t('.time_difference') if policy(@exercise).detailed_statistics?
|
||||
tbody
|
||||
- @all_events.each_with_index do |this, index|
|
||||
- highlight = (index > 0 and @deltas[index] == 0 and this.created_at.to_s != @all_events[index - 1].created_at.to_s)
|
||||
tr data-id=this.id class=('highlight' if highlight)
|
||||
- row_classes = ''
|
||||
- row_classes += ' highlight' if highlight
|
||||
- row_classes += ' before_deadline' if this.is_a?(Submission) && this.before_deadline?
|
||||
- row_classes += ' within_grace_period' if this.is_a?(Submission) && this.within_grace_period?
|
||||
- row_classes += ' after_late_deadline' if this.is_a?(Submission) && this.after_late_deadline?
|
||||
tr data-id=this.id class=row_classes
|
||||
td.clickable = this.created_at.strftime("%F %T")
|
||||
- if this.is_a?(Submission)
|
||||
td = this.cause
|
||||
td = this.score
|
||||
td
|
||||
td.align-middle
|
||||
-this.testruns.each do |run|
|
||||
- if run.passed
|
||||
.unit-test-result.positive-result title=run.output
|
||||
- else
|
||||
.unit-test-result.unknown-result title=run.output
|
||||
td = @working_times_until[index] if index > 0
|
||||
td = @working_times_until[index] if index > 0 if policy(@exercise).detailed_statistics?
|
||||
- elsif this.is_a? UserExerciseIntervention
|
||||
td = this.intervention.name
|
||||
td =
|
||||
td =
|
||||
td = @working_times_until[index] if index > 0
|
||||
p = t('.addendum', delta: StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS / 60)
|
||||
.d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
|
||||
div#progress_chart.col-lg-12
|
||||
.graph-functions-2
|
||||
td = @working_times_until[index] if index > 0 if policy(@exercise).detailed_statistics?
|
||||
small
|
||||
b
|
||||
= t('.legend')
|
||||
.container.px-0.border
|
||||
.row.w-100.mx-0
|
||||
.col-sm-3.py-2
|
||||
= t('.no_deadline')
|
||||
.col-sm-3.before_deadline.py-2
|
||||
= t('.before_deadline')
|
||||
.col-sm-3.within_grace_period.py-2
|
||||
= t('.within_grace_period')
|
||||
.col-sm-3.after_late_deadline.py-2
|
||||
= t('.after_late_deadline')
|
||||
- if current_user.try(:admin?)
|
||||
p = t('.addendum', delta: StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS / 60)
|
||||
.d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
|
||||
div#progress_chart.col-lg-12
|
||||
.graph-functions-2
|
||||
|
||||
- else
|
||||
p = t('.no_data_available')
|
||||
|
@@ -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,12 +25,13 @@ h1 = @exercise
|
||||
p = @exercise.average_working_time
|
||||
|
||||
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label|
|
||||
strong = label
|
||||
-if symbol==:external_users
|
||||
-working_time_array = []
|
||||
- if symbol==:internal_users && current_user.admin?
|
||||
strong = label
|
||||
- if symbol==:external_users
|
||||
- working_time_array = []
|
||||
- @exercise.send(symbol).distinct().each do |user|
|
||||
-working_time = @exercise.average_working_time_for(user.id) or 0
|
||||
-working_time_array.push working_time
|
||||
- working_time = @exercise.average_working_time_for(user.id) or 0
|
||||
- working_time_array.push working_time
|
||||
hr
|
||||
.d-none#data data-working-time=ActiveSupport::JSON.encode(working_time_array)
|
||||
.working-time-graphs
|
||||
@@ -38,19 +39,26 @@ h1 = @exercise
|
||||
hr
|
||||
div#chart_2
|
||||
hr
|
||||
- if current_user.admin?
|
||||
- submissions = Submission.where(user: @exercise.send(symbol).distinct, exercise: @exercise).in_study_group_of(current_user)
|
||||
- if !policy(@exercise).detailed_statistics?
|
||||
- submissions = submissions.final
|
||||
- any_viewable_submission = submissions.first
|
||||
- if any_viewable_submission
|
||||
.table-responsive
|
||||
table.table.table-striped.sortable
|
||||
thead
|
||||
tr
|
||||
- ['.user', '.score', '.runs', '.worktime'].each do |title|
|
||||
th.header = t(title)
|
||||
th.header = t('.user')
|
||||
th.header = t('.score')
|
||||
th.header = t('.runs') if policy(@exercise).detailed_statistics?
|
||||
th.header = t('.worktime') if policy(@exercise).detailed_statistics?
|
||||
tbody
|
||||
- @exercise.send(symbol).distinct().each do |user|
|
||||
- users = symbol.to_s.classify.constantize.where(id: submissions.joins(symbol).group(:user_id).select(:user_id).distinct)
|
||||
- users.each do |user|
|
||||
- if user_statistics[user.id] then us = user_statistics[user.id] else us = {"maximum_score" => nil, "runs" => nil}
|
||||
- label = "#{user.displayname}"
|
||||
tr
|
||||
td = link_to_if symbol==:external_users && policy(user).statistics?, label, {controller: "exercises", action: "statistics", external_user_id: user.id, id: @exercise.id}
|
||||
td = us['maximum_score'] or 0
|
||||
td = us['runs']
|
||||
td = @exercise.average_working_time_for(user.id) or 0
|
||||
td = us['runs'] if policy(@exercise).detailed_statistics?
|
||||
td = @exercise.average_working_time_for(user.id) or 0 if policy(@exercise).detailed_statistics?
|
||||
|
@@ -1,7 +1,7 @@
|
||||
h1 = @user.displayname
|
||||
|
||||
= row(label: 'external_user.name', value: @user.name)
|
||||
= row(label: 'external_user.email', value: @user.email)
|
||||
= row(label: 'external_user.email', value: @user.email) if current_user.admin?
|
||||
= row(label: 'external_user.external_id') do
|
||||
code
|
||||
= @user.external_id
|
||||
@@ -10,10 +10,11 @@ h1 = @user.displayname
|
||||
|
||||
h4.mt-4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user)) if policy(@user).statistics?
|
||||
|
||||
h4.mt-4 = t('.tag_statistics')
|
||||
#loading
|
||||
.spinner
|
||||
= t('.loading_tag_statistics')
|
||||
#no-elements
|
||||
= t('.empty_tag_statistics')
|
||||
#tag-grid
|
||||
- if current_user.admin?
|
||||
h4.mt-4 = t('.tag_statistics')
|
||||
#loading
|
||||
.spinner
|
||||
= t('.loading_tag_statistics')
|
||||
#no-elements
|
||||
= t('.empty_tag_statistics')
|
||||
#tag-grid
|
||||
|
@@ -1,19 +1,30 @@
|
||||
h1 = t('.title')
|
||||
|
||||
- exercises = Exercise.where(:id => @user.submissions.group(:exercise_id).select(:exercise_id).distinct)
|
||||
- submissions = Submission.where(user: @user).in_study_group_of(current_user)
|
||||
- exercises = Exercise.where(id: submissions.joins(:exercise).group(:exercise_id).select(:exercise_id).distinct)
|
||||
- if !policy(exercises.first).detailed_statistics?
|
||||
- submissions = submissions.final
|
||||
- any_viewable_submission = submissions.first
|
||||
|
||||
.table-responsive
|
||||
table.table.table-striped.sortable
|
||||
thead
|
||||
tr
|
||||
- ['.exercise', '.score', '.runs', '.worktime'].each do |title|
|
||||
th.header = t(title)
|
||||
tbody
|
||||
- exercises.each do |exercise|
|
||||
- if statistics[exercise.id]
|
||||
- stats = statistics[exercise.id]
|
||||
tr
|
||||
td = link_to_if policy(exercise).show?, exercise, controller: "exercises", action: "statistics", external_user_id: @user.id, id: exercise.id
|
||||
td = stats["maximum_score"] or 0
|
||||
td = stats["runs"] or 0
|
||||
td = stats["working_time"] or 0
|
||||
- if any_viewable_submission && policy(any_viewable_submission).show_study_group?
|
||||
.table-responsive
|
||||
table.table.table-striped.sortable
|
||||
thead
|
||||
tr
|
||||
th.header = t('.exercise')
|
||||
th.header = t('.score')
|
||||
th.header = t('.runs') if policy(exercises.first).detailed_statistics?
|
||||
th.header = t('.worktime') if policy(exercises.first).detailed_statistics?
|
||||
tbody
|
||||
- exercises.each do |exercise|
|
||||
// Grab any submission in context of study group (or all if admin). Then check for permission
|
||||
- any_submission = submissions.where(exercise: exercise).first
|
||||
- if any_submission && policy(any_submission).show_study_group? && statistics[exercise.id]
|
||||
- stats = statistics[exercise.id]
|
||||
tr
|
||||
td = link_to exercise, controller: "exercises", action: "statistics", external_user_id: @user.id, id: exercise.id
|
||||
td = stats["maximum_score"] or 0
|
||||
td = stats["runs"] or 0 if policy(exercises.first).detailed_statistics?
|
||||
td = stats["working_time"] or 0 if policy(exercises.first).detailed_statistics?
|
||||
- else
|
||||
= t('exercises.external_users.statistics.no_data_available')
|
Reference in New Issue
Block a user