Skip to content

Commit 77dc886

Browse files
bnjmnt4nqreoct
andauthored
Update /assessments endpoint (#729)
* `/assessments`: split out separate `/admin` endpoint * Consistently use `/assessments/:assessmentid` * Merge `/admin/assessments/:assessmentid/{update,publish}` endpoints * Quick swagger docs fix for `AdminAssessmentsController` * Format code via `mix format` * Formatting fix * `/assessments/{assessmentid}/submit`: shift role validation to controller * Add more guards * `/assessments/question/{questionid}/submit`: shift role validation to AnswerController * clean up some docs * Modify `AnswerController#submit` path * Assessments: remove unused definitions * Remove leftover unused route * Split `AssesmentsController#unlock` out of `AssessmentsController#show` `AssesmentsController#unlock` unlocks password-protected assessments and creates a Submission for them, following which the assessments do not require requests with the password. * Fix formatting issues * `/assessments`: split out separate `/admin` endpoint * Consistently use `/assessments/:assessmentid` * Merge `/admin/assessments/:assessmentid/{update,publish}` endpoints * Quick swagger docs fix for `AdminAssessmentsController` * Format code via `mix format` * `/assessments/{assessmentid}/submit`: shift role validation to controller * Add more guards * `/assessments/question/{questionid}/submit`: shift role validation to AnswerController * clean up some docs * Modify `AnswerController#submit` path * Assessments: remove unused definitions * Remove leftover unused route * Split `AssesmentsController#unlock` out of `AssessmentsController#show` `AssesmentsController#unlock` unlocks password-protected assessments and creates a Submission for them, following which the assessments do not require requests with the password. * Fix formatting issues * formatting * Update per PR feedback * Remove unused variable Co-authored-by: qreoct <9080974+qreoct@users.noreply.github.com>
1 parent d284fa6 commit 77dc886

File tree

8 files changed

+703
-645
lines changed

8 files changed

+703
-645
lines changed

lib/cadet/assessments/assessments.ex

Lines changed: 25 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,12 @@ defmodule Cadet.Assessments do
1717

1818
@xp_early_submission_max_bonus 100
1919
@xp_bonus_assessment_type ~w(mission sidequest)
20-
# @submit_answer_roles ~w(student staff admin)a
21-
# @change_dates_assessment_role ~w(staff admin)a
22-
# @delete_assessment_role ~w(staff admin)a
23-
# @publish_assessment_role ~w(staff admin)a
24-
# @unsubmit_assessment_role ~w(staff admin)a
25-
# @see_all_submissions_roles ~w(staff admin)a
26-
# @group_grading_summary_roles @see_all_submissions_roles
2720
@open_all_assessment_roles ~w(staff admin)a
2821

2922
# These roles can save and finalise answers for closed assessments and
3023
# submitted answers
3124
@bypass_closed_roles ~w(staff admin)a
3225

33-
def change_dates_assessment(id, close_at, open_at) do
34-
if Timex.before?(close_at, open_at) do
35-
{:error, {:bad_request, "New end date should occur after new opening date"}}
36-
else
37-
update_assessment(id, %{close_at: close_at, open_at: open_at})
38-
end
39-
end
40-
41-
def toggle_publish_assessment(id) do
42-
assessment = Repo.get(Assessment, id)
43-
update_assessment(id, %{is_published: !assessment.is_published})
44-
end
45-
4626
def delete_assessment(id) do
4727
assessment = Repo.get(Assessment, id)
4828

@@ -487,7 +467,7 @@ defmodule Cadet.Assessments do
487467
)
488468
end
489469

490-
def publish_assessment(id) do
470+
def publish_assessment(id) when is_ecto_id(id) do
491471
update_assessment(id, %{is_published: true})
492472
end
493473

@@ -511,6 +491,14 @@ defmodule Cadet.Assessments do
511491
end
512492
end
513493

