• 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/courses/courses.ex
1
defmodule Cadet.Courses do
2
  @moduledoc """
3
  Courses context contains domain logic for Course administration
4
  management such as course configuration, discussion groups and materials
5
  """
6
  use Cadet, [:context, :display]
7

8
  import Ecto.Query
9
  alias Ecto.Multi
10

11
  alias Cadet.Accounts.{CourseRegistration, User, CourseRegistrations}
12

13
  alias Cadet.Courses.{
14
    AssessmentConfig,
15
    Course,
16
    Group,
17
    Sourcecast,
18
    SourcecastUpload
19
  }
20

21
  alias Cadet.Assessments
22
  alias Cadet.Assessments.Assessment
23
  alias Cadet.Assets.Assets
24

25
  @doc """
26
  Creates a new course configuration, course registration, and sets
27
  the user's latest course id to the newly created course.
28
  """
29
  def create_course_config(params, user) do
30
    Multi.new()
31
    |> Multi.insert(:course, Course.changeset(%Course{}, params))
32
    |> Multi.run(:course_reg, fn _repo, %{course: course} ->
UNCOV
33
      CourseRegistrations.enroll_course(%{
×
UNCOV
34
        course_id: course.id,
×
UNCOV
35
        user_id: user.id,
×
36
        role: :admin
37
      })
38
    end)
UNCOV
39
    |> Repo.transaction()
×
40
  end
41

42
  @doc """
43
  Returns the course configuration for the specified course.
44
  """
45
  @spec get_course_config(integer) ::
46
          {:ok, Course.t()} | {:error, {:bad_request, String.t()}}
47
  def get_course_config(course_id, is_admin \\ false) when is_ecto_id(course_id) do
UNCOV
48
    case retrieve_course(course_id) do
×
UNCOV
49
      nil ->
×
50
        {:error, {:bad_request, "Invalid course id"}}
51

52
      course ->
UNCOV
53
        assessment_configs =
×
54
          AssessmentConfig
UNCOV
55
          |> where(course_id: ^course_id)
×
56
          |> Repo.all()
UNCOV
57
          |> Enum.sort(&(&1.order < &2.order))
×
UNCOV
58
          |> Enum.map(& &1.type)
×
59

60
        {:ok, Map.put_new(course, :assessment_configs, assessment_configs)}
61
    end
62
  end
63

64
  @doc """
65
  Updates the general course configuration for the specified course
66
  """
67
  @spec update_course_config(integer, %{}) ::
68
          {:ok, Course.t()} | {:error, Ecto.Changeset.t()} | {:error, {:bad_request, String.t()}}
69
  def update_course_config(course_id, params) when is_ecto_id(course_id) do
UNCOV
70
    case retrieve_course(course_id) do
×
UNCOV
71
      nil ->
×
72
        {:error, {:bad_request, "Invalid course id"}}
73

74
      course ->
UNCOV
75
        if Map.has_key?(params, :viewable) and not params.viewable do
×
UNCOV
76
          remove_latest_viewed_course_id(course_id)
×
77
        end
78

79
        course
80
        |> Course.changeset(params)
UNCOV
81
        |> Repo.update()
×
82
    end
83
  end
84

85
  def get_all_course_ids do
86
    Course
UNCOV
87
    |> select([c], c.id)
×
UNCOV
88
    |> Repo.all()
×
89
  end
90

91
  defp retrieve_course(course_id) when is_ecto_id(course_id) do
92
    Course
UNCOV
93
    |> where(id: ^course_id)
×
UNCOV
94
    |> Repo.one()
×
95
  end
96

97
  defp remove_latest_viewed_course_id(course_id) do
98
    User
UNCOV
99
    |> where(latest_viewed_course_id: ^course_id)
×
100
    |> Repo.all()
UNCOV
101
    |> Enum.each(fn user ->
×
102
      user
103
      |> User.changeset(%{latest_viewed_course_id: nil})
UNCOV
104
      |> Repo.update()
×
105
    end)
106
  end
107

108
  def get_assessment_configs(course_id) when is_ecto_id(course_id) do
