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

source-academy / backend / 415c28e8ee8b88e6acc822aca1500269b5f844ad

06 Aug 2025 03:59PM UTC coverage: 91.52% (-0.9%) from 92.467%
415c28e8ee8b88e6acc822aca1500269b5f844ad

push

github

web-flow
Leaderboard (#1238)

* added 'enable_leaderboard' columns in courses table

* Leaderboard create course config, leaderboard page routing, leaderboard sql query for all users in course

* added 'top_leaderboard_display' columns in courses table

* added 'all_user_total_xp' function for leaderboard display

* add top leaderboard display options to course settings (select how many to display in leaderboard)

* added contest scores fetching and contest score calculation

* Refactor query execution in assessments module for improved readability

* added functions to fetch contest scoring and voting

* changes to default values

* updated tests

* Fixed xp fetching for all users

* Add top contest leaderboard display configuration and update related tests.
Updated leaderboard fetching and exporting for assessment workspace leaderboard.
Added Leaderboard Dropdown contests fetching.

* Added automatic XP assignment for winning contest entries

* Implement XP assignment for winning contest entries based on contest voting XML and added dispatch endpoint for XP customisation

* Add default value for XP values and improve XP assignment logic for contest entries

* No tiebreak for contest scoring

* Refactor contest scoring endpoints for authentication errors

* Enhance leaderboard update logic and improve error handling for voting questions

* Refactor XP assignment logic for voting questions and set default XP values.
Refactor Score calculation logic to reset to 0 before calculating.

* Temporary Assessment Workspace leaderboard fix for testing

* Fixed tests for assessments (default XP to award for contests)

* Refactor contest fetching logic to filter by voting question contest numbers

* Refactor leaderboard query logic to use RANK() and improve code readability.
Uncommented leaderboard portions after finalising testing

* temporary fix for STePS

* Add ranking to assessment workspace leaderboard queries and update view helpers to include rank

* Post-STePS f... (continued)

117 of 156 new or added lines in 5 files covered. (75.0%)

7 existing lines in 2 files now uncovered.

3216 of 3514 relevant lines covered (91.52%)

7703.7 hits per line

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

71.6
/lib/cadet_web/admin_controllers/admin_assessments_controller.ex
1
defmodule CadetWeb.AdminAssessmentsController do
2
  use CadetWeb, :controller
3

4
  use PhoenixSwagger
5

6
  import Ecto.Query, only: [where: 2]
7
  import Cadet.Updater.XMLParser, only: [parse_xml: 4]
8

9
  alias CadetWeb.AssessmentsHelpers
10
  alias Cadet.Assessments.{Question, Assessment}
11
  alias Cadet.{Assessments, Repo}
12
  alias Cadet.Accounts.CourseRegistration
13

14
  def index(conn, %{"course_reg_id" => course_reg_id}) do
15
    course_reg = Repo.get(CourseRegistration, course_reg_id)
2✔
16
    {:ok, assessments} = Assessments.all_assessments(course_reg)
2✔
17
    assessments = Assessments.format_all_assessments(assessments)
2✔
18
    render(conn, "index.json", assessments: assessments)
2✔
19
  end
20

21
  def get_assessment(conn, %{"course_reg_id" => course_reg_id, "assessmentid" => assessment_id})
22
      when is_ecto_id(assessment_id) do
23
    course_reg = Repo.get(CourseRegistration, course_reg_id)
×
24

25
    case Assessments.assessment_with_questions_and_answers(assessment_id, course_reg) do
×
26
      {:ok, assessment} -> render(conn, "show.json", assessment: assessment)
×
27
      {:error, {status, message}} -> send_resp(conn, status, message)
×
28
    end
29
  end
30

31
  def create(conn, %{
32
        "course_id" => course_id,
33
        "assessment" => assessment,
34
        "forceUpdate" => force_update,
35
        "assessmentConfigId" => assessment_config_id
36
      }) do
37
    file =
2✔
38
      assessment["file"].path
2✔
39
      |> File.read!()
40

41
    result =
2✔
42
      case force_update do
43
        "true" -> parse_xml(file, course_id, assessment_config_id, true)
1✔
44
        "false" -> parse_xml(file, course_id, assessment_config_id, false)
1✔
45
      end
46

47
    case result do
2✔
48
      :ok ->
49
        if force_update == "true" do
1✔
50
          text(conn, "Force update OK")
×
51
        else
52
          text(conn, "OK")
1✔
53
        end
54

55
      {:ok, warning_message} ->
56
        text(conn, warning_message)
×
57

58
      {:error, {status, message}} ->
59
        conn
60
        |> put_status(status)
61
        |> text(message)
1✔
62
    end
63
  end
64

65
  def delete(conn, %{"course_id" => course_id, "assessmentid" => assessment_id}) do
66
    with {:same_course, true} <- {:same_course, is_same_course(course_id, assessment_id)},
2✔
67
         {:ok, _} <- Assessments.delete_assessment(assessment_id) do
1✔
68
      text(conn, "OK")
1✔
69
    else
70
      {:same_course, false} ->
71
        conn
72
        |> put_status(403)
73
        |> text("User not allow to delete assessments from another course")
1✔
74

75
      {:error, {status, message}} ->
76
        conn
77
        |> put_status(status)
78
        |> text(message)
×
79
    end
80
  end
81

82
  def update(conn, params = %{"assessmentid" => assessment_id}) when is_ecto_id(assessment_id) do
83
    open_at = params |> Map.get("openAt")
9✔
84
    close_at = params |> Map.get("closeAt")
9✔
85
    is_published = params |> Map.get("isPublished")
9✔
86
    max_team_size = params |> Map.get("maxTeamSize")
9✔
87
    has_token_counter = params |> Map.get("hasTokenCounter")
9✔
88
    has_voting_features = params |> Map.get("hasVotingFeatures")
9✔
89
    assign_entries_for_voting = params |> Map.get("assignEntriesForVoting")
9✔
90

91
    updated_assessment =
9✔
92
      if is_nil(is_published) do
93
        %{}
7✔
94
      else
95
        %{:is_published => is_published}
2✔
96
      end
97

98
    updated_assessment =
9✔
99
      if is_nil(max_team_size) do
100
        updated_assessment
9✔
101
      else
102
        Map.put(updated_assessment, :max_team_size, max_team_size)
×
103
      end
104

105
    updated_assessment =
9✔
106
      if is_nil(has_token_counter) do
107
        updated_assessment
7✔
108
      else
109
        Map.put(updated_assessment, :has_token_counter, has_token_counter)
2✔
110
      end
111

112
    updated_assessment =
9✔
113
      if is_nil(has_voting_features) do
114
        updated_assessment
7✔
115
      else
116
        Map.put(updated_assessment, :has_voting_features, has_voting_features)
2✔
117
      end
118

119
    is_reassigning_voting =
9✔
120
      if is_nil(assign_entries_for_voting) do
9✔
121
        false
122
      else
123
        assign_entries_for_voting
×
124
      end
125

126
    with {:ok, assessment} <- check_dates(open_at, close_at, updated_assessment),
9✔
127
         {:ok, _nil} <- Assessments.update_assessment(assessment_id, assessment),
8✔
128
         {:ok, _nil} <- Assessments.reassign_voting(assessment_id, is_reassigning_voting) do
8✔
129
      text(conn, "OK")
8✔
130
    else
131
      {:error, {status, message}} ->
132
        conn
133
        |> put_status(status)
134
        |> text(message)
1✔
135
    end
136
  end
137

138
  def calculate_contest_score(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
UNCOV
139
    voting_questions =
×
140
      Question
141
      |> where(type: :voting)
UNCOV
142
      |> where(assessment_id: ^assessment_id)
×
143
      |> Repo.one()
144

NEW
145
    if voting_questions do
×
NEW
146
      Assessments.compute_relative_score(voting_questions.id)
×
NEW
147
      text(conn, "CONTEST SCORE CALCULATED")
×
148
    else
NEW
149
      text(conn, "No voting questions found for the given assessment")
×
150
    end
151
  end
152

153
  def dispatch_contest_xp(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
UNCOV
154
    voting_questions =
×
155
      Question
156
      |> where(type: :voting)
UNCOV
157
      |> where(assessment_id: ^assessment_id)
×
158
      |> Repo.one()
159

NEW
160
    if voting_questions do
×
NEW
161
      Assessments.assign_winning_contest_entries_xp(voting_questions.id)
×
162

NEW
163
      text(conn, "XP Dispatched")
×
164
    else
NEW
165
      text(conn, "No voting questions found for the given assessment")
×
166
    end
167
  end
168

169
  defp check_dates(open_at, close_at, assessment) do
170
    if is_nil(open_at) and is_nil(close_at) do
9✔
171
      {:ok, assessment}
172
    else
173
      formatted_open_date = elem(DateTime.from_iso8601(open_at), 1)
5✔
174
      formatted_close_date = elem(DateTime.from_iso8601(close_at), 1)
5✔
175

176
      if Timex.before?(formatted_close_date, formatted_open_date) do
5✔
177
        {:error, {:bad_request, "New end date should occur after new opening date"}}
178
      else
179
        assessment = Map.put(assessment, :open_at, formatted_open_date)
4✔
180
        assessment = Map.put(assessment, :close_at, formatted_close_date)
4✔
181
        {:ok, assessment}
182
      end
183
    end
184
  end
185

186
  defp is_same_course(course_id, assessment_id) do
187
    Assessment
188
    |> where(id: ^assessment_id)
189
    |> where(course_id: ^course_id)
2✔
190
    |> Repo.exists?()
2✔
191
  end
192

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

196
    summary("Fetches assessment overviews of a user")
197

198
    security([%{JWT: []}])
199

200
    parameters do
201
      courseRegId(:path, :integer, "Course Reg ID", required: true)
202
    end
203

204
    response(200, "OK", Schema.array(:AssessmentsList))
205
    response(401, "Unauthorised")
206
    response(403, "Forbidden")
207
  end
208

209
  swagger_path :create do
1✔
210
    post("/courses/{course_id}/admin/assessments")
211

212
    summary("Creates a new assessment or updates an existing assessment")
213

214
    security([%{JWT: []}])
215

216
    consumes("multipart/form-data")
217

218
    parameters do
219
      assessment(:formData, :file, "Assessment to create or update", required: true)
220
      forceUpdate(:formData, :boolean, "Force update", required: true)
221
    end
222

223
    response(200, "OK")
224
    response(400, "XML parse error")
225
    response(403, "Forbidden")
226
  end
227

228
  swagger_path :delete do
1✔
229
    PhoenixSwagger.Path.delete("/courses/{course_id}/admin/assessments/{assessmentId}")
230

231
    summary("Deletes an assessment")
232

233
    security([%{JWT: []}])
234

235
    parameters do
236
      assessmentId(:path, :integer, "Assessment ID", required: true)
237
    end
238

239
    response(200, "OK")
240
    response(403, "Forbidden")
241
  end
242

243
  swagger_path :update do
1✔
244
    post("/courses/{course_id}/admin/assessments/{assessmentId}")
245

246
    summary("Updates an assessment")
247

248
    security([%{JWT: []}])
249

250
    consumes("application/json")
251

252
    parameters do
253
      assessmentId(:path, :integer, "Assessment ID", required: true)
254

255
      assessment(:body, Schema.ref(:AdminUpdateAssessmentPayload), "Updated assessment details",
256
        required: true
257
      )
258
    end
259

260
    response(200, "OK")
261
    response(401, "Assessment is already opened")
262
    response(403, "Forbidden")
263
  end
264

265
  swagger_path :get_popular_leaderboard do
×
266
    get("/courses/{course_id}/admin/assessments/:assessmentid/popularVoteLeaderboard")
267

268
    summary("get the top 10 contest entries based on popularity")
269

270
    security([%{JWT: []}])
271

272
    parameters do
273
      assessmentId(:path, :integer, "Assessment ID", required: true)
274
    end
275

276
    response(200, "OK", Schema.array(:Leaderboard))
277
    response(401, "Unauthorised")
278
    response(403, "Forbidden")
279
  end
280

281
  swagger_path :get_score_leaderboard do
×
282
    get("/courses/{course_id}/admin/assessments/:assessmentid/scoreLeaderboard")
283

284
    summary("get the top X contest entries based on score")
285

286
    security([%{JWT: []}])
287

288
    parameters do
289
      assessmentId(:path, :integer, "Assessment ID", required: true)
290
    end
291

292
    response(200, "OK", Schema.array(:Leaderboard))
293
    response(401, "Unauthorised")
294
    response(403, "Forbidden")
295
  end
296

297
  def swagger_definitions do
298
    %{
1✔
299
      # Schemas for payloads to modify data
300
      AdminUpdateAssessmentPayload:
301
        swagger_schema do
1✔
302
          properties do
1✔
303
            closeAt(:string, "Open date", required: false)
304
            openAt(:string, "Close date", required: false)
305
            isPublished(:boolean, "Whether the assessment is published", required: false)
306
            maxTeamSize(:number, "Max team size of the assessment", required: false)
1✔
307
          end
308
        end
309
    }
310
  end
311
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