Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/cadet/accounts/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule Cadet.Accounts.Query do

alias Cadet.Accounts.{Authorization, User}
alias Cadet.Course.Group
alias Cadet.Repo

def user_nusnet_ids(user_id) do
Authorization
Expand All @@ -26,6 +27,17 @@ defmodule Cadet.Accounts.Query do
|> where([_, g], g.leader_id == ^id)
end

def avenger_of?(avenger, student_id) do
students = students_of(avenger)

students
|> Repo.get(student_id)
|> case do
nil -> false
_ -> true
end
end

defp nusnet_ids(query) do
query |> where([a], a.provider == "nusnet_id")
end
Expand Down
5 changes: 4 additions & 1 deletion lib/cadet/assessments/answer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ defmodule Cadet.Assessments.Answer do
@spec grading_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def grading_changeset(answer, params) do
answer
|> cast(params, ~w(grader_id xp_adjustment adjustment comment)a)
|> cast(
params,
~w(grader_id xp xp_adjustment grade adjustment autograding_results autograding_status comment)a
)
|> add_belongs_to_id_from_model(:grader, params)
|> foreign_key_constraint(:grader_id)
|> validate_xp_grade_adjustment_total()
Expand Down
95 changes: 91 additions & 4 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Cadet.Assessments do
@xp_early_submission_max_bonus 100
@xp_bonus_assessment_type ~w(mission sidequest)a
@submit_answer_roles ~w(student)a
@unsubmit_assessment_role ~w(staff)a
@grading_roles ~w()a
@see_all_submissions_roles ~w(staff admin)a
@open_all_assessment_roles ~w(staff admin)a
Expand Down Expand Up @@ -401,6 +402,90 @@ defmodule Cadet.Assessments do
end
end

def unsubmit_submission(submission_id, avenger = %User{id: staff_id, role: role})
when is_ecto_id(submission_id) do
if role in @unsubmit_assessment_role do
submission =
Submission
|> join(:inner, [s], a in assoc(s, :assessment))
|> preload([_, a], assessment: a)
|> Repo.get(submission_id)

with {:submission_found?, true} <- {:submission_found?, is_map(submission)},
{:is_open?, true} <- is_open?(submission.assessment),
{:status, :submitted} <- {:status, submission.status},
{:avenger_of?, true} <-
{:avenger_of?, Cadet.Accounts.Query.avenger_of?(avenger, submission.student_id)} do
Multi.new()
|> Multi.run(
:rollback_submission,
fn _ ->
submission
|> Submission.changeset(%{
status: :attempted,
xp_bonus: 0,
unsubmitted_by_id: staff_id,
unsubmitted_at: Timex.now()
})
|> Repo.update()
end
)
|> Multi.run(:rollback_answers, fn _ ->
Answer
|> join(:inner, [a], q in assoc(a, :question))
|> join(:inner, [a, _], s in assoc(a, :submission))
|> preload([_, q, s], question: q, submission: s)
|> where(submission_id: ^submission.id)
|> Repo.all()
|> Enum.reduce_while({:ok, nil}, fn answer, acc ->
case acc do
{:error, _} ->
{:halt, acc}

{:ok, _} ->
{:cont,
answer
|> Answer.grading_changeset(%{
grade: 0,
adjustment: 0,
xp: 0,
xp_adjustment: 0,
autograding_status: :none,
autograding_results: [],
comment: nil,
grader_id: nil
})
|> Repo.update()}
end
end)
end)
|> Repo.transaction()

{:ok, nil}
else
{:submission_found?, false} ->
{:error, {:not_found, "Submission not found"}}

{:is_open?, false} ->
{:error, {:forbidden, "Assessment not open"}}

{:status, :attempting} ->
{:error, {:bad_request, "Some questions have not been attempted"}}

{:status, :attempted} ->
{:error, {:bad_request, "Assessment has not been submitted"}}

{:avenger_of?, false} ->
{:error, {:forbidden, "Only Avenger of student is permitted to unsubmit"}}

_ ->
{:error, {:internal_server_error, "Please try again later."}}
end
else
{:error, {:forbidden, "User is not permitted to unsubmit questions"}}
end
end