109
    AssessmentConfig
110
    |> where([at], at.course_id == ^course_id)
UNCOV
111
    |> order_by(:order)
×
UNCOV
112
    |> Repo.all()
×
113
  end
114

115
  def mass_upsert_and_reorder_assessment_configs(course_id, configs) do
UNCOV
116
    if is_list(configs) do
×
UNCOV
117
      configs_length = configs |> length()
×
118

UNCOV
119
      with true <- configs_length <= 8,
×
UNCOV
120
           true <- configs_length >= 1 do
×
UNCOV
121
        new_configs =
×
122
          configs
123
          |> Enum.map(fn elem ->
UNCOV
124
            {:ok, config} = insert_or_update_assessment_config(course_id, elem)
×
UNCOV
125
            Map.put(elem, :assessment_config_id, config.id)
×
126
          end)
127

UNCOV
128
        reorder_assessment_configs(course_id, new_configs)
×
129
      else
130
        false -> {:error, {:bad_request, "Invalid parameter(s)"}}
131
      end
132
    else
133
      {:error, {:bad_request, "Invalid parameter(s)"}}
134
    end
135
  end
136

137
  def insert_or_update_assessment_config(
138
        course_id,
139
        params = %{assessment_config_id: assessment_config_id}
140
      ) do
141
    AssessmentConfig
142
    |> where(course_id: ^course_id)
UNCOV
143
    |> where(id: ^assessment_config_id)
×
144
    |> Repo.one()
145
    |> case do
146
      nil ->
UNCOV
147
        AssessmentConfig.changeset(%AssessmentConfig{}, Map.put(params, :course_id, course_id))
×
148

149
      at ->
UNCOV
150
        AssessmentConfig.changeset(at, params)
×
151
    end
UNCOV
152
    |> Repo.insert_or_update()
×
153
  end
154

155
  defp update_assessment_config(
156
         course_id,
157
         params = %{assessment_config_id: assessment_config_id}
158
       ) do
159
    AssessmentConfig
160
    |> where(course_id: ^course_id)
UNCOV
161
    |> where(id: ^assessment_config_id)
×
162
    |> Repo.one()
UNCOV
163
    |> case do
×
164
      nil -> {:error, :no_such_entry}
×
UNCOV
165
      at -> at |> AssessmentConfig.changeset(params) |> Repo.update()
×
166
    end
167
  end
168

169
  def reorder_assessment_configs(course_id, configs) do
UNCOV
170
    Repo.transaction(fn ->
×
171
      configs
UNCOV
172
      |> Enum.each(fn elem ->
×
UNCOV
173
        update_assessment_config(course_id, Map.put(elem, :order, nil))
×
174
      end)
175

176
      configs
177
      |> Enum.with_index(1)
UNCOV
178
      |> Enum.each(fn {elem, idx} ->
×
UNCOV
179
        update_assessment_config(course_id, Map.put(elem, :order, idx))
×
180
      end)
181
    end)
182
  end
183

184
  @spec delete_assessment_config(integer(), integer()) ::
185
          {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty}
186
  def delete_assessment_config(course_id, assessment_config_id) do
UNCOV
187
    config =
×
188
      AssessmentConfig
189
      |> where(course_id: ^course_id)
UNCOV
190
      |> where(id: ^assessment_config_id)
×
191
      |> Repo.one()
192

UNCOV
193
    case config do
×
UNCOV
194
      nil ->
×
195
        {:error, "The given assessment configuration does not exist"}
196

197
      config ->
198
        Assessment
UNCOV
199
        |> where(config_id: ^config.id)
×
200
        |> Repo.all()
UNCOV
201
        |> Enum.each(fn assessment -> Assessments.delete_assessment(assessment.id) end)
×
202

UNCOV
203
        Repo.delete(config)
×
204
    end
205
  end
206

207
  def upsert_groups_in_course(usernames_and_groups, course_id, provider) do
208
    usernames_and_groups
UNCOV
209
    |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc ->
×
210
      entry
211
      |> Map.fetch(:group)
212
      |> case do
213
        {:ok, groupname} ->
214
          # Add users to group
UNCOV
215
          upsert_groups_in_course_helper(username, course_id, groupname, provider)
×
216

217
        :error ->
218
          # Delete users from group
UNCOV
219
          upsert_groups_in_course_helper(username, course_id, provider)
×
220
      end
UNCOV
221
      |> case do
×
UNCOV
222
        {:ok, _} -> {:cont, :ok}
×
223
        {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}}
×
224
      end
225
    end)
226
  end
227

228
  defp upsert_groups_in_course_helper(username, course_id, groupname, provider) do
UNCOV
229
    with {:get_group, {:ok, group}} <- {:get_group, get_or_create_group(groupname, course_id)},
×
UNCOV
230
         {:get_course_reg, %{role: role} = course_reg} <-
×
231
           {:get_course_reg,
232
            CourseRegistration
233
            |> where(
234
              user_id:
235
                ^(User
UNCOV
236
                  |> where(username: ^username, provider: ^provider)
×
237
                  |> Repo.one()
238
                  |> Map.fetch!(:id))
239
            )
UNCOV
240
            |> where(course_id: ^course_id)
×
241
            |> Repo.one()} do
242
      # It is ok to assume that user course registions already exist, as they would
243
      # have been created in the admin_user_controller before calling this function
UNCOV
244
      case role do
×
245
        # If student, update his course registration
246
        :student ->
UNCOV
247
          update_course_reg_group(course_reg, group.id)
×
248

249
        # If admin or staff, remove their previous group assignment and set them as group leader
250
        _ ->
UNCOV
251
          if group.leader_id != course_reg.id do
×
UNCOV
252
            update_course_reg_group(course_reg, group.id)
×
UNCOV
253
            remove_staff_from_group(course_id, course_reg.id)
×
254

255
            group
UNCOV
256
            |> Group.changeset(%{leader_id: course_reg.id})
×
UNCOV
257
            |> Repo.update()
×
258
          else
259
            {:ok, nil}
260
          end
261
      end
262
    end
263
  end
264

265
  defp upsert_groups_in_course_helper(username, course_id, provider) do
UNCOV
266
    with {:get_course_reg, %{role: role} = course_reg} <-
×
267
           {:get_course_reg,
268
            CourseRegistration
269
            |> where(
270
              user_id:
271
                ^(User
UNCOV
272
                  |> where(username: ^username, provider: ^provider)
×
273
                  |> Repo.one()
274
                  |> Map.fetch!(:id))
275
            )
UNCOV
276
            |> where(course_id: ^course_id)
×
277
            |> Repo.one()} do
UNCOV
278
      case role do
×
279
        :student ->
UNCOV
280
          update_course_reg_group(course_reg, nil)
×
281

282
        _ ->
UNCOV
283
          remove_staff_from_group(course_id, course_reg.id)
×
UNCOV
284
          update_course_reg_group(course_reg, nil)
×
285
          {:ok, nil}
286
      end
287
    end
288
  end
289

290
  defp remove_staff_from_group(course_id, leader_id) do
291
    Group
292
    |> where(course_id: ^course_id)
UNCOV
293
    |> where(leader_id: ^leader_id)
×
294
    |> Repo.one()
UNCOV
295
    |> case do
×
UNCOV
296
      nil ->
×
297
        nil
298

299
      group ->
300
        group
301
        |> Group.changeset(%{leader_id: nil})
UNCOV
302
        |> Repo.update()
×
303
    end
304
  end
305

306
  defp update_course_reg_group(course_reg, group_id) do
307
    course_reg
308
    |> CourseRegistration.changeset(%{group_id: group_id})
UNCOV
309
    |> Repo.update()
×
310
  end
311

312
  @doc """
313
  Get a group based on the group name and course id or create one if it doesn't exist
314
  """
315
  @spec get_or_create_group(String.t(), integer()) ::
316
          {:ok, Group.t()} | {:error, Ecto.Changeset.t()}
