Skip to content

Commit fbc6221

Browse files
committed
Introduce a new DeadlineScope for Submissions
With this scope, we extend the submissions shown to teachers. Now, a teacher has always access to the latest scored submission and the submission with the highest score (in addition to all "submit" submissions). Furthermore, for these two categories (latest / highest scored), we also respect the three deadlines (before submission deadline, within grace period, after late submission deadline) and show a respective submission. The SubmissionsController#index has not been extended with this new scope, since the performance is too bad (and it is not used frequently).
1 parent e7f4d78 commit fbc6221

File tree

6 files changed

+104
-15
lines changed

6 files changed

+104
-15
lines changed

app/controllers/exercises_controller.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,8 @@ def statistics
496496
# Show general statistic page for specific exercise
497497
contributor_statistics = {InternalUser => {}, ExternalUser => {}, ProgrammingGroup => {}}
498498

499-
query = policy_scope(Submission).select('contributor_id, contributor_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
499+
query = SubmissionPolicy::DeadlineScope.new(current_user, Submission).resolve
500+
.select('contributor_id, contributor_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
500501
.where(exercise_id: @exercise.id)
501502
.group('contributor_id, contributor_type')
502503

@@ -512,10 +513,15 @@ def statistics
512513
def external_user_statistics
513514
# Render statistics page for one specific external user
514515

515-
submissions = policy_scope(Submission).where(contributor: @external_user, exercise: @exercise)
516-
.order(:created_at)
516+
submissions = SubmissionPolicy::DeadlineScope.new(current_user, Submission).resolve
517+
.where(contributor: @external_user, exercise: @exercise)
518+
.order(submissions: {updated_at: :desc})
517519
.includes(:exercise, testruns: [:testrun_messages, {file: [:file_type]}], files: [:file_type])
518520

521+
# From here on, we switch to sort by `created_at`. This is important for the working time estimation,
522+
# since a submission is updated after the corresponding testrun finishes.
523+
# Sorting by `updated_at` would lead to wrong working time estimations (but is more efficient for the database).
524+
519525
if policy(@exercise).detailed_statistics?
520526
@show_autosaves = params[:show_autosaves] == 'true' || submissions.where.not(cause: 'autosave').none?
521527

@@ -549,11 +555,7 @@ def external_user_statistics
549555
end
550556
end
551557
else
552-
@all_events = []
553-
%i[before_deadline within_grace_period after_late_deadline].each do |filter|
554-
relevant_submission = submissions.send(filter).latest
555-
@all_events.push relevant_submission if relevant_submission.present?
556-
end
558+
@all_events = submissions.sort_by(&:created_at)
557559
end
558560

559561
render 'exercises/external_users/statistics'

app/controllers/external_users_controller.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ def show
2222
end
2323

2424
def working_time_query(tag = nil)
25+
deadline_scope_conditions = SubmissionPolicy::DeadlineScope.new(current_user, Submission).resolve
26+
2527
"
28+
WITH filtered_submissions AS (
29+
#{deadline_scope_conditions.to_sql}
30+
)
2631
SELECT contributor_id,
2732
bar.exercise_id,
2833
max(score) as maximum_score,
@@ -41,16 +46,17 @@ def working_time_query(tag = nil)
4146
(SELECT contributor_id,
4247
exercise_id,
4348
max(score) AS score,
44-
id,
45-
(created_at - lag(created_at) over (PARTITION BY contributor_id, exercise_id
46-
ORDER BY created_at)) AS working_time
47-
FROM submissions
49+
filtered_submissions.id,
50+
(filtered_submissions.updated_at - lag(filtered_submissions.updated_at) over (PARTITION BY contributor_id, exercise_id
51+
ORDER BY filtered_submissions.updated_at)) AS working_time
52+
FROM filtered_submissions
53+
JOIN exercises ON filtered_submissions.exercise_id = exercises.id
4854
WHERE #{ExternalUser.sanitize_sql(['contributor_id = ?', @user.id])}
4955
AND contributor_type = 'ExternalUser'
50-
#{current_user.admin? ? '' : "AND #{ExternalUser.sanitize_sql(['study_group_id IN (?)', current_user.study_groups.pluck(:id)])} AND cause = 'submit'"}
5156
GROUP BY exercise_id,
5257
contributor_id,
53-
id
58+
filtered_submissions.id,
59+
filtered_submissions.updated_at
5460
) AS foo
5561
) AS bar
5662
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND #{ExternalUser.sanitize_sql(['et.tag_id = ?', tag])}"}

app/policies/submission_policy.rb

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ def index?
2323
end
2424

2525
def show?
26-
admin? || author? || author_in_programming_group? || (teacher_in_study_group? && CausesScope.new(@user, @record).resolve.include?(@record.cause))
26+
return true if admin? || author? || author_in_programming_group?
27+
# Performance optimization: If this submission is within the CausesScope, we can skip the expensive DeadlineScope
28+
return true if teacher_in_study_group? && CausesScope.new(@user, @record).resolve.include?(@record.cause)
29+
30+
teacher_in_study_group? && DeadlineScope.new(@user, Submission).resolve.exists?(@record.id)
2731
end
2832

2933
class Scope < Scope
@@ -49,4 +53,69 @@ def resolve
4953
end
5054
end
5155
end
56+
57+
class DeadlineScope < Scope
58+
def resolve
59+
resolved_scope = super
60+
return resolved_scope unless @user.teacher?
61+
62+
latest_before_deadline = latest_submissions_assessed.before_deadline.arel
63+
latest_within_grace_period = latest_submissions_assessed.within_grace_period.arel
64+
latest_after_late_deadline = latest_submissions_assessed.after_late_deadline.arel
65+
highest_before_deadline = latest_submissions_assessed(highest_scored: true).before_deadline.arel
66+
highest_within_grace_period = latest_submissions_assessed(highest_scored: true).within_grace_period.arel
67+
highest_after_late_deadline = latest_submissions_assessed(highest_scored: true).after_late_deadline.arel
68+
69+
# Yes, we construct a huge union of seven relations: all three deadlines, all three highest scores and the resolved scope
70+
all_unions = construct_union(
71+
latest_before_deadline,
72+
latest_within_grace_period,
73+
latest_after_late_deadline,
74+
highest_before_deadline,
75+
highest_within_grace_period,
76+
highest_after_late_deadline,
77+
# Dirty hack, since resolved_scope.arel will loose the bindings, and replace them with $1, $2, ...
78+
Arel.sql(resolved_scope.to_sql)
79+
)
80+
81+
# Convert the union to a relation
82+
Submission.from(all_unions.as(Submission.arel_table.name))
83+
end
84+
85+
private
86+
87+
# This method is used to get the latest submission that was assessed or remote assessed.
88+
# By default, it will simply return the one with the last time stamp per exercise and contributor.
89+
# However, with the optional parameter, the highest scored submission that was scored the latest will be returned.
90+
def latest_submissions_assessed(highest_scored: false)
91+
submission_table = Submission.arel_table
92+
93+
desired_table_order = [
94+
submission_table[:exercise_id],
95+
submission_table[:contributor_type],
96+
submission_table[:contributor_id],
97+
highest_scored ? submission_table[:score].desc.nulls_last : nil,
98+
submission_table[:updated_at].desc,
99+
].compact
100+
101+
Submission.from(
102+
submission_table.project(
103+
Arel.sql('DISTINCT ON (submissions.exercise_id, submissions.contributor_type, submissions.contributor_id) submissions.*')
104+
)
105+
.order(*desired_table_order)
106+
.where(submission_table[:cause].in(%w[assess remoteAssess]))
107+
.where(submission_table[:study_group_id].in(@user.study_group_ids_as_teacher))
108+
.as(submission_table.name)
109+
)
110+
end
111+
112+
def construct_union(*args)
113+
return nil if args.empty?
114+
return args.first if args.size == 1
115+
116+
args.reduce do |union, arg|
117+
Arel::Nodes::Union.new(union, arg)
118+
end
119+
end
120+
end
52121
end

app/views/exercises/statistics.html.slim

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ h1 = @exercise
4242
= row(label: '.average_worktime') do
4343
p = @exercise.average_working_time
4444

45+
- unless policy(@exercise).detailed_statistics?
46+
.lead
47+
.card.border-info-subtle.mb-3
48+
.card-header
49+
i.fa-solid.fa-circle-info.text-info
50+
strong.text-info
51+
=> t('.final_submissions_only', count: contributor_statistics.values.sum(&:count))
52+
= t('.final_submissions_only_explanation')
4553
- contributor_statistics.each_pair do |user_type, user_with_submission_stats|
4654
- if user_with_submission_stats.any?
4755
h5 = t(".#{user_type.model_name.collection}")

config/locales/de/exercise.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ de:
212212
deadline: Abgabefrist
213213
external_users: Externe Personen
214214
final_submissions: Finale Abgaben
215+
final_submissions_only: 'Details zu %{count} Teilnehmenden und Programmiergruppen:'
216+
final_submissions_only_explanation: Die folgenden Ansichten zeigen ausschließlich finale Abgaben und Bewertungen. Dies umfasst explizit eingereichte Lösungen sowie die jeweils letzte Bewertung vor Ablauf der Abgabefrist.
215217
finishing_rate: Abschlussrate
216218
intermediate_submissions: Intermediäre Abgaben
217219
internal_users: Interne Personen

config/locales/en/exercise.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ en:
212212
deadline: Deadline
213213
external_users: External Users
214214
final_submissions: Final Submissions
215+
final_submissions_only: 'Details for %{count} distinct users and programming groups only:'
216+
final_submissions_only_explanation: The following views display only the final submissions and assessments. This includes explicitly submitted solutions and the most recent assessment before the submission deadline was reached.
215217
finishing_rate: Finishing Rate
216218
intermediate_submissions: Intermediate Submissions
217219
internal_users: Internal Users

0 commit comments

Comments
 (0)