-
Notifications
You must be signed in to change notification settings - Fork 68
Expand file tree
/
Copy pathgrading_job.ex
More file actions
337 lines (295 loc) · 9.32 KB
/
Copy pathgrading_job.ex
File metadata and controls
337 lines (295 loc) · 9.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
defmodule Cadet.Autograder.GradingJob do
@moduledoc """
This module contains logic for finding answers to be graded and
processing/dispatching them as appropriate
"""
use Cadet, :context
import Ecto.Query
require Logger
alias Cadet.Accounts.{Team, TeamMember}
alias Cadet.Assessments
alias Cadet.Assessments.{Answer, Assessment, Question, Submission, SubmissionVotes}
alias Cadet.Autograder.Utilities
alias Cadet.Courses.AssessmentConfig
alias Cadet.Jobs.Log
def close_and_make_empty_submission(assessment = %Assessment{id: id}) do
id
|> Utilities.fetch_submissions(assessment.course_id)
|> Enum.map(fn %{student_id: student_id, submission: submission} ->
if submission do
update_submission_status_to_submitted(submission)
else
insert_empty_submission(%{student_id: student_id, assessment: assessment})
end
end)
end
def grade_all_due_yesterday do
# 1435 = 1 day - 5 minutes
if Log.log_execution("grading_job", Timex.Duration.from_minutes(1435)) do
Logger.info("Started autograding")
for assessment <- Utilities.fetch_assessments_due_yesterday() do
assessment
|> close_and_make_empty_submission()
|> Enum.each(&grade_individual_submission(&1, assessment))
end
else
Logger.info("Not grading - raced")
end
end
@doc """
Exposed as public function in case future mix tasks are needed to regrade
certain submissions. Manual grading can also be triggered from iex with this
function.
Takes in submission to be graded. Submission will be graded regardless of its
assessment's close_by date or submission status.
Every answer will be regraded regardless of its current autograding status.
"""
def force_grade_individual_submission(submission = %Submission{}, overwrite \\ false) do
assessment =
if Ecto.assoc_loaded?(submission.assessment) do
submission.assessment
else
submission |> Repo.preload(:assessment) |> Map.get(:assessment)
end
assessment = preprocess_assessment_for_grading(assessment)
grade_individual_submission(submission, assessment, true, overwrite)
end
# This function requires that assessment questions are already preloaded in sorted
# order for autograding to function correctly.
defp grade_individual_submission(
%Submission{id: submission_id},
%Assessment{questions: questions},
regrade \\ false,
overwrite \\ false
) do
answers =
Answer
|> where(submission_id: ^submission_id)
|> order_by(:question_id)
|> Repo.all()
grade_submission_question_answer_lists(
submission_id,
questions,
answers,
regrade,
overwrite
)
end
defp preprocess_assessment_for_grading(assessment = %Assessment{}) do
if Ecto.assoc_loaded?(assessment.questions) do
Utilities.sort_assessment_questions(assessment)
else
questions =
Question
|> where(assessment_id: ^assessment.id)
|> order_by(:id)
|> Repo.all()
Map.put(assessment, :questions, questions)
end
end
defp insert_empty_submission(%{student_id: student_id, assessment: assessment}) do
if Assessments.is_team_assessment?(assessment.id) do
# Get current team if any
team =
Team
|> where(assessment_id: ^assessment.id)
|> join(:inner, [t], tm in assoc(t, :team_members))
|> where([_, tm], tm.student_id == ^student_id)
|> Repo.one()
team =
if team do
team
else
# Student is not in any team
# Create new team just for the student
team =
%Team{}
|> Team.changeset(%{
assessment_id: assessment.id
})
|> Repo.insert!()
%TeamMember{}
|> TeamMember.changeset(%{
team_id: team.id,
student_id: student_id
})
|> Repo.insert!()
team
end
find_or_create_team_submission(team.id, assessment)
else
# Individual assessment
%Submission{}
|> Submission.changeset(%{
student_id: student_id,
assessment: assessment,
status: :submitted
})
|> Repo.insert!()
end
end
defp find_or_create_team_submission(team_id, assessment) when is_ecto_id(team_id) do
submission =
Submission
|> where(team_id: ^team_id, assessment_id: ^assessment.id)
|> Repo.one()
if submission do
submission
else
%Submission{}
|> Submission.changeset(%{
team_id: team_id,
assessment: assessment,
status: :submitted
})
|> Repo.insert!()
end
end
defp update_submission_status_to_submitted(submission = %Submission{status: status}) do
if status != :submitted do
Cadet.Accounts.Notifications.write_notification_when_student_submits(submission)
submission
|> Submission.changeset(%{status: :submitted})
|> Repo.update!()
else
submission
end
end
def grade_answer(
question = %Question{type: type},
answers,
answer = %Answer{},
questions,
overwrite \\ false
) do
case type do
:programming ->
Utilities.dispatch_programming_answer(question, answers, answer, questions, overwrite)
:mcq ->
grade_mcq_answer(answer, question)
:voting ->
grade_voting_answer(answer, question)
end
end
defp grade_voting_answer(answer = %Answer{submission_id: submission_id}, question = %Question{}) do
is_nil_entries =
Submission
|> where(id: ^submission_id)
|> join(:inner, [s], sv in SubmissionVotes,
on: sv.voter_id == s.student_id and sv.question_id == ^question.id
)
|> where([_, sv], is_nil(sv.score))
|> Repo.exists?()
xp = if is_nil_entries, do: 0, else: question.max_xp
answer
|> Answer.autograding_changeset(%{
xp_adjustment: 0,
xp: xp,
autograding_status: :success
})
|> Repo.update()
end
defp grade_mcq_answer(answer = %Answer{}, question = %Question{question: question_content}) do
correct_choice =
question_content["choices"]
|> Enum.find(&Map.get(&1, "is_correct"))
|> Map.get("choice_id")
correct? = answer.answer["choice_id"] == correct_choice
xp = if correct?, do: question.max_xp, else: 0
answer
|> Answer.autograding_changeset(%{
xp_adjustment: 0,
xp: xp,
autograding_status: :success
})
|> Repo.update()
end
defp insert_empty_answer(
submission_id,
%Question{id: question_id, type: question_type}
)
when is_ecto_id(submission_id) do
answer_content =
case question_type do
:programming -> %{code: "// Question was left blank by the student."}
:mcq -> %{choice_id: 0}
:voting -> %{completed: false}
end
%Answer{}
|> Answer.changeset(%{
answer: answer_content,
question_id: question_id,
submission_id: submission_id,
type: question_type
})
|> Answer.autograding_changeset(%{grade: 0, autograding_status: :success})
|> Repo.insert()
end
# Two finger walk down question and answer lists.
# Both lists MUST be pre-sorted by id and question_id respectively
defp grade_submission_question_answer_lists(
submission_id,
questions,
answers,
regrade,
overwrite
)
defp grade_submission_question_answer_lists(
submission_id,
questions = [question = %Question{} | question_tail],
answers = [answer = %Answer{} | answer_tail],
regrade,
overwrite
)
when is_boolean(regrade) and is_boolean(overwrite) and is_ecto_id(submission_id) do
if question.id == answer.question_id do
if regrade || answer.autograding_status in [:none, :failed] do
grade_answer(question, answers, answer, questions, overwrite)
end
grade_submission_question_answer_lists(
submission_id,
question_tail,
answer_tail,
regrade,
overwrite
)
else
insert_empty_answer(submission_id, question)
grade_submission_question_answer_lists(
submission_id,
question_tail,
answers,
regrade,
overwrite
)
end
end
defp grade_submission_question_answer_lists(
submission_id,
[question = %Question{} | question_tail],
[],
regrade,
overwrite
)
when is_boolean(regrade) and is_boolean(overwrite) and is_ecto_id(submission_id) do
insert_empty_answer(submission_id, question)
grade_submission_question_answer_lists(
submission_id,
question_tail,
[],
regrade,
overwrite
)
end
defp grade_submission_question_answer_lists(submission_id, [], [], _, _) do
submission = Repo.get(Submission, submission_id)
assessment = Repo.get(Assessment, submission.assessment_id)
assessment_config = Repo.get_by(AssessmentConfig, id: assessment.config_id)
is_grading_auto_published = assessment_config.is_grading_auto_published
is_manually_graded = assessment_config.is_manually_graded
if Assessments.is_fully_autograded?(submission_id) and is_grading_auto_published and
not is_manually_graded do
Assessments.publish_grading(submission_id)
end
end
end