317
  def get_or_create_group(name, course_id) when is_binary(name) and is_ecto_id(course_id) do
318
    Group
319
    |> where(name: ^name)
UNCOV
320
    |> where(course_id: ^course_id)
×
321
    |> Repo.one()
UNCOV
322
    |> case do
×
323
      nil ->
324
        %Group{}
325
        |> Group.changeset(%{name: name, course_id: course_id})
UNCOV
326
        |> Repo.insert()
×
327

UNCOV
328
      group ->
×
329
        {:ok, group}
330
    end
331
  end
332

333
  @doc """
334
  Upload a sourcecast file.
335

336
  Note that there are no checks for whether the user belongs to the course,
337
  as this has been checked inside a plug in the router.
338
  """
339
  def upload_sourcecast_file(
340
        _inserter = %CourseRegistration{user_id: user_id, course_id: course_id},
341
        attrs = %{}
342
      ) do
UNCOV
343
    changeset =
×
344
      Sourcecast.changeset(%Sourcecast{uploader_id: user_id, course_id: course_id}, attrs)
345

UNCOV
346
    case Repo.insert(changeset) do
×
UNCOV
347
      {:ok, sourcecast} ->
×
348
        {:ok, sourcecast}
349

UNCOV
350
      {:error, changeset} ->
×
351
        {:error, {:bad_request, full_error_messages(changeset)}}
352
    end
353
  end
354

355
  # @doc """
356
  # Upload a public sourcecast file.
357

358
  # Note that there are no checks for whether the user belongs to the course,
359
  # as this has been checked inside a plug in the router.
360
  # unused in the current version
361
  # """
362
  # def upload_sourcecast_file_public(
363
  #       inserter,
364
  #       _inserter_course_reg = %CourseRegistration{role: role},
365
  #       attrs = %{}
366
  #     ) do
367
  #   if role in @upload_file_roles do
368
  #     changeset =
369
  #       %Sourcecast{}
370
  #       |> Sourcecast.changeset(attrs)
371
  #       |> put_assoc(:uploader, inserter)
372

373
  #     case Repo.insert(changeset) do
374
  #       {:ok, sourcecast} ->
375
  #         {:ok, sourcecast}
376

377
  #       {:error, changeset} ->
378
  #         {:error, {:bad_request, full_error_messages(changeset)}}
379
  #     end
380
  #   else
381
  #     {:error, {:forbidden, "User is not permitted to upload"}}
382
  #   end
383
  # end
384

385
  @doc """
386
  Delete a sourcecast file
387

388
  Note that there are no checks for whether the user belongs to the course, as this has been checked
389
  inside a plug in the router.
390
  """
391
  def delete_sourcecast_file(sourcecast_id) do
UNCOV
392
    sourcecast = Repo.get(Sourcecast, sourcecast_id)
×
393

UNCOV
394
    case sourcecast do
×
UNCOV
395
      nil ->
×
396
        {:error, {:not_found, "Sourcecast not found!"}}
397

398
      sourcecast ->
UNCOV
399
        SourcecastUpload.delete({sourcecast.audio, sourcecast})
×
UNCOV
400
        Repo.delete(sourcecast)
×
401
    end
402
  end
403

404
  @doc """
405
  Get sourcecast files
406
  """
407
  def get_sourcecast_files(course_id) when is_ecto_id(course_id) do
408
    Sourcecast
UNCOV
409
    |> where(course_id: ^course_id)
×
410
    |> Repo.all()
UNCOV
411
    |> Repo.preload(:uploader)
×
412
  end
413

414
  # unused in the current version
415
  # def get_sourcecast_files do
416
  #   Sourcecast
417
  #   # Public sourcecasts are those without course_id
418
  #   |> where([s], is_nil(s.course_id))
419
  #   |> Repo.all()
420
  #   |> Repo.preload(:uploader)
421
  # end
422

423
  @spec assets_prefix(Course.t()) :: binary()
424
  def assets_prefix(course) do
UNCOV
425
    course.assets_prefix || "#{Assets.assets_prefix()}#{course.id}/"
×
426
  end
427
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