Skip to content

Commit 0a4cdb3

Browse files
committed
Merge branch 'ground_control' into mergeGroundControl
2 parents 3c3c780 + ae7beac commit 0a4cdb3

File tree

7 files changed

+611
-206
lines changed

7 files changed

+611
-206
lines changed

lib/cadet/assessments/assessments.ex

Lines changed: 180 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,62 @@ defmodule Cadet.Assessments do
1616
@xp_early_submission_max_bonus 100
1717
@xp_bonus_assessment_type ~w(mission sidequest)a
1818
@submit_answer_roles ~w(student)a
19+
@change_dates_assessment_role ~w(staff admin)a
20+
@delete_assessment_role ~w(staff admin)a
21+
@publish_assessment_role ~w(staff admin)a
1922
@unsubmit_assessment_role ~w(staff admin)a
2023
@grading_roles ~w()a
2124
@see_all_submissions_roles ~w(staff admin)a
2225
@open_all_assessment_roles ~w(staff admin)a
2326

27+
def change_dates_assessment(_user = %User{role: role}, id, close_at, open_at) do
28+
if role in @change_dates_assessment_role do
29+
assessment = Repo.get(Assessment, id)
30+
previous_open_time = assessment.open_at
31+
32+
cond do
33+
Timex.before?(close_at, open_at) ->
34+
{:error, {:bad_request, "New end date should occur after new opening date"}}
35+
36+
Timex.before?(close_at, Timex.now()) ->
37+
{:error, {:bad_request, "New end date should occur after current time"}}
38+
39+
Timex.equal?(previous_open_time, open_at) or Timex.after?(previous_open_time, Timex.now()) ->
40+
update_assessment(id, %{close_at: close_at, open_at: open_at})
41+
42+
Timex.before?(open_at, Timex.now()) ->
43+
{:error, {:bad_request, "New Opening date should occur after current time"}}
44+
45+
true ->
46+
{:error, {:unauthorized, "Assessment is already opened"}}
47+
end
48+
else
49+
{:error, {:forbidden, "User is not permitted to edit"}}
50+
end
51+
end
52+
53+
def toggle_publish_assessment(_publisher = %User{role: role}, id, toggle_publish_to) do
54+
if role in @publish_assessment_role do
55+
update_assessment(id, %{is_published: toggle_publish_to})
56+
else
57+
{:error, {:forbidden, "User is not permitted to publish"}}
58+
end
59+
end
60+
61+
def delete_assessment(_deleter = %User{role: role}, id) do
62+
if role in @delete_assessment_role do
63+
assessment = Repo.get(Assessment, id)
64+
65+
Submission
66+
|> where(assessment_id: ^id)
67+
|> Repo.delete_all()
68+
69+
Repo.delete(assessment)
70+
else
71+
{:error, {:forbidden, "User is not permitted to delete"}}
72+
end
73+
end
74+
2475
@spec user_total_xp(%User{}) :: integer()
2576
def user_total_xp(%User{id: user_id}) when is_ecto_id(user_id) do
2677
total_xp_bonus =
@@ -163,11 +214,19 @@ defmodule Cadet.Assessments do
163214

164215
def assessment_with_questions_and_answers(id, user = %User{}, password)
165216
when is_ecto_id(id) do
217+
role = user.role
218+
166219
assessment =
167-
Assessment
168-
|> where(id: ^id)
169-
|> where(is_published: true)
170-
|> Repo.one()
220+
if role in @open_all_assessment_roles do
221+
Assessment
222+
|> where(id: ^id)
223+
|> Repo.one()
224+
else
225+
Assessment
226+
|> where(id: ^id)
227+
|> where(is_published: true)
228+
|> Repo.one()
229+
end
171230

