• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / backend / e0330f2cf38b2d8af12bffd20f4cac2158d607fc-PR-1236

31 Mar 2025 09:12AM UTC coverage: 19.982% (-73.6%) from 93.607%
e0330f2cf38b2d8af12bffd20f4cac2158d607fc-PR-1236

Pull #1236

github

RichDom2185
Redate migrations to maintain total ordering
Pull Request #1236: Added Exam mode

12 of 57 new or added lines in 8 files covered. (21.05%)

2430 existing lines in 97 files now uncovered.

671 of 3358 relevant lines covered (19.98%)

3.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lib/cadet/accounts/notifications.ex
1
defmodule Cadet.Accounts.Notifications do
2
  @moduledoc """
3
  Provides functions to fetch, write and acknowledge notifications.
4
  """
5

6
  use Cadet, :context
7

8
  import Ecto.Query
9

10
  alias Cadet.Repo
11
  alias Cadet.Accounts.{Notification, CourseRegistration, CourseRegistration, Team, TeamMember}
12
  alias Cadet.Assessments.Submission
13
  alias Ecto.Multi
14

15
  @doc """
16
  Fetches all unread notifications belonging to a course_reg as an array
17
  """
18
  @spec fetch(CourseRegistration.t()) :: {:ok, {:array, Notification}}
19
  def fetch(course_reg = %CourseRegistration{}) do
UNCOV
20
    notifications =
×
21
      Notification
UNCOV
22
      |> where(course_reg_id: ^course_reg.id)
×
UNCOV
23
      |> where(read: false)
×
24
      |> join(:inner, [n], a in assoc(n, :assessment))
UNCOV
25
      |> preload([n, a], assessment: {a, :config})
×
26
      |> Repo.all()
27

28
    {:ok, notifications}
29
  end
30

31
  @doc """
32
  Writes a new notification into the database, or updates an existing one
33
  """
