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

source-academy / backend / fa896845b1da00b20b6cde45c2ecb8b7eb75707e-PR-1346

18 Apr 2026 05:13AM UTC coverage: 88.503% (-0.4%) from 88.926%
fa896845b1da00b20b6cde45c2ecb8b7eb75707e-PR-1346

Pull #1346

github

geraldnyeo
split GET request for versions history into overview for multiple versions and content for a single version
Pull Request #1346: Versioning and History

108 of 143 new or added lines in 8 files covered. (75.52%)

104 existing lines in 1 file now uncovered.

3872 of 4375 relevant lines covered (88.5%)

6948.1 hits per line

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

71.76
/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
    is_autosave_enabled = params |> Map.get("isAutosaveEnabled")
9✔
90
    assign_entries_for_voting = params |> Map.get("assignEntriesForVoting")
9✔
91

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

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

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

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

120
    updated_assessment =
9✔
121
      if is_nil(is_autosave_enabled) do
122
        updated_assessment
9✔
123
      else
NEW
124
        Map.put(updated_assessment, :is_autosave_enabled, is_autosave_enabled)
×
125
      end
126

127
    is_reassigning_voting =
9✔
128
      if is_nil(assign_entries_for_voting) do
9✔
129
        false
130
      else
131
        assign_entries_for_voting
×
132
      end
133

134
    with {:ok, assessment} <- check_dates(open_at, close_at, updated_assessment),
9✔
135
         {:ok, _nil} <- Assessments.update_assessment(assessment_id, assessment),
8✔
136
         {:ok, _nil} <- Assessments.reassign_voting(assessment_id, is_reassigning_voting) do
8✔
137
      text(conn, "OK")
8✔
138
    else
139
      {:error, {status, message}} ->
140
        conn
141
        |> put_status(status)
142
        |> text(message)
1✔
143
    end
144
  end
145

146
  def calculate_contest_score(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
147
    voting_questions =
×
148
      Question
149
      |> where(type: :voting)
150
      |> where(assessment_id: ^assessment_id)
×
151
      |> Repo.one()
152

153
    if voting_questions do
×
154
      Assessments.compute_relative_score(voting_questions.id)
×
155
      text(conn, "Contest scores calculated")
×
156
    else
157
      text(conn, "No voting questions found for the given assessment")
×
158
    end
159
  end
160

161
  def dispatch_contest_xp(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
162
    voting_questions =
×
163
      Question
164
      |> where(type: :voting)
165
      |> where(assessment_id: ^assessment_id)
×
166
      |> Repo.one()
167

168
    if voting_questions do
×
169
      Assessments.assign_winning_contest_entries_xp(voting_questions.id)
×
170

171
      text(conn, "XP Dispatched")
×
172
    else
173
      text(conn, "No voting questions found for the given assessment")
×
174
    end
175
  end
176

177
  defp check_dates(open_at, close_at, assessment) do
178
    if is_nil(open_at) and is_nil(close_at) do
9✔
179
      {:ok, assessment}
180
    else
181
      formatted_open_date = elem(DateTime.from_iso8601(open_at), 1)
5✔
182
      formatted_close_date = elem(DateTime.from_iso8601(close_at), 1)
5✔
183

184
      if Timex.before?(formatted_close_date, formatted_open_date) do
5✔
185
        {:error, {:bad_request, "New end date should occur after new opening date"}}
186
      else
187
        assessment = Map.put(assessment, :open_at, formatted_open_date)
4✔
188
        assessment = Map.put(assessment, :close_at, formatted_close_date)
4✔
189
        {:ok, assessment}
190
      end
191
    end
192
  end
193

194
  defp is_same_course(course_id, assessment_id) do
195
    Assessment
196
    |> where(id: ^assessment_id)
197
    |> where(course_id: ^course_id)
2✔
198
    |> Repo.exists?()
2✔
199
  end
200

201
  swagger_path :index do
1✔
202
    get("/courses/{course_id}/admin/users/{courseRegId}/assessments")
203

204
    summary("Fetches assessment overviews of a user")
205

206
    security([%{JWT: []}])
207

208
    parameters do
209
      courseRegId(:path, :integer, "Course Reg ID", required: true)
210
    end
211

212
    response(200, "OK", Schema.array(:AssessmentsList))
213
    response(401, "Unauthorised")
214
    response(403, "Forbidden")
215
  end
216

217
  swagger_path :create do
1✔
218
    post("/courses/{course_id}/admin/assessments")
219

220
    summary("Creates a new assessment or updates an existing assessment")
221

222
    security([%{JWT: []}])
223

224
    consumes("multipart/form-data")
225

226
    parameters do
227
      assessment(:formData, :file, "Assessment to create or update", required: true)
228
      forceUpdate(:formData, :boolean, "Force update", required: true)
229
    end
230

231
    response(200, "OK")
232
    response(400, "XML parse error")
233
    response(403, "Forbidden")
234
  end
235

236
  swagger_path :delete do
1✔
237
    PhoenixSwagger.Path.delete("/courses/{course_id}/admin/assessments/{assessmentId}")
238

239
    summary("Deletes an assessment")
240

241
    security([%{JWT: []}])
242

243
    parameters do
244
      assessmentId(:path, :integer, "Assessment ID", required: true)
245
    end
246

247
    response(200, "OK")
248
    response(403, "Forbidden")
249
  end
250

251
  swagger_path :update do
1✔
252
    post("/courses/{course_id}/admin/assessments/{assessmentId}")
253

254
    summary("Updates an assessment")
255

256
    security([%{JWT: []}])
257

258
    consumes("application/json")
259

260
    parameters do
261
      assessmentId(:path, :integer, "Assessment ID", required: true)
262

263
      assessment(:body, Schema.ref(:AdminUpdateAssessmentPayload), "Updated assessment details",
264
        required: true
265
      )
266
    end
267

268
    response(200, "OK")
269
    response(401, "Assessment is already opened")
270
    response(403, "Forbidden")
271
  end
272

273
  swagger_path :get_popular_leaderboard do
×
274
    get("/courses/{course_id}/admin/assessments/:assessmentid/popularVoteLeaderboard")
275

276
    summary("get the top 10 contest entries based on popularity")
277

278
    security([%{JWT: []}])
279

280
    parameters do
281
      assessmentId(:path, :integer, "Assessment ID", required: true)
282
    end
283

284
    response(200, "OK", Schema.array(:Leaderboard))
285
    response(401, "Unauthorised")
286
    response(403, "Forbidden")
287
  end
288

289
  swagger_path :get_score_leaderboard do
×
290
    get("/courses/{course_id}/admin/assessments/:assessmentid/scoreLeaderboard")
291

292
    summary("get the top X contest entries based on score")
293

294
    security([%{JWT: []}])
295

296
    parameters do
297
      assessmentId(:path, :integer, "Assessment ID", required: true)
298
    end
299

300
    response(200, "OK", Schema.array(:Leaderboard))
301
    response(401, "Unauthorised")
302
    response(403, "Forbidden")
303
  end
304

305
  def swagger_definitions do
306
    %{
1✔
307
      # Schemas for payloads to modify data
308
      AdminUpdateAssessmentPayload:
309
        swagger_schema do
1✔
310
          properties do
1✔
311
            closeAt(:string, "Open date", required: false)
312
            openAt(:string, "Close date", required: false)
313
            isPublished(:boolean, "Whether the assessment is published", required: false)
314
            maxTeamSize(:number, "Max team size of the assessment", required: false)
1✔
315
          end
316
        end
317
    }
318
  end
319
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