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

source-academy / backend / 64e812b1e1e43547eebe080b6e9f3a76fc4a3351-PR-1238

17 Mar 2025 01:53PM UTC coverage: 91.307% (-2.3%) from 93.605%
64e812b1e1e43547eebe080b6e9f3a76fc4a3351-PR-1238

Pull #1238

github

tohyzhong
Add top contest leaderboard display configuration and update related tests.
Updated leaderboard fetching and exporting for assessment workspace leaderboard.
Added Leaderboard Dropdown contests fetching.
Pull Request #1238: Leaderboard

4 of 69 new or added lines in 4 files covered. (5.8%)

17 existing lines in 3 files now uncovered.

3088 of 3382 relevant lines covered (91.31%)

1043.89 hits per line

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

86.42
/lib/cadet_web/admin_controllers/admin_user_controller.ex
1
defmodule CadetWeb.AdminUserController do
2
  use CadetWeb, :controller
3
  use PhoenixSwagger
4

5
  import Ecto.Query
6

7
  alias Cadet.Repo
8
  alias Cadet.{Accounts, Assessments, Courses}
9
  alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role}
10

11
  # This controller is used to find all users of a course
12

13
  def index(conn, filter) do
14
    users =
3✔
15
      filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg)
3✔
16

17
    render(conn, "users.json", users: users)
3✔
18
  end
19

20
  def combined_total_xp(conn, %{"course_reg_id" => course_reg_id}) do
21
    course_reg = Repo.get(CourseRegistration, course_reg_id)
1✔
22

23
    course_id = course_reg.course_id
1✔
24
    user_id = course_reg.user_id
1✔
25
    course_reg_id = course_reg.id
1✔
26

27
    total_xp = Assessments.user_total_xp(course_id, user_id, course_reg_id)
1✔
28
    json(conn, %{totalXp: total_xp})
1✔
29
  end
30

31
  def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do
NEW
32
    users_with_xp = Assessments.all_user_total_xp(course_id)
×
NEW
33
    json(conn, %{users: users_with_xp})
×
34
  end
35

36
  @add_users_role ~w(admin)a
37
  def get_students(conn, filter) do
38
    users =
×
39
      filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg)
×
40

41
    render(conn, "get_students.json", users: users)
×
42
  end
43

44
  @add_users_role ~w(admin)a
45
  def upsert_users_and_groups(conn, %{
46
        "course_id" => course_id,
47
        "users" => usernames_roles_groups,
48
        "provider" => provider
49
      }) do
50
    %{role: admin_role} = conn.assigns.course_reg
10✔
51
    usernames_roles_groups = usernames_roles_groups |> Enum.map(&to_snake_case_atom_keys/1)
10✔
52

53
    with {:validate_cap, true} <-
10✔
54
           {:validate_cap,
55
            Enum.count(CourseRegistrations.get_users(course_id) ++ usernames_roles_groups) <= 1500},
56
         {:validate_role, true} <- {:validate_role, admin_role in @add_users_role},
9✔
57
         {:validate_provider, true} <-
8✔
58
           {:validate_provider,
59
            Map.has_key?(Application.get_env(:cadet, :identity_providers, %{}), provider)},
60
         {:validate_usernames, true} <-
7✔
61
           {:validate_usernames,
62
            Enum.all?(usernames_roles_groups, fn x ->
63
              Map.has_key?(x, :username) and is_binary(x.username) and x.username != ""
37✔
64
            end)},
65
         {:validate_roles, true} <-
4✔
66
           {:validate_roles,
67
            Enum.all?(usernames_roles_groups, fn x ->
68
              Map.has_key?(x, :role) and String.to_atom(x.role) in Role.__enums__()
20✔
69
            end)} do
70
      {:ok, conn} =
2✔
71
        Repo.transaction(
72
          fn ->
73
            with {:upsert_users, :ok} <-
2✔
74
                   {:upsert_users,
75
                    CourseRegistrations.upsert_users_in_course(
76
                      provider,
77
                      usernames_roles_groups,
78
                      course_id
79
                    )},
80
                 {:upsert_groups, :ok} <-
2✔
81
                   {:upsert_groups,
82
                    Courses.upsert_groups_in_course(usernames_roles_groups, course_id, provider)} do
83
              text(conn, "OK")
2✔
84
            else
85
              {:upsert_users, {:error, {status, message}}} ->
86
                conn |> put_status(status) |> text(message)
×
87

88
              {:upsert_groups, {:error, {status, message}}} ->
89
                conn |> put_status(status) |> text(message)
×
90
            end
91
          end,
92
          timeout: 20_000
93
        )