34
  @spec write(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
35
  def write(params = %{role: role}) do
UNCOV
36
    case role do
×
UNCOV
37
      :student -> write_student(params)
×
UNCOV
38
      :staff -> write_staff(params)
×
39
      _ -> {:error, Notification.changeset(%Notification{}, params)}
×
40
    end
41
  end
42

43
  def write(params), do: {:error, Notification.changeset(%Notification{}, params)}
×
44

45
  defp write_student(
46
         params = %{
47
           course_reg_id: course_reg_id,
48
           assessment_id: assessment_id,
49
           type: type
50
         }
51
       ) do
52
    Notification
53
    |> where(course_reg_id: ^course_reg_id)
54
    |> where(assessment_id: ^assessment_id)
UNCOV
55
    |> where(type: ^type)
×
56
    |> Repo.one()
57
    |> case do
58
      nil ->
UNCOV
59
        Notification.changeset(%Notification{}, params)
×
60

61
      notification ->
62
        notification
UNCOV
63
        |> Notification.changeset(%{
×
64
          read: false,
65
          role: :student
66
        })
67
    end
UNCOV
68
    |> Repo.insert_or_update()
×
69
  end
70

UNCOV
71
  defp write_student(params), do: {:error, Notification.changeset(%Notification{}, params)}
×
72

73
  defp write_staff(
74
         params = %{
75
           course_reg_id: course_reg_id,
76
           submission_id: submission_id,
77
           type: type
78
         }
79
       ) do
80
    Notification
81
    |> where(course_reg_id: ^course_reg_id)
82
    |> where(submission_id: ^submission_id)
UNCOV
83
    |> where(type: ^type)
×
84
    |> Repo.one()
85
    |> case do
86
      nil ->
UNCOV
87
        Notification.changeset(%Notification{}, params)
×
88

89
      notification ->
90
        notification
UNCOV
91
        |> Notification.changeset(%{
×
92
          read: false,
93
          role: :staff
94
        })
95
    end
UNCOV
96
    |> Repo.insert_or_update()
×
97
  end
98

99
  defp write_staff(params), do: {:error, Notification.changeset(%Notification{}, params)}
×
100

101
  @doc """
102
  Changes read status of notification(s) from false to true.
103
  """
104
  @spec acknowledge({:array, :integer}, CourseRegistration.t()) ::
105
          {:ok, Ecto.Schema.t()}
106
          | {:error, any}
107
          | {:error, Ecto.Multi.name(), any, %{Ecto.Multi.name() => any}}
108
  def acknowledge(notification_ids, course_reg = %CourseRegistration{})
109
      when is_list(notification_ids) do
110
    Multi.new()
111
    |> Multi.run(:update_all, fn _repo, _ ->
UNCOV
112
      Enum.reduce_while(notification_ids, {:ok, nil}, fn n_id, acc ->
×
113
        # credo:disable-for-next-line
UNCOV
114
        case acc do
×
UNCOV
115
          {:ok, _} ->
×
116
            {:cont, acknowledge(n_id, course_reg)}
117

118
          _ ->
×
119
            {:halt, acc}
120
        end
121
      end)
122
    end)
UNCOV
123
    |> Repo.transaction()
×
124
  end
125

126
  @spec acknowledge(:integer, CourseRegistration.t()) :: {:ok, Ecto.Schema.t()} | {:error, any()}
127
  def acknowledge(notification_id, course_reg = %CourseRegistration{}) do
UNCOV
128
    notification = Repo.get_by(Notification, id: notification_id, course_reg_id: course_reg.id)
×
129

UNCOV
130
    case notification do
×
UNCOV
131
      nil ->
×
132
        {:error, {:not_found, "Notification does not exist or does not belong to user"}}
133

134
      notification ->
135
        notification
UNCOV
136
        |> Notification.changeset(%{role: course_reg.role, read: true})
×
UNCOV
137
        |> Repo.update()
×
138
    end
139
  end
140

141
  @doc """
142
  Function that handles notifications when a submission is unsubmitted.
143
  """
144
  @spec handle_unsubmit_notifications(integer(), CourseRegistration.t()) ::
145
          {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
146
  def handle_unsubmit_notifications(assessment_id, student = %CourseRegistration{})
147
      when is_ecto_id(assessment_id) do
148
    # Fetch and delete all notifications of :unpublished_grading
149
    # Add new notification :unsubmitted
150

151
    Notification
UNCOV
152
    |> where(course_reg_id: ^student.id)
×
153
    |> where(assessment_id: ^assessment_id)
UNCOV
154
    |> where([n], n.type in ^[:unpublished_grading])
×
UNCOV
155
    |> Repo.delete_all()
×
156

UNCOV
157
    write(%{
×
158
      type: :unsubmitted,
159
      role: :student,
UNCOV
160
      course_reg_id: student.id,
×
161
      assessment_id: assessment_id
162
    })
163
  end
164

165
  @doc """
166
  Function that handles notifications when a submission grade is unpublished.
167
  Deletes all :published notifications and adds a new :unpublished_grading notification.
168
  """
169
  @spec handle_unpublish_grades_notifications(integer(), CourseRegistration.t()) ::
170
          {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
171
  def handle_unpublish_grades_notifications(assessment_id, student = %CourseRegistration{})
172
      when is_ecto_id(assessment_id) do
173
    Notification
UNCOV
174
    |> where(course_reg_id: ^student.id)
×
175
    |> where(assessment_id: ^assessment_id)
UNCOV
176
    |> where([n], n.type in ^[:published_grading])
×
UNCOV
177
    |> Repo.delete_all()
×
178

UNCOV
179
    write(%{
×
180
      type: :unpublished_grading,
181
      read: false,
182
      role: :student,
UNCOV
183
      course_reg_id: student.id,
×
184
      assessment_id: assessment_id
185
    })
186
  end
187

188
  @doc """
189
  Writes a notification that a student's submission has been
190
  graded successfully. (for the student)
191
  """
192
  @spec write_notification_when_published(integer(), any()) ::
193
          {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
194
  def write_notification_when_published(submission_id, type) when type in [:published_grading] do
UNCOV
195
    case Repo.get(Submission, submission_id) do
×
UNCOV
196
      nil ->
×
197
        {:error, %Ecto.Changeset{}}
198

199
      submission ->
UNCOV
200
        case submission.student_id do
×
201
          nil ->
UNCOV
202
            team = Repo.get(Team, submission.team_id)
×
203

UNCOV
204
            query =
×
UNCOV
205
              from(t in Team,
×
206
                join: tm in TeamMember,
207
                on: t.id == tm.team_id,
208
                join: cr in CourseRegistration,
209
                on: tm.student_id == cr.id,
UNCOV
210
                where: t.id == ^team.id,
×
211
                select: cr.id
212
              )
213

UNCOV
214
            team_members = Repo.all(query)
×
215

UNCOV
216
            Enum.each(team_members, fn tm_id ->
×
UNCOV
217
              write(%{
×
218
                type: type,
219
                read: false,
220
                role: :student,
221
                course_reg_id: tm_id,
UNCOV
222
                assessment_id: submission.assessment_id
×
223
              })
224
            end)
225

226
          student_id ->
UNCOV
227
            write(%{
×
228
              type: type,
229
              read: false,
230
              role: :student,
231
              course_reg_id: student_id,
UNCOV
232
              assessment_id: submission.assessment_id
×
233
            })
234
        end
235
    end
236
  end
237

238
  @doc """
239
  Writes a notification to all students that a new assessment is available.
240
  """
241
  @spec write_notification_for_new_assessment(integer(), integer()) ::
242
          {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
243
  def write_notification_for_new_assessment(course_id, assessment_id)
244
      when is_ecto_id(assessment_id) and is_ecto_id(course_id) do
245
    Multi.new()
246
    |> Multi.run(:insert_all, fn _repo, _ ->
247
      CourseRegistration
248
      |> where(course_id: ^course_id)
UNCOV
249
      |> where(role: ^:student)
×
250
      |> Repo.all()
UNCOV
251
      |> Enum.reduce_while({:ok, nil}, fn student, acc ->
×
252
        # credo:disable-for-next-line
UNCOV
253
        case acc do
×
UNCOV
254
          {:ok, _} ->
×
255
            {:cont,
256
             write(%{
257
               type: :new,
258
               read: false,
259
               role: :student,
UNCOV
260
               course_reg_id: student.id,
×
261
               assessment_id: assessment_id
262
             })}
263

264
          _ ->
×
265
            {:halt, acc}
266
        end
267
      end)
268
    end)
UNCOV
269
    |> Repo.transaction()
×
270
  end
271

272
  @doc """
273
  When a student has finalized a submission, writes a notification to the corresponding
274
  grader (Avenger) in charge of the student.
275
  """
276
  @spec write_notification_when_student_submits(Submission.t()) ::
277
          {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
278
  def write_notification_when_student_submits(submission = %Submission{}) do
UNCOV
279
    id =
×
UNCOV
280
      case submission.student_id do
×
281
        nil ->
UNCOV
282
          team_id = submission.team_id
×
283

UNCOV
284
          team =
×
285
            Repo.one(
286
              from(t in Team,
287
                where: t.id == ^team_id,
288
                preload: [:team_members]
289
              )
290
            )
291

292
          # Does not matter if team members have different Avengers
293
          # Just require one of them to be notified of the submission
UNCOV
294
          s_id = team.team_members |> hd() |> Map.get(:student_id)
×
UNCOV
295
          s_id
×
296

297
        _ ->
UNCOV
298
          submission.student_id
×
299
      end
300

UNCOV
301
    avenger_id = get_avenger_id_of(id)
×
302

UNCOV
303
    if is_nil(avenger_id) do
×
304
      {:ok, nil}
305
    else
UNCOV
306
      write(%{
×
307
        type: :submitted,
308
        read: false,
309
        role: :staff,
310
        course_reg_id: avenger_id,
UNCOV
311
        assessment_id: submission.assessment_id,
×
UNCOV
312
        submission_id: submission.id
×
313
      })
314
    end
315
  end
316

317
  defp get_avenger_id_of(student_id) when is_ecto_id(student_id) do
318
    CourseRegistration
319
    |> Repo.get_by(id: student_id)
320
    |> Repo.preload(:group)
321
    |> Map.get(:group)
UNCOV
322
    |> case do
×
UNCOV
323
      nil -> nil
×
UNCOV
324
      group -> Map.get(group, :leader_id)
×
325
    end
326
  end
327
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc