Skip to content
This repository has been archived by the owner on Aug 28, 2019. It is now read-only.

Add notifications autograding #8

Open
wants to merge 27 commits into
base: add-notifications
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fe804a0
Merge pull request #1 from source-academy/master
geshuming Mar 17, 2019
52216f7
bumped tzdata from 0.5.19 to 0.5.20
geshuming Mar 28, 2019
1d2a877
Merge branch 'master' into master
geshuming Mar 28, 2019
334c7aa
Merge pull request #3 from source-academy/master
geshuming Apr 11, 2019
ad210df
Merge branch 'master' of https://github.com/source-academy/cadet
geshuming May 27, 2019
52a3660
Merge branch 'master' of https://github.com/geshuming/cadet
geshuming May 27, 2019
59e7fd9
Added basic model for notifications
geshuming Jun 3, 2019
3aa62b5
Change from :submissions to :assessments
geshuming Jun 3, 2019
ae6ecc6
Change from :submissions to :assessments
geshuming Jun 3, 2019
3eb642b
Merge branch 'add-notifications' of https://github.com/geshuming/cade…
geshuming Jun 3, 2019
e761b74
Added MVC for notifications
geshuming Jun 3, 2019
9910aa2
FIxed spelling
geshuming Jun 3, 2019
0e0d20f
Added submission_id and role to Notification
geshuming Jun 4, 2019
9ecdfff
Added factory for Notification
geshuming Jun 4, 2019
f825776
Merge branch 'master' into add-notifications
geshuming Jun 4, 2019
5ee31f4
Refactored notification
geshuming Jun 4, 2019
fd781dc
Merge branch 'add-notifications' into add-notifications-mocks
geshuming Jun 4, 2019
b3864cc
Merge branch 'master' into add-notifications
geshuming Jun 7, 2019
6393129
Implemented fetch for notifications, tried to work on write and ackno…
alcen Jun 11, 2019
fc99f1e
Did mix format
alcen Jun 11, 2019
ab3a3c8
Reworked acknowledge function in notification.ex
alcen Jun 12, 2019
e4aa2a7
Updated functions to work properly
geshuming Jun 12, 2019
73b99d0
Applied credo suggestion
geshuming Jun 12, 2019
1826dbe
Merge pull request #4 from geshuming/add-notifications-functions
geshuming Jun 12, 2019
9e4529b
Added functions for autograding and grading notifications
alcen Jun 17, 2019
381737c
Refactored write_notification_when_autograded and write_notification_…
alcen Jun 18, 2019
c545ab2
Added function call to send notifications for new assessments in asse…
alcen Jun 19, 2019
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
211 changes: 211 additions & 0 deletions lib/cadet/accounts/notification.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
defmodule Cadet.Accounts.Notification do
@moduledoc """
Provides the Notification schema as well as functions to
fetch, write and acknowledge notifications.

Also provides functions that implement notification sending when an
assignment has been autograded or manually graded.
"""
use Cadet, :model

import Ecto.Query

alias Cadet.Repo
alias Cadet.Accounts.{Notification, NotificationType, Role, User}
alias Cadet.Assessments.{Assessment, Question, Submission}
alias Ecto.Multi

schema "notifications" do
field(:type, NotificationType)
field(:read, :boolean)
field(:role, Role, virtual: true)

belongs_to(:user, User)
belongs_to(:assessment, Assessment)
belongs_to(:submission, Submission)
belongs_to(:question, Question)

timestamps()
end

@required_fields ~w(type read role user_id)a
@optional_fields ~w(assessment_id submission_id question_id)a

def changeset(answer, params) do
answer
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> validate_assessment_or_submission()
|> foreign_key_constraint(:user)
|> foreign_key_constraint(:assessment_id)
|> foreign_key_constraint(:submission_id)
|> foreign_key_constraint(:question_id)
end

defp validate_assessment_or_submission(changeset) do
case get_change(changeset, :role) do
:staff ->
validate_required(changeset, [:submission_id])

:student ->
validate_required(changeset, [:assessment_id])

_ ->
add_error(changeset, :role, "Invalid role")
end
end

@doc """
Fetches all unread notifications belonging to a user as an array
"""
@spec fetch(%User{}) :: {:ok, {:array, Notification}}
def fetch(user = %User{}) do
notifications =
Notification
|> where(user_id: ^user.id)
|> where(read: false)
|> Repo.all()

{:ok, notifications}
end

@doc """
Writes a new notification into the database
"""
@spec write(:any) :: Ecto.Changeset.t()
def write(params) do
%Notification{}
|> changeset(params)
|> Repo.insert!()
end

@doc """
Changes a notification's read status from false to true
"""
@spec acknowledge(:integer, %User{}) :: {:ok, Ecto.Schema.t()} | {:error, :any}
def acknowledge(notification_id, user = %User{}) do
notification = Repo.get_by(Notification, id: notification_id, user_id: user.id)

case notification do
nil ->
{:error, {:not_found, "Notification does not exist or does not belong to user"}}

notification ->
notification
|> changeset(%{role: user.role, read: true})
|> Repo.update()
end
end

@doc """
Writes a notification that a student's submission has been
autograded successfully. (for the student)
"""
@spec write_notification_when_autograded(integer() | String.t()) :: Ecto.Changeset.t()
def write_notification_when_autograded(submission_id) do
submission =
Submission
|> Repo.get_by(id: submission_id)

params = %{
type: :autograded,
read: false,
role: :student,
user_id: submission.student_id,
assessment_id: submission.assessment_id,
submission_id: submission_id
}

write(params)
end

@doc """
Writes a notification that a student's submission has been
manually graded successfully by an Avenger or other teaching staff.
(for the student)
"""
@spec write_notification_when_manually_graded(integer() | String.t()) :: Ecto.Changeset.t()
def write_notification_when_manually_graded(submission_id) do
submission =
Submission
|> Repo.get_by(id: submission_id)

params = %{
type: :manually_graded,
read: false,
role: :student,
user_id: submission.student_id,
assessment_id: submission.assessment_id,
submission_id: submission_id
}

write(params)
end

@doc """
Writes a notification to all students that a new assessment is available.
"""
@spec write_notification_for_new_assessment(integer() | String.t()) ::
{:ok, any()}
| {:error, any()}
| {:error, Ecto.Multi.name(), any(), %{required(Ecto.Multi.name()) => any()}}
def write_notification_for_new_assessment(assessment_id) do
assessment =
Assessment
|> Repo.get_by(id: assessment_id)

notification_multi = Multi.new()

if Cadet.Assessments.is_open?(assessment) do
User
|> where(role: ^:student)
|> Repo.all()
|> Enum.each(fn %User{id: student_id} ->
params = %{
type: :new,
read: false,
role: :student,
user_id: student_id,
assessment_id: assessment_id
}

changes =
%Notification{}
|> changeset(params)

Multi.insert(
notification_multi,
String.to_atom("notify_new_for_student_#{student_id}"),
changes
)
end)

Repo.transaction(notification_multi)
end
end

@doc """
When a student has finalized a submission, writes a notification to the corresponding
grader (Avenger) in charge of the student.
"""
@spec write_notification_when_student_submits(%Submission{}) :: Ecto.Changeset.t()
def write_notification_when_student_submits(submission = %Submission{}) do
leader_id =
User
|> Repo.get_by(id: submission.student_id)
|> Repo.preload(:group)
|> Map.get(:group)
|> Map.get(:leader_id)

params = %{
type: :submitted,
read: false,
role: :staff,
user_id: leader_id,
assessment_id: submission.assessment_id,
submission_id: submission.id
}

write(params)
end
end
18 changes: 18 additions & 0 deletions lib/cadet/accounts/notification_type.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import EctoEnum

defenum(Cadet.Accounts.NotificationType, :notification_type, [
# Notifications for new assessments
:new,

# Notifications for deadlines
:deadline,

# Notifications for autograded assessments
:autograded,

# Notifications for manually graded assessments
:manually_graded,

# Notifications for submitted assessments
:submitted
])
41 changes: 39 additions & 2 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule Cadet.Assessments do

import Ecto.Query

alias Cadet.Accounts.User
alias Cadet.Accounts.{Notification, User}
alias Cadet.Assessments.{Answer, Assessment, Query, Question, Submission}
alias Cadet.Autograder.GradingJob
alias Ecto.Multi
Expand Down Expand Up @@ -302,6 +302,8 @@ defmodule Cadet.Assessments do

def publish_assessment(id) do
update_assessment(id, %{is_published: true})
# Send a notification for new assessment
Notification.write_notification_for_new_assessment(id)
end

def create_question_for_assessment(params, assessment_id) when is_ecto_id(assessment_id) do
Expand Down Expand Up @@ -381,6 +383,9 @@ defmodule Cadet.Assessments do
{:is_open?, true} <- is_open?(submission.assessment),
{:status, :attempted} <- {:status, submission.status},
{:ok, updated_submission} <- update_submission_status_and_xp_bonus(submission) do
# Send a notification to the student's grader
Notification.write_notification_when_student_submits(submission)
# Begin autograding job
GradingJob.force_grade_individual_submission(updated_submission)

{:ok, nil}
Expand Down Expand Up @@ -636,6 +641,32 @@ defmodule Cadet.Assessments do
end
end

defp check_grading_status_for_notifications(submission_id) do
submission =
Submission
|> Repo.get_by(id: submission_id)

question_count =
Question
|> where(assessment_id: ^submission.assessment_id)
|> select([q], count(q.id))
|> Repo.one()

graded_count =
Answer
|> where([a], submission_id: ^submission_id)
|> where([a], not is_nil(a.grader_id))
|> select([a], count(a.id))
|> Repo.one()

if question_count == graded_count do
# Every answer in this submission has been graded manually
Notification.write_notification_when_manually_graded(submission_id)
else
# Manual grading for the entire submission has not been completed
end
end

@spec update_grading_info(
%{submission_id: integer() | String.t(), question_id: integer() | String.t()},
%{},
Expand Down Expand Up @@ -675,6 +706,8 @@ defmodule Cadet.Assessments do
{:valid, changeset = %Ecto.Changeset{valid?: true}} <-
{:valid, Answer.grading_changeset(answer, attrs)},
{:ok, _} <- Repo.update(changeset) do
# We check whether we can send a notification or not
check_grading_status_for_notifications(submission_id)
{:ok, nil}
else
{:answer_found?, false} ->
Expand Down Expand Up @@ -710,7 +743,11 @@ defmodule Cadet.Assessments do
end
end

defp is_open?(%Assessment{open_at: open_at, close_at: close_at, is_published: is_published}) do
@doc """
Checks if an assessment is open and published.
"""
@spec is_open?(%Assessment{}) :: {:is_open?, boolean()}
def is_open?(%Assessment{open_at: open_at, close_at: close_at, is_published: is_published}) do
{:is_open?, Timex.between?(Timex.now(), open_at, close_at) and is_published}
end

Expand Down
4 changes: 3 additions & 1 deletion lib/cadet/jobs/autograder/grading_job.ex
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ defmodule Cadet.Autograder.GradingJob do
)
end

defp grade_submission_question_answer_lists(_, [], [], _) do
defp grade_submission_question_answer_lists(submission_id, [], [], _) do
# The entire question-answer list has been graded, so we can send a notification
Cadet.Accounts.Notification.write_notification_when_manually_graded(submission_id)
end
end
Loading