94

95
      conn
2✔
96
    else
97
      {:validate_cap, false} ->
98
        conn |> put_status(:bad_request) |> text("A course can have maximum of 1500 users")
1✔
99

100
      {:validate_role, false} ->
101
        conn |> put_status(:forbidden) |> text("User is not permitted to add users")
1✔
102

103
      {:validate_provider, false} ->
104
        conn |> put_status(:bad_request) |> text("Invalid authentication provider")
1✔
105

106
      {:validate_usernames, false} ->
107
        conn |> put_status(:bad_request) |> text("Invalid username(s) provided")
3✔
108

109
      {:validate_roles, false} ->
110
        conn |> put_status(:bad_request) |> text("Invalid role(s) provided")
2✔
111
    end
112
  end
113

114
  @update_role_roles ~w(admin)a
115
  def update_role(conn, %{"role" => role, "course_reg_id" => course_reg_id}) do
116
    course_reg_id = course_reg_id |> String.to_integer()
6✔
117

118
    %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} =
6✔
119
      conn.assigns.course_reg
6✔
120

121
    with {:validate_role, true} <- {:validate_role, admin_role in @update_role_roles},
6✔
122
         {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != course_reg_id},
6✔
123
         {:get_cr, user_course_reg} when not is_nil(user_course_reg) <-
6✔
124
           {:get_cr, CourseRegistration |> where(id: ^course_reg_id) |> Repo.one()},
6✔
125
         {:validate_same_course, true} <-
5✔
126
           {:validate_same_course, user_course_reg.course_id == admin_course_id} do
5✔
127
      case CourseRegistrations.update_role(role, course_reg_id) do
4✔
128
        {:ok, %{}} ->
129
          text(conn, "OK")
3✔
130

131
        {:error, {status, message}} ->
132
          conn
133
          |> put_status(status)
134
          |> text(message)
1✔
135
      end
136
    else
137
      {:validate_role, false} ->
138
        conn |> put_status(:forbidden) |> text("User is not permitted to change others' roles")
×
139

140
      {:validate_not_self, false} ->
141
        conn |> put_status(:bad_request) |> text("Admin not allowed to downgrade own role")
×
142

143
      {:get_cr, _} ->
144
        conn |> put_status(:bad_request) |> text("User course registration does not exist")
1✔
145

146
      {:validate_same_course, false} ->
147
        conn |> put_status(:forbidden) |> text("User is in a different course")
1✔
148
    end
149
  end
150

151
  @delete_user_roles ~w(admin)a
152
  def delete_user(conn, %{"course_reg_id" => course_reg_id}) do
153
    course_reg_id = course_reg_id |> String.to_integer()
6✔
154

155
    %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} =
6✔
156
      conn.assigns.course_reg
6✔
157

158
    with {:validate_role, true} <- {:validate_role, admin_role in @delete_user_roles},
6✔
159
         {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != course_reg_id},
6✔
160
         {:get_cr, user_course_reg} when not is_nil(user_course_reg) <-
5✔
161
           {:get_cr, CourseRegistration |> where(id: ^course_reg_id) |> Repo.one()},
5✔
162
         {:prevent_delete_admin, true} <- {:prevent_delete_admin, user_course_reg.role != :admin},
4✔
163
         {:validate_same_course, true} <-
3✔
164
           {:validate_same_course, user_course_reg.course_id == admin_course_id} do
3✔
165
      case CourseRegistrations.delete_course_registration(course_reg_id) do
2✔
166
        {:ok, %{}} ->
167
          text(conn, "OK")
2✔
168

169
        {:error, {status, message}} ->
170
          conn
171
          |> put_status(status)
172
          |> text(message)
×
173
      end
174
    else
175
      {:validate_role, false} ->
176
        conn |> put_status(:forbidden) |> text("User is not permitted to delete other users")
×
177

178
      {:validate_not_self, false} ->
179
        conn
180
        |> put_status(:bad_request)
181
        |> text("Admin not allowed to delete ownself from course")
1✔
182

183
      {:get_cr, _} ->
184
        conn |> put_status(:bad_request) |> text("User course registration does not exist")
1✔
185

186
      {:prevent_delete_admin, false} ->
187
        conn |> put_status(:bad_request) |> text("Admins cannot be deleted")
