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

source-academy / backend / 18dc689a4df4836fc6967bf0f74dc252964bd175-PR-1180

08 Sep 2024 06:14PM UTC coverage: 79.088% (-15.3%) from 94.372%
18dc689a4df4836fc6967bf0f74dc252964bd175-PR-1180

Pull #1180

github

josh1248
Change appropriate routes into admin scope
Pull Request #1180: Transfer groundControl (and admin panel) from staff to admin route

7 of 12 new or added lines in 1 file covered. (58.33%)

499 existing lines in 25 files now uncovered.

2602 of 3290 relevant lines covered (79.09%)

1023.2 hits per line

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

95.88
/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} ->
33
      CourseRegistrations.enroll_course(%{
3✔
34
        course_id: course.id,
3✔
35
        user_id: user.id,
3✔
36
        role: :admin
37
      })
38
    end)
39
    |> Repo.transaction()
5✔
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) when is_ecto_id(course_id) do
48
    case retrieve_course(course_id) do
3✔
49
      nil ->
1✔
50
        {:error, {:bad_request, "Invalid course id"}}
51

52
      course ->
53
        assessment_configs =
2✔
54
          AssessmentConfig
55
          |> where(course_id: ^course_id)
2✔
56
          |> Repo.all()
57
          |> Enum.sort(&(&1.order < &2.order))
4✔
58
          |> Enum.map(& &1.type)
5✔
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
70
    case retrieve_course(course_id) do
9✔
71
      nil ->
1✔
72
        {:error, {:bad_request, "Invalid course id"}}
73

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

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

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

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

97
  defp remove_latest_viewed_course_id(course_id) do
98
    User
99
    |> where(latest_viewed_course_id: ^course_id)
3✔
100
    |> Repo.all()
101
    |> Enum.each(fn user ->
3✔
102
      user
103
      |> User.changeset(%{latest_viewed_course_id: nil})
104
      |> Repo.update()
1✔
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)
111
    |> order_by(:order)
14✔
112
    |> Repo.all()
14✔
113
  end
114

115
  def mass_upsert_and_reorder_assessment_configs(course_id, configs) do
116
    if is_list(configs) do
6✔
117
      configs_length = configs |> length()
5✔
118

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

128
        reorder_assessment_configs(course_id, new_configs)
3✔
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)
143
    |> where(id: ^assessment_config_id)
14✔
144
    |> Repo.one()
145
    |> case do
146
      nil ->
147
        AssessmentConfig.changeset(%AssessmentConfig{}, Map.put(params, :course_id, course_id))
4✔
148

149
      at ->
150
        AssessmentConfig.changeset(at, params)
10✔
151
    end
152
    |> Repo.insert_or_update()
14✔
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)
161
    |> where(id: ^assessment_config_id)
32✔
162
    |> Repo.one()
163
    |> case do
32✔
164
      nil -> {:error, :no_such_entry}
×
165
      at -> at |> AssessmentConfig.changeset(params) |> Repo.update()
32✔
166
    end
167
  end
168

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

176
      configs
177
      |> Enum.with_index(1)
178
      |> Enum.each(fn {elem, idx} ->
4✔
179
        update_assessment_config(course_id, Map.put(elem, :order, idx))
16✔
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
187
    config =
3✔
188
      AssessmentConfig
189
      |> where(course_id: ^course_id)
190
      |> where(id: ^assessment_config_id)
3✔
191
      |> Repo.one()
192

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

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

203
        Repo.delete(config)
2✔
204
    end
205
  end
206

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

217
        :error ->
218
          # Delete users from group
219
          upsert_groups_in_course_helper(username, course_id, provider)
2✔
220
      end
221
      |> case do
9✔
222
        {:ok, _} -> {:cont, :ok}
9✔
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
229
    with {:get_group, {:ok, group}} <- {:get_group, get_or_create_group(groupname, course_id)},
7✔
230
         {:get_course_reg, %{role: role} = course_reg} <-
7✔
231
           {:get_course_reg,
232
            CourseRegistration
233
            |> where(
234
              user_id:
235
                ^(User
236
                  |> where(username: ^username, provider: ^provider)
7✔
237
                  |> Repo.one()
238
                  |> Map.fetch!(:id))
239
            )
240
            |> where(course_id: ^course_id)
7✔
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
244
      case role do
7✔
245
        # If student, update his course registration
246
        :student ->
247
          update_course_reg_group(course_reg, group.id)
3✔
248

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

255
            group
256
            |> Group.changeset(%{leader_id: course_reg.id})
3✔
257
            |> Repo.update()
3✔
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
266
    with {:get_course_reg, %{role: role} = course_reg} <-
2✔
267
           {:get_course_reg,
268
            CourseRegistration
269
            |> where(
270
              user_id:
271
                ^(User
272
                  |> where(username: ^username, provider: ^provider)
2✔
273
                  |> Repo.one()
274
                  |> Map.fetch!(:id))
275
            )
276
            |> where(course_id: ^course_id)
2✔
277
            |> Repo.one()} do
278
      case role do
2✔
279
        :student ->
280
          update_course_reg_group(course_reg, nil)
1✔
281

282
        _ ->
283
          remove_staff_from_group(course_id, course_reg.id)
1✔
284
          update_course_reg_group(course_reg, nil)
1✔
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)
293
    |> where(leader_id: ^leader_id)
4✔
294
    |> Repo.one()
295
    |> case do
4✔
296
      nil ->
2✔
297
        nil
298

299
      group ->
300
        group
301
        |> Group.changeset(%{leader_id: nil})
302
        |> Repo.update()
2✔
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})
309
    |> Repo.update()
8✔
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)
320
    |> where(course_id: ^course_id)
9✔
321
    |> Repo.one()
322
    |> case do
9✔
323
      nil ->
324
        %Group{}
325
        |> Group.changeset(%{name: name, course_id: course_id})
326
        |> Repo.insert()
4✔
327

328
      group ->
5✔
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
343
    changeset =
1✔
344
      Sourcecast.changeset(%Sourcecast{uploader_id: user_id, course_id: course_id}, attrs)
345

346
    case Repo.insert(changeset) do
1✔
347
      {:ok, sourcecast} ->
1✔
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
392
    sourcecast = Repo.get(Sourcecast, sourcecast_id)
1✔
393

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

398
      sourcecast ->
399
        SourcecastUpload.delete({sourcecast.audio, sourcecast})
1✔
400
        Repo.delete(sourcecast)
1✔
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
409
    |> where(course_id: ^course_id)
1✔
410
    |> Repo.all()
411
    |> Repo.preload(:uploader)
1✔
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
425
    course.assets_prefix || "#{Assets.assets_prefix()}#{course.id}/"
4✔
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