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
38 changes: 27 additions & 11 deletions lib/cadet/assessments/answer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule Cadet.Assessments.Answer do

schema "answers" do
field(:grade, :integer, default: 0)
field(:xp, :integer, default: 0)
field(:xp_adjustment, :integer, default: 0)
field(:autograding_status, AutogradingStatus, default: :none)
field(:autograding_errors, {:array, :map}, default: [])
field(:answer, :map)
Expand All @@ -24,50 +26,64 @@ defmodule Cadet.Assessments.Answer do
end

@required_fields ~w(answer submission_id question_id type)a
@optional_fields ~w(grade comment adjustment)a
@optional_fields ~w(xp xp_adjustment grade comment adjustment)a

def changeset(answer, params) do
answer
|> cast(params, @required_fields ++ @optional_fields)
|> add_belongs_to_id_from_model([:submission, :question], params)
|> add_question_type_from_model(params)
|> validate_required(@required_fields)
|> validate_number(:grade, greater_than_or_equal_to: 0.0)
|> foreign_key_constraint(:submission_id)
|> foreign_key_constraint(:question_id)
|> validate_answer_content()
|> validate_xp_grade_adjustment_total()
end

@spec grading_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def grading_changeset(answer, params) do
answer
|> cast(params, ~w(adjustment comment)a)
|> validate_grade_adjustment_total()
|> cast(params, ~w(xp_adjustment adjustment comment)a)
|> validate_xp_grade_adjustment_total()
end

@spec autograding_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def autograding_changeset(answer, params) do
cast(answer, params, ~w(grade adjustment autograding_status autograding_errors)a)
answer
|> cast(params, ~w(grade adjustment xp autograding_status autograding_errors)a)
|> validate_xp_grade_adjustment_total()
end