1✔
188

189
      {:validate_same_course, false} ->
190
        conn |> put_status(:forbidden) |> text("User is in a different course")
1✔
191
    end
192
  end
193

194
  swagger_path :index do
1✔
195
    get("/courses/{course_id}/admin/users")
196

197
    summary("Returns a list of users in the course owned by the admin")
198

199
    security([%{JWT: []}])
200
    produces("application/json")
201
    response(200, "OK", Schema.ref(:AdminUserInfo))
202
    response(401, "Unauthorised")
203
  end
204

205
  swagger_path :combined_total_xp do
1✔
206
    get("/courses/{course_id}/admin/users/{course_reg_id}/total_xp")
207

208
    summary("Get the specified user's total XP from achievements and assessments")
209

210
    security([%{JWT: []}])
211
    produces("application/json")
212

213
    parameters do
214
      course_id(:path, :integer, "Course ID", required: true)
215
      course_reg_id(:path, :integer, "Course registration ID", required: true)
216
    end
217

218
    response(200, "OK", Schema.ref(:TotalXPInfo))
219
    response(401, "Unauthorised")
220
  end
221

222
  swagger_path :upsert_users_and_groups do
1✔
223
    put("/courses/{course_id}/admin/users")
224

225
    summary("Adds the list of usernames and roles to the course")
226
    security([%{JWT: []}])
227
    consumes("application/json")
228

229
    parameters do
230
      course_id(:path, :integer, "Course ID", required: true)
231
      users(:body, Schema.array(:UsernameAndRole), "Array of usernames and roles", required: true)
232

233
      provider(:body, :string, "The authentication provider linked to these usernames",
234
        required: true
235
      )
236
    end
237

238
    response(200, "OK")
239
    response(400, "Bad Request. Invalid provider, username or role")
240
    response(403, "Forbidden. You are not an admin")
241
  end
242

243
  swagger_path :update_role do
1✔
244
    put("/courses/{course_id}/admin/users/{course_reg_id}/role")
245

246
    summary("Updates the role of the given user in the the course")
247
    security([%{JWT: []}])
248
    consumes("application/json")
249

250
    parameters do
251
      course_id(:path, :integer, "Course ID", required: true)
252
      role(:body, :role, "The new role", required: true)
253

254
      courseRegId(
255
        :body,
256
        :integer,
257
        "The course registration of the user whose role is to be updated",
258
        required: true
259
      )
260
    end
261

262
    response(200, "OK")
263

264
    response(
265
      400,
266
      "Bad Request. User course registration does not exist or admin not allowed to downgrade own role"
267
    )
268

269
    response(403, "Forbidden. User is in different course, or you are not an admin")
270
  end
271

272
  swagger_path :delete_user do
1✔
273
    delete("/courses/{course_id}/admin/users/{course_reg_id}")
274

275
    summary("Deletes a user from a course")
276
    consumes("application/json")
277

278
    parameters do
279
      course_id(:path, :integer, "Course ID", required: true)
280

281
      courseRegId(
282
        :body,
283
        :integer,
284
        "The course registration of the user whose role is to be updated",
285
        required: true
286
      )
287
    end
288

289
    response(200, "OK")
290

291
    response(
292
      400,
293
      "Bad Request. User course registration does not exist or admin not allowed to delete ownself from course or admins cannot be deleted"
294
    )
295

296
    response(403, "Forbidden. User is in different course, or you are not an admin")
297
  end
298

299
  def swagger_definitions do
300
    %{
1✔
301
      AdminUserInfo:
302
        swagger_schema do
1✔
303
          title("User")
304
          description("Basic information about the user in this course")
305

306
          properties do
1✔
307
            userId(:integer, "User's ID")
308
            name(:string, "Full name of the user")
309

310
            role(
311
              :string,
312
              "Role of the user in this course. Can be 'student', 'staff', or 'admin'"
313
            )
314

315
            group(
1✔
316
              :string,
317
              "Group the user belongs to in this course. May be null if the user does not belong to any group"
318
            )
319
          end
320
        end,
321
      UsernameAndRole:
322
        swagger_schema do
1✔
323
          title("Username and role")
324
          description("Username and role of the user to add to this course")
325

326
          properties do
1✔
327
            username(:string, "The user's username")
328
            role(:role, "The user's role. Can be 'student', 'staff', or 'admin'")
1✔
329
          end
330
        end
331
    }
332
  end
333
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

© 2026 Coveralls, Inc