• 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

98.57
/lib/cadet/accounts/teams.ex
1
defmodule Cadet.Accounts.Teams do
2
  @moduledoc """
3
  This module provides functions to manage teams in the Cadet system.
4
  """
5

6
  use Cadet, [:context, :display]
7
  use Ecto.Schema
8

9
  import Ecto.{Changeset, Query}
10

11
  alias Cadet.Repo
12
  alias Cadet.Accounts.{Team, TeamMember, CourseRegistration, Notification}
13
  alias Cadet.Assessments.{Answer, Assessment, Submission}
14

15
  @doc """
16
  Creates a new team and assigns an assessment and team members to it.
17

18
  ## Parameters
19

20
    * `attrs` - A map containing the attributes for assessment id and creating the team and its members.
21

22
  ## Returns
23

24
  Returns a tuple `{:ok, team}` on success; otherwise, an error tuple.
25

26
  """
27
  def create_team(attrs) do
28
    assessment_id = attrs["assessment_id"]
13✔
29
    teams = attrs["student_ids"]
13✔
30
    assessment = Cadet.Repo.get(Cadet.Assessments.Assessment, assessment_id)
13✔
31

32
    cond do
13✔
33
      !all_team_within_max_size?(teams, assessment.max_team_size) ->
13✔
34
        {:error, {:conflict, "One or more teams exceed the maximum team size!"}}
35

36
      !all_students_distinct?(teams) ->
12✔
37
        {:error, {:conflict, "One or more students appear multiple times in a team!"}}
38

39
      !all_student_enrolled_in_course?(teams, assessment.course_id) ->
10✔
40
        {:error, {:conflict, "One or more students not enrolled in this course!"}}
41

42
      student_already_assigned?(teams, assessment_id) ->
8✔
43
        {:error, {:conflict, "One or more students already in a team for this assessment!"}}
44

45
      true ->
7✔
46
        Enum.reduce_while(attrs["student_ids"], {:ok, nil}, fn team_attrs, {:ok, _} ->
7✔
47
          student_ids = Enum.map(team_attrs, &Map.get(&1, "userId"))
7✔
48

49
          {:ok, team} =
7✔
50
            %Team{}
51
            |> Team.changeset(attrs)
52
            |> Repo.insert()
53

54
          team_id = team.id
7✔
55

56
          Enum.each(team_attrs, fn student ->
7✔
57
            student_id = Map.get(student, "userId")
15✔
58
            attributes = %{student_id: student_id, team_id: team_id}
15✔
59

60
            %TeamMember{}
61
            |> cast(attributes, [:student_id, :team_id])
62
            |> Repo.insert()
15✔
63
          end)
64

65
          {:cont, {:ok, team}}
66
        end)
67
    end
68
  end
69

70
  @doc """
71
  Validates whether there are student(s) who are already assigned to another group.
72

73
  ## Parameters
74

75
    * `team_attrs` - A list of all the teams and their members.
76
    * `assessment_id` - Id of the target assessment.
77

78
  ## Returns
79

80
  Returns `true` on success; otherwise, `false`.
81

82
  """
83
  defp student_already_assigned?(team_attrs, assessment_id) do
84
    Enum.all?(team_attrs, fn team ->
8✔
85
      ids = Enum.map(team, &Map.get(&1, "userId"))
8✔
86

87
      unique_ids_count = ids |> Enum.uniq() |> Enum.count()
8✔
88
      all_ids_distinct = unique_ids_count == Enum.count(ids)
8✔
89

90
      student_already_in_team?(-1, ids, assessment_id)
8✔
91
    end)
92
  end
93

94
  @doc """
95
  Checks there is no duplicated student during team creation.
96

97
  ## Parameters
98

99
    * `team_attrs` - IDs of the team members being created
100

101
  ## Returns
102

103
  Returns `true` if all students in the list are distinct; otherwise, returns `false`.
104

105
  """
106
  defp all_students_distinct?(team_attrs) do
107
    all_ids =
12✔
108
      team_attrs
109
      |> Enum.flat_map(fn team ->
110
        Enum.map(team, fn row -> Map.get(row, "userId") end)
13✔
111
      end)
112

113
    all_ids_count = all_ids |> Enum.uniq() |> Enum.count()
12✔
114
    all_ids_distinct = all_ids_count == Enum.count(all_ids)
12✔
115

116
    all_ids_distinct
12✔
117
  end
118

119
  @doc """
120
  Checks if all the teams satisfy the max team size constraint.
121

122
  ## Parameters
123

124
    * `teams` - IDs of the team members being created
125
    * `max_team_size` - max team size of the team
126

127
  ## Returns
128

129
  Returns `true` if all the teams have size less or equal to the max team size; otherwise, returns `false`.
130

131
  """
132
  defp all_team_within_max_size?(teams, max_team_size) do
133
    Enum.all?(teams, fn team ->
13✔
134
      ids = Enum.map(team, &Map.get(&1, "userId"))
14✔
135
      length(ids) <= max_team_size
14✔
136
    end)
137
  end
138

139
  @doc """
140
  Checks if one or more students are enrolled in the course.
141

142
  ## Parameters
143

144
    * `teams` - ID of the team being created
145
    * `course_id` - ID of the course
146

147
  ## Returns
148

149
  Returns `true` if all students in the list enroll in the course; otherwise, returns `false`.
150

151
  """
152
  defp all_student_enrolled_in_course?(teams, course_id) do
153
    all_ids =
10✔
154
      teams
155
      |> Enum.flat_map(fn team ->
156
        Enum.map(team, fn row -> Map.get(row, "userId") end)
10✔
157
      end)
158

159
    query =
10✔
160
      from(cr in Cadet.Accounts.CourseRegistration,
161
        where: cr.id in ^all_ids and cr.course_id == ^course_id,
162
        select: count(cr.id)
163
      )