494+
def get_question(id) when is_ecto_id(id) do
495+
Question
496+
|> where(id: ^id)
497+
|> join(:inner, [q], assessment in assoc(q, :assessment))
498+
|> preload([_, a], assessment: a)
499+
|> Repo.one()
500+
end
501+
514502
def delete_question(id) when is_ecto_id(id) do
515503
question = Repo.get(Question, id)
516504
Repo.delete(question)
@@ -525,53 +513,32 @@ defmodule Cadet.Assessments do
525513
`{:bad_request, "Missing or invalid parameter(s)"}`
526514
527515
"""
528-
def answer_question(id, user = %User{role: role}, raw_answer) when is_ecto_id(id) do
529-
# if role in @submit_answer_roles do
530-
question =
531-
Question
532-
|> where(id: ^id)
533-
|> join(:inner, [q], assessment in assoc(q, :assessment))
534-
|> preload([_, a], assessment: a)
535-
|> Repo.one()
536-
537-
bypass = role in @bypass_closed_roles
538-
539-
with {:question_found?, true} <- {:question_found?, is_map(question)},
540-
{:is_open?, true} <- {:is_open?, bypass or is_open?(question.assessment)},
541-
{:ok, submission} <- find_or_create_submission(user, question.assessment),
542-
{:status, true} <- {:status, bypass or submission.status != :submitted},
516+
def answer_question(question = %Question{}, user = %User{}, raw_answer, force_submit) do
517+
with {:ok, submission} <- find_or_create_submission(user, question.assessment),
518+
{:status, true} <- {:status, force_submit or submission.status != :submitted},
543519
{:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer) do
544520
update_submission_status(submission, question.assessment)
545521

546522
{:ok, nil}
547523
else
548-
{:question_found?, false} -> {:error, {:not_found, "Question not found"}}
549-
{:is_open?, false} -> {:error, {:forbidden, "Assessment not open"}}
550524
{:status, _} -> {:error, {:forbidden, "Assessment submission already finalised"}}
551525
{:error, :race_condition} -> {:error, {:internal_server_error, "Please try again later."}}
552526
_ -> {:error, {:bad_request, "Missing or invalid parameter(s)"}}
553527
end
554-
555-
# else
556-
# {:error, {:forbidden, "User is not permitted to answer questions"}}
557-
# end
558528
end
559529

560-
def finalise_submission(assessment_id, %User{id: user_id, role: role})
530+
def get_submission(assessment_id, %User{id: user_id})
561531
when is_ecto_id(assessment_id) do
562-
# if role in @submit_answer_roles do
563-
submission =
564-
Submission
565-
|> where(assessment_id: ^assessment_id)
566-
|> where(student_id: ^user_id)
567-
|> join(:inner, [s], a in assoc(s, :assessment))
568-
|> preload([_, a], assessment: a)
569-
|> Repo.one()
532+
Submission
533+
|> where(assessment_id: ^assessment_id)
534+
|> where(student_id: ^user_id)
535+
|> join(:inner, [s], a in assoc(s, :assessment))
536+
|> preload([_, a], assessment: a)
537+
|> Repo.one()
538+
end
570539

571-
with {:submission_found?, true} <- {:submission_found?, is_map(submission)},
572-
{:is_open?, true} <-
573-
{:is_open?, role in @bypass_closed_roles or is_open?(submission.assessment)},
574-
{:status, :attempted} <- {:status, submission.status},
540+
def finalise_submission(submission = %Submission{}) do
541+
with {:status, :attempted} <- {:status, submission.status},
575542
{:ok, updated_submission} <- update_submission_status_and_xp_bonus(submission) do
576543
# TODO: Couple with update_submission_status_and_xp_bonus to ensure notification is sent
577544
Notifications.write_notification_when_student_submits(submission)
@@ -580,12 +547,6 @@ defmodule Cadet.Assessments do
580547

581548
{:ok, nil}
582549
else
583-
{:submission_found?, false} ->
584-
{:error, {:not_found, "Submission not found"}}
585-
586-
{:is_open?, false} ->
587-
{:error, {:forbidden, "Assessment not open"}}
588-
589550
{:status, :attempting} ->
590551
{:error, {:bad_request, "Some questions have not been attempted"}}
591552

@@ -595,10 +556,6 @@ defmodule Cadet.Assessments do
595556
_ ->
596557
{:error, {:internal_server_error, "Please try again later."}}
597558
end
598-
599-
# else
600-
# {:error, {:forbidden, "User is not permitted to answer questions"}}
601-
# end
602559
end
603560

604561
def unsubmit_submission(submission_id, user = %User{id: user_id, role: role})
@@ -1003,7 +960,7 @@ defmodule Cadet.Assessments do
1003960

1004961
# Checks if an assessment is open and published.
1005962
@spec is_open?(%Assessment{}) :: boolean()
1006-
defp is_open?(%Assessment{open_at: open_at, close_at: close_at, is_published: is_published}) do
963+
def is_open?(%Assessment{open_at: open_at, close_at: close_at, is_published: is_published}) do
1007964
Timex.between?(Timex.now(), open_at, close_at) and is_published
1008965
end
1009966

@@ -1016,9 +973,9 @@ defmodule Cadet.Assessments do
1016973
submitted_sidequests: number()
1017974
}
1018975

1019-
@spec get_group_grading_summary() ::
976+
@spec get_group_grading_summary ::
1020977
{:ok, [group_summary_entry()]}
1021-
def get_group_grading_summary() do
978+
def get_group_grading_summary do
1022979
subs =
1023980
Answer
1024981
|> join(:left, [ans], s in Submission, on: s.id == ans.submission_id)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
defmodule CadetWeb.AdminAssessmentsController do
2+
use CadetWeb, :controller
3+
4+
use PhoenixSwagger
5+
6+
alias Cadet.Assessments
7+
import Cadet.Updater.XMLParser, only: [parse_xml: 2]
8+
9+
def create(conn, %{"assessment" => assessment, "forceUpdate" => force_update}) do
10+
file =
11+
assessment["file"].path
12+
|> File.read!()
13+
14+
result =
15+
case force_update do
16+
"true" -> parse_xml(file, true)
17+
"false" -> parse_xml(file, false)
18+
end
19+
20+
case result do
21+
:ok ->
22+
if force_update == "true" do
23+
text(conn, "Force update OK")
24+
else
25+
text(conn, "OK")
26+
end
27+
28+
{:ok, warning_message} ->
29+
text(conn, warning_message)
30+
31+
{:error, {status, message}} ->
32+
conn
33+
|> put_status(status)
34+
|> text(message)
35+
end
36+
end
37+
38+
def delete(conn, %{"assessmentid" => assessment_id}) do
39+
result = Assessments.delete_assessment(assessment_id)
40+
41+
case result do
42+
{:ok, _nil} ->
43+
text(conn, "OK")
44+
45+
{:error, {status, message}} ->
46+
conn
47+
|> put_status(status)
48+
|> text(message)
49+
end
50+
end
51+
52+
def update(conn, params = %{"assessmentid" => assessment_id}) when is_ecto_id(assessment_id) do
53+
open_at = params |> Map.get("openAt")
54+
close_at = params |> Map.get("closeAt")
55+
is_published = params |> Map.get("isPublished")
56+
57+
updated_assessment =
58+
if is_published == nil do
59+
%{}
60+
else
61+
%{:is_published => is_published}
62+
end
63+
64+
with {:ok, assessment} <- check_dates(open_at, close_at, updated_assessment),
65+
{:ok, _nil} <- Assessments.update_assessment(assessment_id, assessment) do
66+
text(conn, "OK")
67+
else
68+
{:error, {status, message}} ->
69+
conn
70+
|> put_status(status)
71+
|> text(message)
72+
end
73+
end
74+
75+
defp check_dates(open_at, close_at, assessment) do
76+
if open_at == nil and close_at == nil do
77+
{:ok, assessment}
78+
else
79+
formatted_open_date = elem(DateTime.from_iso8601(open_at), 1)
80+
formatted_close_date = elem(DateTime.from_iso8601(close_at), 1)
81+
82+
if Timex.before?(formatted_close_date, formatted_open_date) do
83+
{:error, {:bad_request, "New end date should occur after new opening date"}}
84+
else
85+
assessment = Map.put(assessment, :open_at, formatted_open_date)
86+
assessment = Map.put(assessment, :close_at, formatted_close_date)
87+
88+
{:ok, assessment}
89+
end
90+
end
91+
end
92+
93+
swagger_path :create do
94+
post("/admin/assessments")
95+
96+
summary("Creates a new assessment or updates an existing assessment.")
97+
98+
security([%{JWT: []}])
99+
100+
consumes("multipart/form-data")
101+
102+
parameters do
103+
assessment(:body, :file, "Assessment to create or update", required: true)
104+
forceUpdate(:body, :boolean, "Force update", required: true)
105+
end
106+
107+
response(200, "OK")
108+
response(400, "XML parse error")
109+
response(403, "Forbidden")
110+
end
111+
112+
swagger_path :delete do
113+
PhoenixSwagger.Path.delete("/admin/assessments/{assessmentid}")
114+
115+
summary("Deletes an assessment.")
116+
117+
security([%{JWT: []}])
118+
119+
parameters do
120+
assessmentId(:path, :integer, "Assessment ID", required: true)
121+
end
122+
123+
response(200, "OK")
124+
response(403, "Forbidden")
125+
end
126+
127+
swagger_path :update do
128+
post("/admin/assessments/{assessmentid}")
129+
130+
summary("Updates an assessment.")
131+
132+
security([%{JWT: []}])
133+
134+
consumes("application/json")
135+
136+
parameters do
137+
assessmentId(:path, :integer, "Assessment ID", required: true)
138+
closeAt(:body, :string, "Open date", required: false)
139+
openAt(:body, :string, "Close date", required: false)
140+
isPublished(:body, :boolean, "Whether the assessment is published", required: false)
141+
end
142+
143+
response(200, "OK")
144+
response(401, "Assessment is already opened")
145+
response(403, "Forbidden")
146+
end
147+
end

lib/cadet_web/controllers/answer_controller.ex

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,31 @@ defmodule CadetWeb.AnswerController do
55

66
alias Cadet.Assessments
77

8+
# These roles can save and finalise answers for closed assessments and
9+
# submitted answers
10+
@bypass_closed_roles ~w(staff admin)a
11+
812
def submit(conn, %{"questionid" => question_id, "answer" => answer})
913
when is_ecto_id(question_id) do
10-
case Assessments.answer_question(question_id, conn.assigns.current_user, answer) do
11-
{:ok, _nil} ->
12-
text(conn, "OK")
14+
user = conn.assigns[:current_user]
15+
can_bypass? = user.role in @bypass_closed_roles
16+
17+
with {:question, question} when not is_nil(question) <-
18+
{:question, Assessments.get_question(question_id)},
19+
{:is_open?, true} <-
20+
{:is_open?, can_bypass? or Assessments.is_open?(question.assessment)},
21+
{:ok, _nil} <- Assessments.answer_question(question, user, answer, can_bypass?) do
22+
text(conn, "OK")
23+
else
24+
{:question, nil} ->
25+
conn
26+
|> put_status(:not_found)
27+
|> text("Question not found")
28+
29+
{:is_open?, false} ->
30+
conn
31+
|> put_status(:forbidden)
32+
|> text("Assessment not open")
1333

1434
{:error, {status, message}} ->
1535
conn
@@ -18,12 +38,12 @@ defmodule CadetWeb.AnswerController do
1838
end
1939
end
2040

21-
def submit(conn, _parms) do
41+
def submit(conn, _params) do
2242
send_resp(conn, :bad_request, "Missing or invalid parameter(s)")
2343
end
2444

2545
swagger_path :submit do
26-
post("/assessments/question/{questionId}/submit")
46+
post("/assessments/question/{questionId}/answer")
2747

2848
summary("Submit an answer to a question.")
2949

@@ -43,7 +63,7 @@ defmodule CadetWeb.AnswerController do
4363
response(200, "OK")
4464
response(400, "Invalid parameters")
4565
response(403, "User not permitted to answer questions or assessment not open")
46-
response(404, "Assessment not found")
66+
response(404, "Question not found")
4767
end
4868

4969
def swagger_definitions do

0 commit comments

Comments
 (0)