172231
if assessment do
173232
assessment_with_questions_and_answers(assessment, user, password)
@@ -210,7 +269,7 @@ defmodule Cadet.Assessments do
210269
Returns a list of assessments with all fields and an indicator showing whether it has been attempted
211270
by the supplied user
212271
"""
213-
def all_published_assessments(user = %User{}) do
272+
def all_assessments(user = %User{}) do
214273
assessments =
215274
Query.all_assessments_with_max_xp_and_grade()
216275
|> subquery()
@@ -240,7 +299,7 @@ defmodule Cadet.Assessments do
240299
question_count: q_count.count,
241300
graded_count: a_count.count
242301
})
243-
|> where(is_published: true)
302+
|> filter_published_assessments(user)
244303
|> order_by(:open_at)
245304
|> Repo.all()
246305
|> Enum.map(fn assessment = %Assessment{} ->
@@ -259,6 +318,15 @@ defmodule Cadet.Assessments do
259318
{:ok, assessments}
260319
end
261320

321+
def filter_published_assessments(assessments, user) do
322+
role = user.role
323+
324+
case role do
325+
:student -> where(assessments, is_published: true)
326+
_ -> assessments
327+
end
328+
end
329+
262330
defp build_grading_status(submission_status, a_type, q_count, g_count) do
263331
case a_type do
264332
type when type in [:mission, :sidequest] ->
@@ -283,33 +351,101 @@ defmodule Cadet.Assessments do
283351
@doc """
284352
The main function that inserts or updates assessments from the XML Parser
285353
"""
286-
@spec insert_or_update_assessments_and_questions(map(), [map()]) ::
354+
@spec insert_or_update_assessments_and_questions(map(), [map()], boolean()) ::
287355
{:ok, any()}
288356
| {:error, Ecto.Multi.name(), any(), %{optional(Ecto.Multi.name()) => any()}}
289-
def insert_or_update_assessments_and_questions(assessment_params, questions_params) do
357+
def insert_or_update_assessments_and_questions(
358+
assessment_params,
359+
questions_params,
360+
force_update
361+
) do
290362
assessment_multi =
291363
Multi.insert_or_update(
292364
Multi.new(),
293365
:assessment,
294-
insert_or_update_assessment_changeset(assessment_params)
366+
insert_or_update_assessment_changeset(assessment_params, force_update)
295367
)
296368

297-
questions_params
298-
|> Enum.with_index(1)
299-
|> Enum.reduce(assessment_multi, fn {question_params, index}, multi ->
300-
Multi.run(multi, String.to_atom("question#{index}"), fn _repo,
301-
%{assessment: %Assessment{id: id}} ->
302-
question_params
303-
|> Map.put(:display_order, index)
304-
|> build_question_changeset_for_assessment_id(id)
305-
|> Repo.insert()
369+
if force_update and invalid_force_update(assessment_multi, questions_params) do
370+
{:error, "Question count is different"}
371+
else
372+
questions_params
373+
|> Enum.with_index(1)
374+
|> Enum.reduce(assessment_multi, fn {question_params, index}, multi ->
375+
Multi.run(multi, String.to_atom("question#{index}"), fn _repo,
376+
%{assessment: %Assessment{id: id}} ->
377+
question_exists =
378+
Repo.exists?(
379+
where(Question, [q], q.assessment_id == ^id and q.display_order == ^index)
380+
)
381+
382+
# the !question_exists check allows for force updating of brand new assessments
383+
if !force_update or !question_exists do
384+
question_params
385+
|> Map.put(:display_order, index)
386+
|> build_question_changeset_for_assessment_id(id)
387+
|> Repo.insert()
388+
else
389+
params =
390+
question_params
391+
|> Map.put_new(:max_xp, 0)
392+
|> Map.put(:display_order, index)
393+
394+
%{id: question_id, type: type} =
395+
Question
396+
|> where([q], q.display_order == ^index and q.assessment_id == ^id)
397+
|> Repo.one()
398+
399+
if question_params.type != Atom.to_string(type) do
400+
{:error,
401+
create_invalid_changeset_with_error(
402+
:question,
403+
"Question types should remain the same"
404+
)}
405+
else
406+
changeset =
407+
Question.changeset(%Question{assessment_id: id, id: question_id}, params)
408+
409+
Repo.update(changeset)
410+
end
411+
end
412+
end)
306413
end)
307-
end)
308-
|> Repo.transaction()
414+
|> Repo.transaction()
415+
end
309416
end
310417

311-
@spec insert_or_update_assessment_changeset(map()) :: Ecto.Changeset.t()
312-
defp insert_or_update_assessment_changeset(params = %{number: number}) do
418+
# Function that checks if the force update is invalid. The force update is only invalid
419+
# if the new question count is different from the old question count.
420+
defp invalid_force_update(assessment_multi, questions_params) do
421+
assessment_id =
422+
(assessment_multi.operations
423+
|> List.first()
424+
|> elem(1)
425+
|> elem(1)).data.id
426+
427+
if assessment_id do
428+
open_date = Repo.get(Assessment, assessment_id).open_at
429+
# check if assessment is already opened
430+
if Timex.after?(open_date, Timex.now()) do
431+
false
432+
else
433+
existing_questions_count =
434+
Question
435+
|> where([q], q.assessment_id == ^assessment_id)
436+
|> Repo.all()
437+
|> Enum.count()
438+
439+
new_questions_count = Enum.count(questions_params)
440+
existing_questions_count != new_questions_count
441+
end
442+
else
443+
false
444+
end
445+
end
446+
447+
@spec insert_or_update_assessment_changeset(map(), boolean()) :: Ecto.Changeset.t()
448+
defp insert_or_update_assessment_changeset(params = %{number: number}, force_update) do
313449
Assessment
314450
|> where(number: ^number)
315451
|> Repo.one()
@@ -318,18 +454,30 @@ defmodule Cadet.Assessments do
318454
Assessment.changeset(%Assessment{}, params)
319455

320456
assessment ->
321-
if Timex.after?(assessment.open_at, Timex.now()) do
322-
# Delete all existing questions
323-
%{id: assessment_id} = assessment
457+
cond do
458+
Timex.after?(assessment.open_at, Timex.now()) ->
459+
# Delete all existing questions
460+
%{id: assessment_id} = assessment
324461

325-
Question
326-
|> where(assessment_id: ^assessment_id)
327-
|> Repo.delete_all()
462+
Question
463+
|> where(assessment_id: ^assessment_id)
464+
|> Repo.delete_all()
328465

329-
Assessment.changeset(assessment, params)
330-
else
331-
# if the assessment is already open, don't mess with it
332-
create_invalid_changeset_with_error(:assessment, "is already open")
466+
Assessment.changeset(assessment, params)
467+
468+
force_update ->
469+
# Maintain the same open/close date when force updating an assessment
470+
new_params =
471+
params
472+
|> Map.delete(:open_at)
473+
|> Map.delete(:close_at)
474+
|> Map.delete(:is_published)
475+
476+
Assessment.changeset(assessment, new_params)
477+
478+
true ->
479+
# if the assessment is already open, don't mess with it
480+
create_invalid_changeset_with_error(:assessment, "is already open")
333481
end
334482
end
335483
end

0 commit comments

Comments
 (0)