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

ubccpsc / classy / d75e2632-84ef-47f0-968b-c513a1ce335b

12 Aug 2025 10:17PM UTC coverage: 88.309% (+1.1%) from 87.191%
d75e2632-84ef-47f0-968b-c513a1ce335b

push

circleci

web-flow
Merge pull request #480 from ubccpsc310/main

Merge 25w1 changes

1092 of 1321 branches covered (82.66%)

Branch coverage included in aggregate %.

3931 of 4367 relevant lines covered (90.02%)

37.09 hits per line

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

98.89
packages/portal/backend/src/controllers/TeamController.ts
1
import Log from "@common/Log";
1✔
2

3
import { TeamTransport } from "@common/types/PortalTypes";
4
import Util from "@common/Util";
1✔
5
import Config, { ConfigKey } from "@common/Config";
1✔
6

7
import { Deliverable, GitHubStatus, Person, PersonKind, Team } from "../Types";
1✔
8
import { DatabaseController } from "./DatabaseController";
1✔
9
import { GitHubActions, IGitHubActions } from "./GitHubActions";
1✔
10

11
export class TeamController {
1✔
12
        /**
13
         * A special GitHub team that contains all admins for a course (instructors, lead TAs).
14
         */
15
        public static readonly ADMIN_NAME = Config.getInstance().getProp(ConfigKey.adminTeamName) as string;
1✔
16

17
        /**
18
         * A special GitHub team for all other course staff (e.g., all TAs).
19
         */
20
        public static readonly STAFF_NAME = Config.getInstance().getProp(ConfigKey.staffTeamName) as string;
1✔
21

22
        /**
23
         * A special GitHub team that contains all students enrolled in a course.
24
         */
25
        public static readonly STUDENTS_NAME = "students";
1✔
26

27
        /**
28
         * A special GitHub team that contains all test students enrolled in a course.
29
         * This is used for staff to view Classy with a student view.
30
         */
31
        public static readonly TEST_STUDENTS_NAME = "students-test";
1✔
32

33
        private db: DatabaseController = DatabaseController.getInstance();
279✔
34
        private gha: IGitHubActions;
35

36
        constructor(gha?: IGitHubActions) {
37
                if (typeof gha === "undefined") {
279!
38
                        this.gha = GitHubActions.getInstance();
279✔
39
                } else {
40
                        this.gha = gha;
×
41
                }
42
        }
43

44
        /**
45
         * Returns all student teams.
46
         *
47
         * Special teams are _not_ returned.
48
         *
49
         * @returns {Promise<Team[]>}
50
         */
51
        public async getAllTeams(): Promise<Team[]> {
52
                Log.trace("TeamController::getAllTeams() - start");
48✔
53
                const start = Date.now();
48✔
54

55
                // remove special teams
56
                const teamsToReturn = [];
48✔
57
                const teams = await this.db.getTeams();
48✔
58
                for (const team of teams) {
48✔
59
                        if (
252✔
60
                                team.id === TeamController.ADMIN_NAME ||
720✔
61
                                team.id === TeamController.STAFF_NAME ||
62
                                team.id === TeamController.STUDENTS_NAME ||
63
                                team.id === TeamController.TEST_STUDENTS_NAME
64
                        ) {
65
                                // do not include
66
                        } else {
67
                                teamsToReturn.push(team);
108✔
68
                        }
69
                }
70

71
                Log.trace("TeamController::getAllTeams() - done; took: " + Util.took(start));
48✔
72
                return teamsToReturn;
48✔
73
        }
74

75
        public async getTeam(name: string): Promise<Team | null> {
76
                Log.trace("TeamController::getTeam( " + name + " ) - start");
105✔
77
                const start = Date.now();
105✔
78

79
                const team = await this.db.getTeam(name);
105✔
80
                Log.trace("TeamController::getTeam( " + name + " ) - done; took: " + Util.took(start));
105✔
81
                return team;
105✔
82
        }
83

84
        /**
85
         * Gets the GitHub team number.
86
         *
87
         * Returns null if the team does not exist on GitHub.
88
         *
89
         * @param {string} name
90
         * @returns {Promise<number | null>}
91
         */
92
        public async getTeamNumber(name: string): Promise<number | null> {
93
                Log.trace("TeamController::getTeamNumber( " + name + " ) - start");
39✔
94
                const start = Date.now();
39✔
95

96
                const team = await this.db.getTeam(name);
39✔
97
                if (team === null) {
39✔
98
                        Log.warn("TeamController::getTeamNumber( " + name + " ) - team does not exist in database");
2✔
99
                        return null;
2✔
100
                }
101

102
                if (typeof team.githubId === "undefined" || team.githubId === null) {
37✔
103
                        // teamId not known; get it & store it
104
                        let teamNum = null;
21✔
105
                        const ghTeam = await this.gha.getTeamByName(name);
21✔
106
                        // NOTE: this is using teamNumber as a provisioning hint
107
                        if (ghTeam !== null && ghTeam.githubTeamNumber >= 0) {
21✔
108
                                Log.warn("TeamController::getTeamNumber( " + name + " ) - team does not exist on GitHub; setting null.");
13✔
109
                                teamNum = ghTeam.githubTeamNumber;
13✔
110
                        }
111
                        // let teamNum: number | null = await this.gha.getTeamNumber(team.id);
112
                        // if (teamNum < 0) {
113
                        //     Log.warn("TeamController::getTeamNumber( " + name + " ) - team does not exist on GitHub; setting null.");
114
                        //     teamNum = null;
115
                        // }
116
                        team.githubId = teamNum;
21✔
117
                        await this.saveTeam(team);
21✔
118
                } else {
119
                        // githubId is set, should we check to see if it"s right?
120
                        // TODO: verify that the number is right?
121
                        // could do it with this: this.gha.getTeam(team.githubId)
122
                }
123

124
                Log.info("TeamController::getTeamNumber( " + name + " ) - done; took: " + Util.took(start));
37✔
125
                return team.githubId;
37✔
126
        }
127

128
        public async saveTeam(team: Team): Promise<Team> {
129
                Log.info("TeamController::saveTeam(..) - start");
22✔
130
                const dc = DatabaseController.getInstance();
22✔
131
                await dc.writeTeam(team);
22✔
132
                return team;
22✔
133
        }
134

135
        public async getTeamsForPerson(myPerson: Person): Promise<Team[]> {
136
                Log.trace("TeamController::getTeamsForPerson( " + myPerson.id + " ) - start");
26✔
137
                const start = Date.now();
26✔
138

139
                let myTeams: Team[] = [];
26✔
140
                const allTeams = await this.db.getTeams();
26✔
141
                for (const team of allTeams) {
26✔
142
                        if (team.personIds.indexOf(myPerson.id) >= 0) {
137✔
143
                                myTeams.push(team);
35✔
144
                        }
145
                }
146

147
                // sort by delivIds
148
                myTeams = myTeams.sort(function (a: Team, b: Team) {
26✔
149
                        return a.delivId.localeCompare(b.delivId);
17✔
150
                });
151

152
                Log.info("TeamController::getTeamsForPerson( " + myPerson.id + " ) - # teams: " + myTeams.length + "; took: " + Util.took(start));
26✔
153
                return myTeams;
26✔
154
        }
155

156
        /**
157
         * Convenience method for creating team objects when only primitive types are known. This is
158
         * especially useful for students specifying their own teams as it checks to ensure that team
159
         * constraints (specified in the deliverable) are adhered to. Once all checks pass, the code
160
         * passes through to TeamController::teamCreate(...).
161
         *
162
         * @param teamId
163
         * @param deliv
164
         * @param people
165
         * @param adminOverride
166
         * @returns {Promise<Team | null>}
167
         */
168
        public async formTeam(teamId: string, deliv: Deliverable, people: Person[], adminOverride: boolean): Promise<Team | null> {
169
                Log.info("TeamController::formTeam( " + teamId + ", ... ) - start; override: " + adminOverride);
17✔
170

171
                // sanity checking
172
                if (deliv === null) {
17✔
173
                        throw new Error("Team not created; deliverable does not exist.");
1✔
174
                }
175

176
                // check for non-existent people
177
                if (people.indexOf(null) >= 0) {
16✔
178
                        throw new Error("Team not created; some students not members of the course.");
1✔
179
                }
180

181
                // make sure the team is not too large
182
                if (people.length > deliv.teamMaxSize && !adminOverride) {
15✔
183
                        throw new Error("Team not created; too many team members specified for this deliverable.");
1✔
184
                }
185

186
                // make sure the team is not too small
187
                if (people.length < deliv.teamMinSize && !adminOverride) {
14✔
188
                        throw new Error("Team not created; too few team members specified for this deliverable.");
1✔
189
                }
190

191
                // make sure students can form their own teams
192
                if (deliv.teamMaxSize > 1) {
13✔
193
                        // only matters if the team size is grater than 1
194
                        if (deliv.teamStudentsForm === false && !adminOverride) {
6✔
195
                                throw new Error("Team not created; students cannot form their own teams for this deliverable.");
1✔
196
                        }
197
                }
198

199
                // make sure all students are still registered in the class
200
                for (const p of people) {
12✔
201
                        if (p.kind === PersonKind.WITHDRAWN && !adminOverride) {
18✔
202
                                throw new Error("Team not created; at least one student is not an active member of the class.");
1✔
203
                        }
204
                }
205

206
                // ensure members are all in the same lab section (if required)
207
                if (deliv.teamSameLab === true && !adminOverride) {
11✔
208
                        let labName = null;
10✔
209
                        for (const p of people) {
10✔
210
                                if (labName === null) {
14✔
211
                                        labName = p.labId;
10✔
212
                                }
213
                                if (labName !== p.labId) {
14✔
214
                                        Log.warn("TeamController::formTeam( ... ) - members not all in same lab ( " + labName + ", " + p.labId + " )");
1✔
215
                                        throw new Error("Team not created; all members are not in the same lab.");
1✔
216
                                }
217
                        }
218
                }
219

220
                // ensure members are not already on a team for that deliverable
221
                for (const person of people) {
10✔
222
                        const teamsForPerson = await this.getTeamsForPerson(person);
13✔
223
                        for (const existingTeam of teamsForPerson) {
13✔
224
                                if (existingTeam.delivId === deliv.id) {
13✔
225
                                        // NOTE: no adminOverride for this, this must be enforced
226
                                        Log.warn(
2✔
227
                                                "TeamController::formTeam( ... ) - member already on team: " + existingTeam.id + " for deliverable: " + deliv.id
228
                                        );
229
                                        if (
2✔
230
                                                existingTeam.personIds.length === people.length &&
4✔
231
                                                people.every((p) => existingTeam.personIds.includes(p.id)) &&
2✔
232
                                                existingTeam.id === teamId
233
                                        ) {
234
                                                // The team being made is the same as one that was already made, so return the old one
235
                                                return existingTeam;
1✔
236
                                        } else {
237
                                                throw new Error("Team not created; some members are already on existing teams for this deliverable.");
1✔
238
                                        }
239
                                }
240
                        }
241
                }
242

243
                const team = await this.createTeam(teamId, deliv, people, {});
8✔
244
                Log.info("TeamController::formTeam( " + teamId + ", ... ) - done");
8✔
245
                return team;
8✔
246
        }
247

248
        /**
249
         * Creates a team object.
250
         *
251
         * @param {string} name
252
         * @param {Deliverable} deliv
253
         * @param {Person[]} people
254
         * @param {any} custom
255
         * @returns {Promise<Team | null>}
256
         */
257
        public async createTeam(name: string, deliv: Deliverable, people: Person[], custom: any): Promise<Team | null> {
258
                Log.info("TeamController::teamCreate( " + name + ", ... ) - start");
52✔
259

260
                if (typeof name === "undefined" || name === null || name.length < 1) {
52✔
261
                        throw new Error("TeamController::teamCreate() - no team name provided.");
1✔
262
                }
263

264
                if (typeof deliv === "undefined" || deliv === null) {
51✔
265
                        throw new Error("TeamController::teamCreate() - no deliverable provided.");
1✔
266
                }
267

268
                // this constraint is not necessary
269
                // if (Array.isArray(people) === false || people.length < 1) {
270
                //     throw new Error("TeamController::teamCreate() - no people provided.");
271
                // }
272

273
                try {
50✔
274
                        const existingTeam = await this.getTeam(name);
50✔
275
                        if (existingTeam === null) {
50✔
276
                                const peopleIds: string[] = people.map((person) => person.id);
83✔
277

278
                                const team: Team = {
49✔
279
                                        id: name,
280
                                        delivId: deliv.id,
281
                                        githubId: null,
282
                                        gitHubStatus: GitHubStatus.NOT_PROVISIONED,
283
                                        URL: null,
284
                                        personIds: peopleIds,
285
                                        custom: custom,
286
                                        // repoName:  null,  // team counts above used repoName
287
                                        // repoUrl:   null
288
                                };
289
                                await this.db.writeTeam(team);
49✔
290

291
                                Log.info("TeamController::teamCreate( " + name + ", ... ) - done");
49✔
292
                                return await this.db.getTeam(name);
49✔
293
                        } else {
294
                                throw new Error("Duplicate team name: " + name);
1✔
295
                        }
296
                } catch (err) {
297
                        Log.error("TeamController::teamCreate() - ERROR: " + err.message);
1✔
298
                        throw err;
1✔
299
                }
300
        }
301

302
        /**
303
         * Serialize team object to a TeamTransport.
304
         *
305
         * @param team
306
         */
307
        public teamToTransport(team: Team): TeamTransport {
308
                return {
7✔
309
                        id: team.id,
310
                        delivId: team.delivId,
311
                        people: team.personIds,
312
                        URL: team.URL,
313
                        // repoName: team.repoName,
314
                        // repoUrl:  team.repoUrl
315
                };
316
        }
317
}
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