@spec update_submission_status_and_xp_bonus(%Submission{}) ::
{:ok, %Submission{}} | {:error, Ecto.Changeset.t()}
defp update_submission_status_and_xp_bonus(submission = %Submission{}) do
Expand Down Expand Up @@ -471,23 +556,25 @@ defmodule Cadet.Assessments do
x in subquery(Query.submissions_xp_and_grade()),
s.id == x.submission_id
)
|> join(:inner, [s], st in assoc(s, :student))
|> join(:inner, [s, _], st in assoc(s, :student))
|> join(:inner, [_, _, st], g in assoc(st, :group))
|> join(:left, [s, _, _, g], u in assoc(s, :unsubmitted_by))
|> join(
:inner,
[s],
[s, _, _, _, _],
a in subquery(Query.all_assessments_with_max_xp_and_grade()),
s.assessment_id == a.id
)
|> select([s, x, st, g, a], %Submission{
|> select([s, x, st, g, u, a], %Submission{
s
| grade: x.grade,
adjustment: x.adjustment,
xp: x.xp,
xp_adjustment: x.xp_adjustment,
student: st,
assessment: a,
group_name: g.name
group_name: g.name,
unsubmitted_by: u
})

cond do
Expand Down
7 changes: 5 additions & 2 deletions lib/cadet/assessments/submission.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ defmodule Cadet.Assessments.Submission do
field(:xp_bonus, :integer, default: 0)
field(:group_name, :string, virtual: true)
field(:status, SubmissionStatus, default: :attempting)
field(:unsubmitted_at, Timex.Ecto.DateTime)

belongs_to(:assessment, Assessment)
belongs_to(:student, User)
belongs_to(:unsubmitted_by, User)
has_many(:answers, Answer)

timestamps()
end

@required_fields ~w(student_id assessment_id status)a
@optional_fields ~w(xp_bonus)a
@optional_fields ~w(xp_bonus unsubmitted_by_id unsubmitted_at)a
@xp_early_submission_max_bonus 100

def changeset(submission, params) do
Expand All @@ -33,9 +35,10 @@ defmodule Cadet.Assessments.Submission do
greater_than_or_equal_to: 0,
less_than_or_equal_to: @xp_early_submission_max_bonus
)
|> add_belongs_to_id_from_model([:student, :assessment], params)
|> add_belongs_to_id_from_model([:student, :assessment, :unsubmitted_by], params)
|> validate_required(@required_fields)
|> foreign_key_constraint(:student_id)
|> foreign_key_constraint(:assessment_id)
|> foreign_key_constraint(:unsubmitted_by_id)
end
end
38 changes: 38 additions & 0 deletions lib/cadet_web/controllers/grading_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,26 @@ defmodule CadetWeb.GradingController do
|> text("Missing parameter")
end

def unsubmit(conn, %{"submissionid" => submission_id}) when is_ecto_id(submission_id) do
user = conn.assigns[:current_user]

case Assessments.unsubmit_submission(submission_id, user) do
{:ok, nil} ->
text(conn, "OK")

{:error, {status, message}} ->
conn
|> put_status(status)
|> text(message)
end
end

def unsubmit(conn, _params) do
conn
|> put_status(:bad_request)
|> text("Missing parameter")
end

swagger_path :index do
get("/grading")

Expand All @@ -99,6 +119,21 @@ defmodule CadetWeb.GradingController do
response(401, "Unauthorised")
end

swagger_path :unsubmit do
post("/grading/{submissionId}/unsubmit")
summary("Unsubmit submission. Can only be done by the Avenger of a student.")
security([%{JWT: []}])

parameters do
submissionId(:path, :integer, "submission id", required: true)
end

response(200, "OK")
response(400, "Invalid parameters")
response(403, "User not permitted to unsubmit assessment or assessment not open")
response(404, "Submission not found")
end

swagger_path :show do
get("/grading/{submissionId}")

Expand Down Expand Up @@ -165,6 +200,9 @@ defmodule CadetWeb.GradingController do

assessment(Schema.ref(:AssessmentInfo))
student(Schema.ref(:StudentInfo))

unsubmittedBy(Schema.ref(:GraderInfo))
unsubmittedAt(:string, "Last unsubmitted at", format: "date-time", required: false)
end
end,
AssessmentInfo:
Expand Down
1 change: 1 addition & 0 deletions lib/cadet_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule CadetWeb.Router do

get("/grading", GradingController, :index)
get("/grading/:submissionid", GradingController, :show)
post("/grading/:submissionid/unsubmit", GradingController, :unsubmit)
post("/grading/:submissionid/:questionid", GradingController, :update)

get("/user", UserController, :index)
Expand Down
4 changes: 3 additions & 1 deletion lib/cadet_web/views/grading_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ defmodule CadetWeb.GradingView do
coverImage: :cover_picture
}),
groupName: :group_name,
status: :status
status: :status,
unsubmittedBy: &unsubmitted_by_builder(&1.unsubmitted_by),
unsubmittedAt: &format_datetime(&1.unsubmitted_at)
})
end

Expand Down
20 changes: 14 additions & 6 deletions lib/cadet_web/views/view_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@ defmodule CadetWeb.ViewHelpers do
Helper functions shared throughout views
"""

defp build_staff(user) do
transform_map_for_view(user, [:name, :id])
end

def unsubmitted_by_builder(nil), do: nil

def unsubmitted_by_builder(staff) do
build_staff(staff)
end

def grader_builder(nil), do: nil

def grader_builder(_) do
fn %{grader: grader} ->
transform_map_for_view(grader, [:name, :id])
end
fn %{grader: grader} -> build_staff(grader) end
end

def graded_at_builder(nil), do: nil

def graded_at_builder(_) do
fn %{updated_at: updated_at} ->
format_datetime(updated_at)
end
fn %{updated_at: updated_at} -> format_datetime(updated_at) end
end

def format_datetime(nil), do: nil

def format_datetime(datetime = %DateTime{}) do
datetime
|> DateTime.truncate(:millisecond)
Expand Down
10 changes: 10 additions & 0 deletions priv/repo/migrations/20190530065753_add_unsubmit_fields.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Cadet.Repo.Migrations.AddUnsubmitFields do
use Ecto.Migration

def change do
alter table(:submissions) do
add(:unsubmitted_by_id, references(:users), null: true)
add(:unsubmitted_at, :timestamp, null: true)
end
end
end
Loading