@spec validate_grade_adjustment_total(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_grade_adjustment_total(changeset) do
@spec validate_xp_grade_adjustment_total(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_xp_grade_adjustment_total(changeset) do
answer = apply_changes(changeset)

total = answer.grade + answer.adjustment
total_grade = answer.grade + answer.adjustment

total_xp = answer.xp + answer.xp_adjustment

with {:question_id, question_id} when is_ecto_id(question_id) <-
{:question_id, answer.question_id},
question <- Repo.get(Question, question_id),
{:total, true} <- {:total, total >= 0 and total <= question.max_grade} do
{:question, %{max_grade: max_grade, max_xp: max_xp}} <-
{:question, Repo.get(Question, question_id)},
{:total_grade, true} <- {:total_grade, total_grade >= 0 and total_grade <= max_grade},
{:total_xp, true} <- {:total_xp, total_xp >= 0 and total_xp <= max_xp} do
changeset
else
{:question_id, _} ->
add_error(changeset, :question_id, "is required")

{:total, false} ->
{:question, _} ->
add_error(changeset, :question_id, "refers to non-existent question")

{:total_grade, false} ->
add_error(changeset, :adjustment, "must make total be between 0 and question.max_grade")

{:total_xp, false} ->
add_error(changeset, :xp_adjustment, "must make total be between 0 and question.max_xp")
end
|> validate_number(:grade, greater_than_or_equal_to: 0)
|> validate_number(:xp, greater_than_or_equal_to: 0)
end

defp add_question_type_from_model(changeset, params) do
Expand Down
3 changes: 3 additions & 0 deletions lib/cadet/assessments/assessment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ defmodule Cadet.Assessments.Assessment do

schema "assessments" do
field(:max_grade, :integer, virtual: true)
field(:grade, :integer, virtual: true)
field(:max_xp, :integer, virtual: true)
field(:xp, :integer, virtual: true)
field(:user_status, SubmissionStatus, virtual: true)
field(:title, :string)
field(:is_published, :boolean, default: false)
Expand Down
121 changes: 90 additions & 31 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,62 @@ defmodule Cadet.Assessments do
alias Cadet.Autograder.GradingJob
alias Ecto.Multi

@xp_early_submission_max_bonus 100
@xp_bonus_assessment_type ~w(mission sidequest)a
@submit_answer_roles ~w(student)a
@grading_roles ~w(staff)a
@open_all_assessment_roles ~w(staff admin)a

@spec user_max_grade(%User{}) :: integer()
def user_max_grade(%User{id: user_id}) when is_ecto_id(user_id) do
max_grade =
@spec user_total_xp(%User{}) :: integer()
def user_total_xp(%User{id: user_id}) when is_ecto_id(user_id) do
total_xp_bonus =
Submission
|> where(status: ^:submitted)
|> where(student_id: ^user_id)
|> join(
:inner,
[s],
a in subquery(Query.all_assessments_with_max_grade()),
s.assessment_id == a.id
)
|> select([_, a], sum(a.max_grade))
|> Repo.aggregate(:sum, :xp_bonus)
|> case do
nil -> 0
xp when is_integer(xp) -> xp
end

total_xp =
Query.all_submissions_with_xp()
|> subquery()
|> where(student_id: ^user_id)
|> select([q], fragment("? + ?", sum(q.xp), sum(q.xp_adjustment)))
|> Repo.one()
|> decimal_to_integer()

if max_grade do
Decimal.to_integer(max_grade)
else
0
end
total_xp_bonus + total_xp
end

@spec user_max_grade(%User{}) :: integer()
def user_max_grade(%User{id: user_id}) when is_ecto_id(user_id) do
Submission
|> where(status: ^:submitted)
|> where(student_id: ^user_id)
|> join(
:inner,
[s],
a in subquery(Query.all_assessments_with_max_grade()),
s.assessment_id == a.id
)
|> select([_, a], sum(a.max_grade))
|> Repo.one()
|> decimal_to_integer()
end

def user_total_grade(%User{id: user_id}) do
grade =
Query.all_submissions_with_grade()
|> subquery()
|> where(student_id: ^user_id)
|> select([q], fragment("? + ?", sum(q.grade), sum(q.adjustment)))
|> Repo.one()
Query.all_submissions_with_grade()
|> subquery()
|> where(student_id: ^user_id)
|> select([q], fragment("? + ?", sum(q.grade), sum(q.adjustment)))
|> Repo.one()
|> decimal_to_integer()
end

if grade do
Decimal.to_integer(grade)
defp decimal_to_integer(decimal) do
if Decimal.decimal?(decimal) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something about this tickles me hahahahaha

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙃

Decimal.to_integer(decimal)
else
0
end
Expand Down Expand Up @@ -141,10 +161,20 @@ defmodule Cadet.Assessments do
"""
def all_published_assessments(user = %User{}) do
assessments =
Query.all_assessments_with_max_grade()
Query.all_assessments_with_max_xp_and_grade()
|> subquery()
|> join(:left, [a], s in Submission, a.id == s.assessment_id and s.student_id == ^user.id)
|> select([a, s], %{a | user_status: s.status})
|> join(
:left,
[a],
s in subquery(Query.all_submissions_with_xp_and_grade()),
a.id == s.assessment_id and s.student_id == ^user.id
)
|> select([a, s], %{
a
| xp: fragment("? + ? + ?", s.xp, s.xp_adjustment, s.xp_bonus),
grade: fragment("? + ?", s.grade, s.adjustment),
user_status: s.status
})
|> where(is_published: true)
|> order_by(:open_at)
|> Repo.all()
Expand Down Expand Up @@ -315,8 +345,7 @@ defmodule Cadet.Assessments do
with {:submission_found?, true} <- {:submission_found?, is_map(submission)},
{:is_open?, true} <- is_open?(submission.assessment),
{:status, :attempted} <- {:status, submission.status},
{:ok, updated_submission} <-
submission |> Submission.changeset(%{status: :submitted}) |> Repo.update() do
{:ok, updated_submission} <- update_submission_status_and_xp_bonus(submission) do
GradingJob.force_grade_individual_submission(updated_submission)

{:ok, nil}
Expand All @@ -341,6 +370,29 @@ defmodule Cadet.Assessments do
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
assessment = submission.assessment

xp_bonus =
cond do
assessment.type not in @xp_bonus_assessment_type ->
0

Timex.before?(Timex.now(), Timex.shift(assessment.open_at, hours: 48)) ->
@xp_early_submission_max_bonus

true ->
deduction = Timex.diff(Timex.now(), assessment.open_at, :hours) - 48
Enum.max([0, @xp_early_submission_max_bonus - deduction])
end

submission
|> Submission.changeset(%{status: :submitted, xp_bonus: xp_bonus})
|> Repo.update()
end

def update_submission_status(submission = %Submission{}, assessment = %Assessment{}) do
model_assoc_count = fn model, assoc, id ->
model
Expand Down Expand Up @@ -375,18 +427,25 @@ defmodule Cadet.Assessments do

submissions =
Submission
|> join(:inner, [s], x in subquery(Query.submissions_grade()), s.id == x.submission_id)
|> join(
:inner,
[s],
x in subquery(Query.submissions_xp_and_grade()),
s.id == x.submission_id
)
|> join(:inner, [s], st in subquery(students), s.student_id == st.id)
|> join(
:inner,
[s],
a in subquery(Query.all_assessments_with_max_grade()),
a in subquery(Query.all_assessments_with_max_xp_and_grade()),
s.assessment_id == a.id
)
|> select([s, x, st, a], %Submission{
s
| grade: x.grade,
adjustment: x.adjustment,
xp: x.xp,
xp_adjustment: x.xp_adjustment,
student: st,
assessment: a
})
Expand Down
74 changes: 74 additions & 0 deletions lib/cadet/assessments/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@ defmodule Cadet.Assessments.Query do

alias Cadet.Assessments.{Answer, Assessment, Question, Submission}

@doc """
Returns a query with the following bindings:
[submissions_with_xp_and_grade, answers]
"""
@spec all_submissions_with_xp_and_grade :: Ecto.Query.t()
def all_submissions_with_xp_and_grade do
Submission
|> join(:inner, [s], q in subquery(submissions_xp_and_grade()), s.id == q.submission_id)
|> select([s, q], %Submission{
s
| xp: q.xp,
xp_adjustment: q.xp_adjustment,
grade: q.grade,
adjustment: q.adjustment
})
end

@doc """
Returns a query with the following bindings:
[submissions_with_xp, answers]
"""
@spec all_submissions_with_xp :: Ecto.Query.t()
def all_submissions_with_xp do
Submission
|> join(:inner, [s], q in subquery(submissions_xp()), s.id == q.submission_id)
|> select([s, q], %Submission{s | xp: q.xp, xp_adjustment: q.xp_adjustment})
end

@doc """
Returns a query with the following bindings:
[submissions_with_grade, answers]
Expand All @@ -17,6 +45,17 @@ defmodule Cadet.Assessments.Query do
|> select([s, q], %Submission{s | grade: q.grade, adjustment: q.adjustment})
end

@doc """
Returns a query with the following bindings:
[assessments_with_xp_and_grade, questions]
"""
@spec all_assessments_with_max_xp_and_grade :: Ecto.Query.t()
def all_assessments_with_max_xp_and_grade do
Assessment
|> join(:inner, [a], q in subquery(assessments_max_xp_and_grade()), a.id == q.assessment_id)
|> select([a, q], %Assessment{a | max_grade: q.max_grade, max_xp: q.max_xp})
end

@doc """
Returns a query with the following bindings:
[assessments_with_grade, questions]
Expand All @@ -28,6 +67,19 @@ defmodule Cadet.Assessments.Query do
|> select([a, q], %Assessment{a | max_grade: q.max_grade})
end

@spec submissions_xp_and_grade :: Ecto.Query.t()
def submissions_xp_and_grade do
Answer
|> group_by(:submission_id)
|> select([a], %{
submission_id: a.submission_id,
grade: sum(a.grade),
adjustment: sum(a.adjustment),
xp: sum(a.xp),
xp_adjustment: sum(a.xp_adjustment)
})
end

@spec submissions_grade :: Ecto.Query.t()
def submissions_grade do
Answer
Expand All @@ -39,10 +91,32 @@ defmodule Cadet.Assessments.Query do
})
end

@spec submissions_xp :: Ecto.Query.t()
def submissions_xp do
Answer
|> group_by(:submission_id)
|> select([a], %{
submission_id: a.submission_id,
xp: sum(a.xp),
xp_adjustment: sum(a.xp_adjustment)
})
end

@spec assessments_max_grade :: Ecto.Query.t()
def assessments_max_grade do
Question
|> group_by(:assessment_id)
|> select([q], %{assessment_id: q.assessment_id, max_grade: sum(q.max_grade)})
end

@spec assessments_max_xp_and_grade :: Ecto.Query.t()
def assessments_max_xp_and_grade do
Question
|> group_by(:assessment_id)
|> select([q], %{
assessment_id: q.assessment_id,
max_grade: sum(q.max_grade),
max_xp: sum(q.max_xp)
})
end
end
3 changes: 2 additions & 1 deletion lib/cadet/assessments/question.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule Cadet.Assessments.Question do
field(:question, :map)
field(:type, QuestionType)
field(:max_grade, :integer)
field(:max_xp, :integer)
field(:answer, :map, virtual: true)
embeds_one(:library, Library)
embeds_one(:grading_library, Library)
Expand All @@ -21,7 +22,7 @@ defmodule Cadet.Assessments.Question do
end

@required_fields ~w(question type assessment_id)a
@optional_fields ~w(display_order max_grade)a
@optional_fields ~w(display_order max_grade max_xp)a
@required_embeds ~w(library)a

def changeset(question, params) do
Expand Down
Loading