164

165
    count = Repo.one(query)
10✔
166
    count == length(all_ids)
10✔
167
  end
168

169
  @doc """
170
  Checks if one or more students are already in another team for the same assessment.
171

172
  ## Parameters
173

174
    * `team_id` - ID of the team being updated (use -1 for team creation)
175
    * `student_ids` - List of student IDs
176
    * `assessment_id` - ID of the assessment
177

178
  ## Returns
179

180
  Returns `true` if any student in the list is already a member of another team for the same assessment; otherwise, returns `false`.
181

182
  """
183
  defp student_already_in_team?(team_id, student_ids, assessment_id) do
184
    query =
10✔
185
      from(tm in TeamMember,
10✔
186
        join: t in assoc(tm, :team),
187
        where:
188
          tm.student_id in ^student_ids and t.assessment_id == ^assessment_id and t.id != ^team_id,
189
        select: tm.student_id
190
      )
191

192
    existing_student_ids = Repo.all(query)
10✔
193

194
    Enum.any?(student_ids, fn student_id -> Enum.member?(existing_student_ids, student_id) end)
10✔
195
  end
196

197
  @doc """
198
  Updates an existing team, the corresponding assessment, and its members.
199

200
  ## Parameters
201

202
    * `team` - The existing team to be updated
203
    * `new_assessment_id` - The ID of the updated assessment
204
    * `student_ids` - List of student ids for team members
205

206
  ## Returns
207

208
  Returns a tuple `{:ok, updated_team}` on success, containing the updated team details; otherwise, an error tuple.
209

210
  """
211
  def update_team(team = %Team{}, new_assessment_id, student_ids) do
212
    old_assessment_id = team.assessment_id
2✔
213
    team_id = team.id
2✔
214
    new_student_ids = Enum.map(hd(student_ids), fn student -> Map.get(student, "userId") end)
2✔
215

216
    if student_already_in_team?(team_id, new_student_ids, new_assessment_id) do
2✔
217
      {:error,
218
       {:conflict, "One or more students are already in another team for the same assessment!"}}
219
    else
220
      attrs = %{assessment_id: new_assessment_id}
1✔
221

222
      team
223
      |> cast(attrs, [:assessment_id])
224
      |> validate_required([:assessment_id])
225
      |> foreign_key_constraint(:assessment_id)
226
      |> Ecto.Changeset.change()
227
      |> Repo.update()
228
      |> case do
1✔
229
        {:ok, updated_team} ->
230
          update_team_members(updated_team, student_ids, team_id)
1✔
231
          {:ok, updated_team}
232
      end
233
    end
234
  end
235

236
  @doc """
237
  Updates team members based on the new list of student IDs.
238

239
  ## Parameters
240

241
    * `team` - The team being updated
242
    * `student_ids` - List of student ids for team members
243
    * `team_id` - ID of the team
244

245
  """
246
  defp update_team_members(team, student_ids, team_id) do
247
    current_student_ids = team.team_members |> Enum.map(& &1.student_id)
1✔
248
    new_student_ids = Enum.map(hd(student_ids), fn student -> Map.get(student, "userId") end)
1✔
249

250
    student_ids_to_add =
1✔
251
      Enum.filter(new_student_ids, fn elem -> not Enum.member?(current_student_ids, elem) end)
3✔
252

253
    student_ids_to_remove =
1✔
254
      Enum.filter(current_student_ids, fn elem -> not Enum.member?(new_student_ids, elem) end)
2✔
255

256
    Enum.each(student_ids_to_add, fn student_id ->
1✔
257
      %TeamMember{}
258
      # Change here
259
      |> Ecto.Changeset.change(%{team_id: team_id, student_id: student_id})
260
      |> Repo.insert()
1✔
261
    end)
262

263
    Enum.each(student_ids_to_remove, fn student_id ->
1✔
UNCOV
264
      Repo.delete_all(
×
265
        from(tm in TeamMember, where: tm.team_id == ^team_id and tm.student_id == ^student_id)
266
      )
267
    end)
268
  end
269

270
  @doc """
271
  Deletes a team along with its associated submissions and answers.
272

273
  ## Parameters
274

275
    * `team` - The team to be deleted
276

277
  """
278
  def delete_team(team = %Team{}) do
279
    if has_submitted_answer?(team.id) do
2✔
280
      {:error, {:conflict, "This team has submitted their answers! Unable to delete the team!"}}
281
    else
282
      submission =
1✔
283
        Submission
284
        |> where(team_id: ^team.id)
1✔
285
        |> Repo.one()
286

287
      if submission do
1✔
288
        Submission
289
        |> where(team_id: ^team.id)
1✔
290
        |> Repo.all()
291
        |> Enum.each(fn x ->
1✔
292
          Answer
293
          |> where(submission_id: ^x.id)
1✔
294
          |> Repo.delete_all()
1✔
295
        end)
296

297
        Notification
298
        |> where(submission_id: ^submission.id)
1✔
299
        |> Repo.delete_all()
1✔
300
      end
301

302
      team
303
      |> Repo.delete()
1✔
304
    end
305
  end
306

307
  @doc """
308
  Check whether a team has subnitted submissions and answers.
309

310
  ## Parameters
311

312
    * `team_id` - The team id of the team to be checked
313

314
  ## Returns
315

316
  Returns `true` if any one of the submission has the status of "submitted", `false` otherwise
317

318
  """
319
  defp has_submitted_answer?(team_id) do
320
    submission =
2✔
321
      Submission
322
      |> where([s], s.team_id == ^team_id and s.status == :submitted)
2✔
323
      |> Repo.all()
324

325
    length(submission) > 0
2✔
326
  end
327
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