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

ubccpsc / classy / c3613766-7c49-4a6d-8ad5-a549be905ad5

04 Apr 2025 02:16PM UTC coverage: 87.191% (+0.09%) from 87.102%
c3613766-7c49-4a6d-8ad5-a549be905ad5

push

circleci

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

Contribute 310's 24w2 changes upstream

1101 of 1340 branches covered (82.16%)

Branch coverage included in aggregate %.

3950 of 4453 relevant lines covered (88.7%)

35.93 hits per line

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

90.0
packages/portal/backend/src/controllers/AdminController.ts
1
import Config, { ConfigKey } from "@common/Config";
1✔
2
import Log from "@common/Log";
1✔
3

4
import { GradeReport } from "@common/types/ContainerTypes";
5
import {
6
        AutoTestDashboardTransport,
7
        AutoTestGradeTransport,
8
        AutoTestResultSummaryTransport,
9
        CourseTransport,
10
        DeliverableTransport,
11
        GradeTransport,
12
        ProvisionTransport,
13
        RepositoryTransport,
14
        StudentTransport,
15
        TeamTransport,
16
} from "@common/types/PortalTypes";
17
import Util from "@common/Util";
1✔
18
import { Factory } from "../Factory";
1✔
19
import { AuditLabel, Course, Deliverable, GitHubStatus, Grade, Person, PersonKind, Repository, Result, Team } from "../Types";
1✔
20
import { DatabaseController } from "./DatabaseController";
1✔
21
import { DeliverablesController } from "./DeliverablesController";
1✔
22
import { GitHubActions } from "./GitHubActions";
1✔
23
import { GitHubController, IGitHubController } from "./GitHubController";
1✔
24
import { GradesController } from "./GradesController";
1✔
25
import { PersonController } from "./PersonController";
1✔
26
import { RepositoryController } from "./RepositoryController";
1✔
27
import { ResultsController, ResultsKind } from "./ResultsController";
1✔
28
import { TeamController } from "./TeamController";
1✔
29

30
export class AdminController {
1✔
31
        protected dbc = DatabaseController.getInstance();
55✔
32
        protected pc = new PersonController();
55✔
33
        protected rc = new RepositoryController();
55✔
34
        protected tc = new TeamController();
55✔
35
        protected gc = new GradesController();
55✔
36
        protected resC = new ResultsController();
55✔
37
        // protected cc: ICourseController;
38
        protected gh: IGitHubController = null;
55✔
39

40
        constructor(ghController: IGitHubController) {
41
                Log.trace("AdminController::<init>");
55✔
42
                this.gh = ghController;
55✔
43
        }
44

45
        /**
46
         * Returns the name for this instance. Not defensive: If name is null or something goes wrong there will be errors all over.
47
         *
48
         * @returns {string | null}
49
         */
50
        public static getName(): string | null {
51
                return Config.getInstance().getProp(ConfigKey.name);
1✔
52
        }
53

54
        /**
55
         * Validates the CourseTransport object.
56
         *
57
         * @param {CourseTransport} courseTrans
58
         * @returns {string | null} null if object is valid; string description of error if not.
59
         */
60
        public static validateCourseTransport(courseTrans: CourseTransport): string | null {
61
                if (typeof courseTrans === "undefined" || courseTrans === null) {
6✔
62
                        const msg = "Course not populated.";
1✔
63
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
64
                        throw new Error(msg);
1✔
65
                }
66

67
                // noinspection SuspiciousTypeOfGuard
68
                if (typeof courseTrans.id !== "string") {
5✔
69
                        const msg = "Course.id not specified";
1✔
70
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
71
                        throw new Error(msg);
1✔
72
                }
73

74
                // noinspection SuspiciousTypeOfGuard
75
                if (typeof courseTrans.defaultDeliverableId !== "string") {
4✔
76
                        const msg = "defaultDeliverableId not specified";
1✔
77
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
78
                        return msg;
1✔
79
                }
80

81
                // noinspection SuspiciousTypeOfGuard
82
                if (typeof courseTrans.custom !== "object") {
3✔
83
                        const msg = "custom not specified";
1✔
84
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
85
                        return msg;
1✔
86
                }
87

88
                return null;
2✔
89
        }
90

91
        /**
92
         * Returns null if the object is valid. This API is terrible.
93
         *
94
         * @param {ProvisionTransport} obj
95
         * @returns {ProvisionTransport | null}
96
         */
97
        public static validateProvisionTransport(obj: ProvisionTransport): ProvisionTransport | null {
98
                if (typeof obj === "undefined" || obj === null) {
5✔
99
                        const msg = "Transport not populated.";
1✔
100
                        Log.error("AdminController::validateProvisionTransport(..) - ERROR: " + msg);
1✔
101
                        throw new Error(msg);
1✔
102
                }
103

104
                // noinspection SuspiciousTypeOfGuard
105
                if (typeof obj.delivId !== "string") {
4✔
106
                        const msg = "Provision.id not specified";
1✔
107
                        Log.error("AdminController::validateProvisionTransport(..) - ERROR: " + msg);
1✔
108
                        throw new Error(msg);
1✔
109
                }
110

111
                // noinspection SuspiciousTypeOfGuard
112
                if (typeof obj.formSingle !== "boolean") {
3✔
113
                        const msg = "formSingle not specified";
1✔
114
                        Log.error("AdminController::validateProvisionTransport(..) - ERROR: " + msg);
1✔
115
                        throw new Error(msg);
1✔
116
                }
117

118
                return null;
2✔
119
        }
120

121
        /**
122
         * Processes the new autotest grade. Only returns true if the grade was accepted and saved.
123
         *
124
         * @param {AutoTestGradeTransport} grade
125
         * @returns {Promise<boolean>} Whether the new grade was saved
126
         */
127
        public async processNewAutoTestGrade(grade: AutoTestGradeTransport): Promise<boolean> {
128
                Log.trace("AdminController::processNewAutoTestGrade(..) - start");
4✔
129

130
                const cc = await Factory.getCourseController(this.gh);
4✔
131

132
                try {
4✔
133
                        Log.trace("AdminController::processNewAutoTestGrade(..) - payload: " + JSON.stringify(grade));
4✔
134
                        const repo = await this.rc.getRepository(grade.repoId);
4✔
135
                        if (repo === null) {
4✔
136
                                // sanity check
137
                                Log.error("AdminController::processNewAutoTestGrade(..) - invalid repo name: " + grade.repoId);
1✔
138
                                return false;
1✔
139
                        }
140

141
                        const peopleIds = await this.rc.getPeopleForRepo(grade.repoId);
3✔
142
                        if (peopleIds.length < 1) {
3!
143
                                // sanity check
144
                                Log.error("AdminController::processNewAutoTestGrade(..) - no people to associate grade record with.");
×
145
                                return false;
×
146
                        }
147

148
                        Log.trace("AdminController::processNewAutoTestGrade(..) - getting deliv"); // NOTE: for hangup debugging
3✔
149

150
                        const delivController = new DeliverablesController();
3✔
151
                        const deliv = await delivController.getDeliverable(grade.delivId);
3✔
152

153
                        let saved = false;
3✔
154

155
                        for (const personId of peopleIds) {
3✔
156
                                const newGrade: Grade = {
6✔
157
                                        personId: personId,
158
                                        delivId: grade.delivId,
159
                                        score: grade.score,
160
                                        comment: grade.comment,
161
                                        urlName: grade.urlName,
162
                                        URL: grade.URL,
163
                                        timestamp: grade.timestamp,
164
                                        custom: grade.custom,
165
                                };
166

167
                                Log.trace("AdminController::processNewAutoTestGrade(..) - getting grade for " + personId);
6✔
168
                                const existingGrade = await this.gc.getGrade(personId, grade.delivId);
6✔
169
                                const existingGradeScore = existingGrade?.score ? existingGrade.score : "N/A";
6✔
170
                                Log.trace(
6✔
171
                                        "AdminController::processNewAutoTestGrade(..) - handling grade for " +
172
                                                personId +
173
                                                "; repo: " +
174
                                                grade.repoId +
175
                                                "; existingGrade: " +
176
                                                existingGradeScore +
177
                                                "; newGrade: " +
178
                                                newGrade.score
179
                                );
180
                                const shouldSave = await cc.handleNewAutoTestGrade(deliv, newGrade, existingGrade);
6✔
181
                                // Log.trace("AdminController::processNewAutoTestGrade(..) - handled grade for " + personId +
182
                                //     "; shouldSave: " + shouldSave); // NOTE: for hangup debugging
183

184
                                Log.trace(
6✔
185
                                        "AdminController::processNewAutoTestGrade(..) - grade: " +
186
                                                JSON.stringify(newGrade) +
187
                                                "; repoId: " +
188
                                                grade.repoId +
189
                                                "; shouldSave: " +
190
                                                shouldSave
191
                                );
192

193
                                if (shouldSave === true) {
6✔
194
                                        Log.info(
4✔
195
                                                "AdminController::processNewAutoTestGrade(..) - saving grade for deliv: " +
196
                                                        newGrade.delivId +
197
                                                        "; repo: " +
198
                                                        grade.repoId
199
                                        );
200
                                        await this.dbc.writeAudit(AuditLabel.GRADE_AUTOTEST, "AutoTest", existingGrade, newGrade, { repoId: grade.repoId });
4✔
201
                                        await this.gc.saveGrade(newGrade);
4✔
202
                                        saved = true;
4✔
203
                                }
204
                        }
205
                        return saved;
3✔
206
                } catch (err) {
207
                        Log.error("AdminController::processNewAutoTestGrade(..) - ERROR: " + err);
×
208
                        return false;
×
209
                }
210
        }
211

212
        public async getCourse(): Promise<Course> {
213
                let record: Course = await this.dbc.getCourseRecord();
8✔
214
                if (record === null) {
8✔
215
                        // create default and write it
216
                        record = {
3✔
217
                                id: Config.getInstance().getProp(ConfigKey.name),
218
                                defaultDeliverableId: null,
219
                                custom: {},
220
                        };
221
                        await this.dbc.writeCourseRecord(record);
3✔
222
                }
223
                return record;
8✔
224
        }
225

226
        public async saveCourse(course: Course): Promise<boolean> {
227
                const record: Course = await this.dbc.getCourseRecord();
5✔
228
                if (record !== null) {
5!
229
                        // merge the new with the old
230
                        record.defaultDeliverableId = course.defaultDeliverableId;
5✔
231
                        record.custom = Object.assign({}, record.custom, course.custom); // merge custom properties
5✔
232
                }
233
                return await this.dbc.writeCourseRecord(record);
5✔
234
        }
235

236
        /**
237
         * Gets the students associated with the course. Admins, staff, and withdrawn students are not included.
238
         *
239
         * @returns {Promise<StudentTransport[]>}
240
         */
241
        public async getStudents(): Promise<StudentTransport[]> {
242
                const people = await this.pc.getAllPeople();
5✔
243

244
                const students: StudentTransport[] = [];
5✔
245
                for (const person of people) {
5✔
246
                        if (person.kind === PersonKind.STUDENT || person.kind === null) {
60✔
247
                                // null should be set on first login
248
                                const studentTransport = {
36✔
249
                                        id: person.id,
250
                                        firstName: person.fName,
251
                                        lastName: person.lName,
252
                                        githubId: person.githubId,
253
                                        userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + "/" + person.githubId,
254
                                        studentNum: person.studentNumber,
255
                                        labId: person.labId,
256
                                };
257
                                students.push(studentTransport);
36✔
258
                        }
259
                }
260
                return students;
5✔
261
        }
262

263
        /**
264
         * Gets the staff associated with the course.
265
         *
266
         * @returns {Promise<StudentTransport[]>}
267
         */
268
        public async getStaff(): Promise<StudentTransport[]> {
269
                const people = await this.pc.getAllPeople();
1✔
270

271
                const adminStaff: StudentTransport[] = [];
1✔
272
                for (const person of people) {
1✔
273
                        if (person.kind === PersonKind.ADMIN || person.kind === PersonKind.STAFF || person.kind === PersonKind.ADMINSTAFF) {
12✔
274
                                const isAdmin = person.kind === PersonKind.ADMIN || person.kind === PersonKind.ADMINSTAFF;
3✔
275
                                const isStaff = person.kind === PersonKind.STAFF || person.kind === PersonKind.ADMINSTAFF;
3✔
276

277
                                const studentTransport = {
3✔
278
                                        id: person.id,
279
                                        firstName: person.fName,
280
                                        lastName: person.lName,
281
                                        githubId: person.githubId,
282
                                        userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + "/" + person.githubId,
283
                                        studentNum: person.studentNumber,
284
                                        labId: person.labId,
285
                                        kind: person.kind,
286
                                        isAdmin,
287
                                        isStaff,
288
                                };
289
                                adminStaff.push(studentTransport);
3✔
290
                        }
291
                }
292
                return adminStaff;
1✔
293
        }
294

295
        /**
296
         * Gets the teams associated with the course.
297
         *
298
         * @returns {Promise<TeamTransport[]>}
299
         */
300
        public async getTeams(): Promise<TeamTransport[]> {
301
                const allTeams = await this.tc.getAllTeams();
2✔
302
                const teams: TeamTransport[] = [];
2✔
303
                for (const team of allTeams) {
2✔
304
                        const teamTransport: TeamTransport = {
4✔
305
                                id: team.id,
306
                                delivId: team.delivId,
307
                                people: team.personIds,
308
                                URL: team.URL,
309
                        };
310
                        teams.push(teamTransport);
4✔
311
                }
312
                return teams;
2✔
313
        }
314

315
        /**
316
         * Gets the repos associated with the course.
317
         *
318
         * @returns {Promise<RepositoryTransport[]>}
319
         */
320
        public async getRepositories(): Promise<RepositoryTransport[]> {
321
                const allRepos = await this.rc.getAllRepos();
2✔
322
                const repos: RepositoryTransport[] = [];
2✔
323
                for (const repo of allRepos) {
2✔
324
                        const repoTransport: RepositoryTransport = {
4✔
325
                                id: repo.id,
326
                                URL: repo.URL,
327
                                delivId: repo.delivId,
328
                        };
329
                        repos.push(repoTransport);
4✔
330
                }
331
                return repos;
2✔
332
        }
333

334
        /**
335
         * Gets the grades associated with the course.
336
         *
337
         * @returns {Promise<GradeTransport[]>}
338
         */
339
        public async getGrades(): Promise<GradeTransport[]> {
340
                Log.info("AdminController::getGrades() - start");
2✔
341
                const start = Date.now();
2✔
342
                const allGrades = await this.gc.getAllGrades();
2✔
343
                Log.trace("AdminController::getGrades() - getting grades took: " + Util.took(start));
2✔
344

345
                let part = Date.now();
2✔
346
                const grades: GradeTransport[] = [];
2✔
347
                const pc = new PersonController();
2✔
348
                const allPeople = await pc.getAllPeople(); // just make this query once
2✔
349
                Log.trace("AdminController::getGrades() - getting people took: " + Util.took(part));
2✔
350

351
                part = Date.now();
2✔
352
                for (const grade of allGrades) {
2✔
353
                        const p = allPeople.find((person) => person.id === grade.personId);
6✔
354
                        const gradeTrans: GradeTransport = {
4✔
355
                                personId: grade.personId,
356
                                personURL: Config.getInstance().getProp(ConfigKey.githubHost) + "/" + p.githubId,
357
                                delivId: grade.delivId,
358
                                score: grade.score,
359
                                comment: grade.comment,
360
                                urlName: grade.urlName,
361
                                URL: grade.URL,
362
                                timestamp: grade.timestamp,
363
                                custom: grade.custom,
364
                        };
365
                        grades.push(gradeTrans);
4✔
366
                }
367

368
                Log.trace("AdminController::getGrades() - post-processing took: " + Util.took(part));
2✔
369

370
                Log.info("AdminController::getGrades() - done; took: " + Util.took(start));
2✔
371
                return grades;
2✔
372
        }
373

374
        /**
375
         * Gets the results associated with the course.
376
         * @param reqDelivId ("any" for *)
377
         * @param reqRepoId ("any" for *)
378
         * @param maxNumResults (optional, default 500)
379
         * @param kind
380
         * @returns {Promise<AutoTestGradeTransport[]>}
381
         */
382
        public async getDashboard(
383
                reqDelivId: string,
384
                reqRepoId: string,
385
                maxNumResults?: number,
386
                kind: ResultsKind = ResultsKind.ALL
6✔
387
        ): Promise<AutoTestDashboardTransport[]> {
388
                Log.info("AdminController::getDashboard( " + reqDelivId + ", " + reqRepoId + ", " + maxNumResults + " ) - start");
8✔
389
                const start = Date.now();
8✔
390
                const NUM_RESULTS = maxNumResults ? maxNumResults : 500; // max # of records
8✔
391

392
                const repoIds: string[] = [];
8✔
393
                const results: AutoTestDashboardTransport[] = [];
8✔
394
                const allResults = await this.matchResults(reqDelivId, reqRepoId, kind);
8✔
395
                for (const result of allResults) {
8✔
396
                        const repoId = result.input.target.repoId;
88✔
397
                        if (results.length < NUM_RESULTS) {
88✔
398
                                const resultTrans = await this.createDashboardTransport(result);
83✔
399
                                // just return the first result for a repo, unless they are specified
400
                                if (reqRepoId !== "any" || repoIds.indexOf(repoId) < 0) {
83✔
401
                                        results.push(resultTrans);
32✔
402
                                        repoIds.push(repoId);
32✔
403
                                }
404
                        } else {
405
                                // result does not match filter
406
                        }
407
                }
408
                Log.info("AdminController::getDashboard(..) - # results: " + results.length + "; took: " + Util.took(start));
8✔
409
                return results;
8✔
410
        }
411

412
        public async matchResults(reqDelivId: string, reqRepoId: string, kind: ResultsKind): Promise<Result[]> {
413
                Log.trace("AdminController::matchResults(..) - start");
12✔
414
                const start = Date.now();
12✔
415
                const WILDCARD = "any";
12✔
416

417
                let allResults: Result[];
418
                if (reqRepoId !== WILDCARD) {
12✔
419
                        // if both are not "any" just use this one too
420
                        // ResultsKind not supported for getAllResults(..)
421
                        allResults = await this.resC.getResultsForRepo(reqRepoId);
5✔
422
                } else if (reqDelivId !== WILDCARD) {
7✔
423
                        allResults = await this.resC.getResultsForDeliverable(reqDelivId, kind);
2✔
424
                } else {
425
                        // ResultsKind not supported for getAllResults(..)
426
                        allResults = await this.resC.getAllResults();
5✔
427
                }
428
                Log.trace("AdminController::matchResults(..) - search done; # results: " + allResults.length + "; took: " + Util.took(start));
12✔
429

430
                const NUM_RESULTS = 1000;
12✔
431

432
                const results: Result[] = [];
12✔
433
                for (const result of allResults) {
12✔
434
                        // const repo = await rc.getRepository(result.repoId); // this happens a lot and ends up being too slow
435
                        const delivId = result.delivId;
146✔
436
                        const repoId = result.input.target.repoId;
146✔
437

438
                        if (
146!
439
                                (reqDelivId === WILDCARD || delivId === reqDelivId) &&
519✔
440
                                (reqRepoId === WILDCARD || repoId === reqRepoId) &&
441
                                results.length <= NUM_RESULTS
442
                        ) {
443
                                results.push(result);
146✔
444
                        } else {
445
                                // result does not match filter
446
                        }
447
                }
448

449
                Log.trace("AdminController::matchResults(..) - done; # results: " + results.length + "; took: " + Util.took(start));
12✔
450
                return results;
12✔
451
        }
452

453
        /**
454
         * Gets the list of GitHub ids associated with the "students" team on GitHub
455
         * and marks them as PersonKind.WITHDRAWN. Does nothing if the students team
456
         * does not exist or is empty.
457
         *
458
         * @returns {Promise<string>} A message summarizing the outcome of the operation.
459
         */
460
        public async performStudentWithdraw(): Promise<string> {
461
                Log.info("AdminController::performStudentWithdraw() - start");
2✔
462
                const gha = GitHubActions.getInstance(true);
2✔
463
                // const tc = new TeamController();
464
                // const teamNum = await tc.getTeamNumber("students"); // await gha.getTeamNumber("students");
465
                // const registeredGithubIds = await gha.getTeamMembers(teamNum);
466
                const registeredGithubIds = await gha.getTeamMembers("students");
2✔
467

468
                if (registeredGithubIds.length > 0) {
2!
469
                        const pc = new PersonController();
2✔
470
                        const msg = await pc.markStudentsWithdrawn(registeredGithubIds);
2✔
471
                        Log.info("AdminController::performStudentWithdraw() - done; msg: " + msg);
2✔
472
                        return msg;
2✔
473
                } else {
474
                        throw new Error("No students specified in the students team on GitHub; operation aborted.");
×
475
                }
476
        }
477

478
        /**
479
         * Gets the results associated with the course.
480
         * @param reqDelivId ("any" for *)
481
         * @param reqRepoId ("any" for *)
482
         * @param kind
483
         * @returns {Promise<AutoTestGradeTransport[]>}
484
         */
485
        public async getResults(
486
                reqDelivId: string,
487
                reqRepoId: string,
488
                kind: ResultsKind = ResultsKind.ALL
4✔
489
        ): Promise<AutoTestResultSummaryTransport[]> {
490
                Log.info("AdminController::getResults( " + reqDelivId + ", " + reqRepoId + ", " + kind + " ) - start");
4✔
491
                const start = Date.now();
4✔
492
                const NUM_RESULTS = 1000; // max # of records
4✔
493

494
                const results: AutoTestResultSummaryTransport[] = [];
4✔
495
                const allResults = await this.matchResults(reqDelivId, reqRepoId, kind);
4✔
496
                for (const result of allResults) {
4✔
497
                        // const repo = await rc.getRepository(result.repoId); // this happens a lot and ends up being too slow
498
                        // const repoId = result.input.target.repoId;
499
                        if (results.length <= NUM_RESULTS) {
58!
500
                                const resultTrans = await this.clipAutoTestResult(result);
58✔
501
                                results.push(resultTrans);
58✔
502
                        } else {
503
                                // result does not match filter
504
                        }
505
                }
506
                Log.info(
4✔
507
                        "AdminController::getResults( " +
508
                                reqDelivId +
509
                                ", " +
510
                                reqRepoId +
511
                                ", " +
512
                                kind +
513
                                ") - done; # results: " +
514
                                results.length +
515
                                "; took: " +
516
                                Util.took(start)
517
                );
518
                return results;
4✔
519
        }
520

521
        /**
522
         * Gets the deliverables associated with the course.
523
         *
524
         * @returns {Promise<DeliverableTransport[]>}
525
         */
526
        public async getDeliverables(): Promise<DeliverableTransport[]> {
527
                const deliverables = await this.dbc.getDeliverables();
4✔
528
                const start = Date.now();
4✔
529
                Log.trace("AdminController::getDeliverables() - start");
4✔
530

531
                let delivs: DeliverableTransport[] = [];
4✔
532
                for (const deliv of deliverables) {
4✔
533
                        const delivTransport = DeliverablesController.deliverableToTransport(deliv);
20✔
534

535
                        delivs.push(delivTransport);
20✔
536
                }
537

538
                delivs = delivs.sort(function (d1: DeliverableTransport, d2: DeliverableTransport) {
4✔
539
                        return d1.id.localeCompare(d2.id);
16✔
540
                });
541

542
                Log.trace("AdminController::getDeliverables() - done; # delivs: " + delivs.length + "; took: " + Util.took(start));
4✔
543
                return delivs;
4✔
544
        }
545

546
        /**
547
         * This plans the repo provisioning process. Planning is separated from doing so
548
         * that course staff can look at the repos being proposed and have the opportunity
549
         * to provision a subset of repos if they wish (e.g., for testing before creating
550
         * all of them).
551
         *
552
         * @param {Deliverable} deliv
553
         * @param {boolean} formSingleTeams specify whether singletons should be allocated into teams.
554
         * Choose false if you want to wait for the students to specify, choose true if you want to
555
         * let them work individually. (Note: if your teams are of max size 1, you still need to say
556
         * yes to make this happen.)
557
         *
558
         * @returns {Promise<RepositoryTransport[]>}
559
         */
560
        public async planProvision(deliv: Deliverable, formSingleTeams: boolean): Promise<RepositoryTransport[]> {
561
                Log.info("AdminController::planProvision( " + deliv.id + ", " + formSingleTeams + " ) - start");
5✔
562
                const cc = await Factory.getCourseController(this.gh);
5✔
563

564
                let allPeople: Person[] = await this.pc.getAllPeople();
5✔
565
                Log.info("AdminController::planProvision( .. ) - # people (all): " + allPeople.length);
5✔
566

567
                // remove all withdrawn people, we do not need to provision these
568
                allPeople = allPeople.filter((person) => person.kind !== PersonKind.WITHDRAWN);
33✔
569
                Log.info("AdminController::planProvision( .. ) - # people (not withdrawn): " + allPeople.length);
5✔
570

571
                // teams were either formed by students (or the admin in the UI)
572
                // _or_ the deliv is for single students and we will form them below
573
                let allTeams: Team[] = await this.tc.getAllTeams();
5✔
574
                Log.info("AdminController::planProvision( .. ) - # teams: " + allTeams.length);
5✔
575

576
                // just for logging, will remove with filter below
577
                for (const team of allTeams) {
5✔
578
                        if (team.personIds.length < 1) {
9!
579
                                Log.warn("AdminController::planProvision(..) - team has no people: " + team.id);
×
580
                        }
581
                }
582

583
                // remove teams that have no people
584
                allTeams = allTeams.filter((team) => team.personIds.length > 0);
9✔
585
                Log.info("AdminController::planProvision(..) - # teams after removing teams without people: " + allTeams.length);
5✔
586

587
                if (deliv.teamMaxSize === 1) {
5✔
588
                        formSingleTeams = true;
3✔
589
                        Log.info("AdminController::planProvision(..) - team maxSize 1: formSingleTeams forced to true");
3✔
590
                } else {
591
                        Log.info("AdminController::planProvision(..) - team maxSize > 1: formSingleTeams not forced");
2✔
592
                }
593

594
                const delivTeams: Team[] = [];
5✔
595
                for (const team of allTeams) {
5✔
596
                        if (team === null || deliv === null || team.id === null || deliv.id === null) {
9!
597
                                // seeing this during 310 provisioning, need to figure this out
598
                                Log.error(
×
599
                                        "AdminController::planProvision(..) - ERROR! null team: " + JSON.stringify(team) + " or deliv: " + JSON.stringify(deliv)
600
                                );
601
                        } else {
602
                                if (team.delivId === deliv.id) {
9✔
603
                                        Log.info("AdminController::planProvision(..) - adding team: " + team.id + " to delivTeams");
5✔
604
                                        delivTeams.push(team);
5✔
605
                                }
606
                        }
607
                }
608
                Log.info("AdminController::planProvision(..) - # deliv teams: " + delivTeams.length);
5✔
609

610
                // remove any people who are already on teams
611
                for (const team of delivTeams) {
5✔
612
                        for (const personId of team.personIds) {
5✔
613
                                const index = allPeople
7✔
614
                                        .map(function (p: Person) {
615
                                                return p.id;
18✔
616
                                        })
617
                                        .indexOf(personId);
618
                                if (index >= 0) {
7!
619
                                        Log.info("AdminController::planProvision(..) - person already on team: " + personId + " ( team: " + team.id + " )");
7✔
620
                                        allPeople.splice(index, 1);
7✔
621
                                } else {
622
                                        Log.warn("AdminController::planProvision(..) - allPeople does not contain: " + personId);
×
623
                                        const person = await this.pc.getPerson(personId);
×
624
                                        if (person !== null) {
×
625
                                                Log.warn("AdminController::planProvision(..) - person details: " + JSON.stringify(person));
×
626
                                        } else {
627
                                                Log.warn("AdminController::planProvision(..) - person is not in database");
×
628
                                        }
629
                                }
630
                        }
631
                }
632
                Log.trace("AdminController::planProvision(..) - # people not on teams: " + allPeople.length);
5✔
633

634
                if (formSingleTeams === true) {
5✔
635
                        // now create teams for individuals
636
                        Log.info("AdminController::planProvision(..) - handling single teams");
3✔
637
                        for (const individual of allPeople) {
3✔
638
                                try {
5✔
639
                                        const name = await cc.computeNames(deliv, [individual]);
5✔
640
                                        const team = await this.tc.formTeam(name.teamName, deliv, [individual], false);
5✔
641
                                        delivTeams.push(team);
5✔
642
                                } catch (err) {
643
                                        Log.error("AdminController::planProvision(..) - single team creation ERROR: " + err.message);
×
644
                                }
645
                        }
646
                        Log.info("AdminController::planProvision(..) - single teams done");
3✔
647
                }
648

649
                Log.info("AdminController::planProvision(..) - # delivTeams after individual teams added: " + delivTeams.length);
5✔
650

651
                const reposToProvision: Repository[] = [];
5✔
652
                // now process the teams to create their repos
653
                for (const delivTeam of delivTeams) {
5✔
654
                        Log.info("AdminController::planProvision(..) - preparing to provision team: " + delivTeam.id);
10✔
655

656
                        const people: Person[] = [];
10✔
657
                        for (const pId of delivTeam.personIds) {
10✔
658
                                people.push(await this.pc.getPerson(pId));
12✔
659
                        }
660
                        Log.trace("AdminController::planProvision(..) - preparing to provision pIds: " + JSON.stringify(delivTeam.personIds));
10✔
661
                        if (delivTeam.personIds.length !== people.length) {
10!
662
                                Log.warn("AdminController::planProvision(..) - preparing to provision missing people; people: " + JSON.stringify(people));
×
663
                        }
664

665
                        const names = await cc.computeNames(deliv, people);
10✔
666

667
                        Log.info(
10✔
668
                                "AdminController::planProvision(..) - delivTeam: " +
669
                                        delivTeam.id +
670
                                        "; computed team: " +
671
                                        names.teamName +
672
                                        "; computed repo: " +
673
                                        names.repoName
674
                        );
675

676
                        const team = await this.tc.getTeam(names.teamName);
10✔
677
                        let repo = await this.rc.getRepository(names.repoName);
10✔
678

679
                        if (team === null) {
10!
680
                                // sanity checking team must not be null given what we have done above (should never happen)
681
                                throw new Error("AdminController::planProvision(..) - team unexpectedly null: " + name); // s.teamName);
×
682
                        }
683

684
                        if (repo === null) {
10✔
685
                                repo = await this.rc.createRepository(names.repoName, deliv, [team], {});
7✔
686
                        }
687

688
                        if (repo === null) {
10!
689
                                // sanity checking repo must not be null given what we have done above (should never happen)
690
                                throw new Error("AdminController::planProvision(..) - repo unexpectedly null: " + names.repoName); // names.repoName);
×
691
                        }
692

693
                        // /* istanbul ignore if */
694
                        // if (typeof repo.custom.githubCreated !== "undefined" && repo.custom.githubCreated === true && repo.URL === null) {
695
                        //     // HACK: this is just for dealing with inconsistent databases
696
                        //     // This whole block should be removed in the future
697
                        //     Log.warn("AdminController::planProvision(..) - repo URL should not be null: " + repo.id);
698
                        //     const config = Config.getInstance();
699
                        //     repo.URL = config.getProp(ConfigKey.githubHost) + "/" + config.getProp(ConfigKey.org) + "/" + repo.id;
700
                        //     await this.dbc.writeRepository(repo);
701
                        // }
702

703
                        reposToProvision.push(repo);
10✔
704
                        Log.info("AdminController::planProvision(..) - team planning done for team: " + delivTeam.id);
10✔
705
                }
706

707
                Log.info("AdminController::planProvision(..) - # repos to provision: " + reposToProvision.length);
5✔
708

709
                const repoTrans: RepositoryTransport[] = [];
5✔
710
                for (const repo of reposToProvision) {
5✔
711
                        const newRepo = { delivId: deliv.id, id: repo.id, URL: repo.URL };
10✔
712
                        repoTrans.push(newRepo);
10✔
713
                }
714

715
                return repoTrans;
5✔
716
        }
717

718
        /**
719
         * Creates the GitHub side of the provided repositories. Only provisions those that
720
         * have not already been configured (e.g., their URL field is null).
721
         *
722
         * Does not release the repos to the students (e.g., the student team is not attached
723
         * to the repository; this should be done with performRelease). Released repos will
724
         * have their Team.URL fields set. e.g., creating the repo sets Repository.URL; releasing
725
         * the repo sets Team.URL (for the student teams associated with the repo).
726
         *
727
         * @param {Repository[]} repos
728
         * @param {string} importURL
729
         * @returns {Promise<Repository[]>}
730
         */
731
        public async performProvision(repos: Repository[], importURL: string): Promise<RepositoryTransport[]> {
732
                const gha = GitHubActions.getInstance(true);
4✔
733
                const ghc = new GitHubController(gha);
4✔
734
                const cc = await Factory.getCourseController(this.gh);
4✔
735

736
                const config = Config.getInstance();
4✔
737
                const dbc = DatabaseController.getInstance();
4✔
738

739
                Log.info("AdminController::performProvision(..) - start; # repos: " + repos.length + "; importURL: " + importURL);
4✔
740
                const provisionedRepos: Repository[] = [];
4✔
741

742
                const errors = [];
4✔
743

744
                for (const repo of repos) {
4✔
745
                        try {
8✔
746
                                const start = Date.now();
8✔
747
                                Log.info("AdminController::performProvision(..) ***** START *****; repo: " + repo.id);
8✔
748
                                // if (repo.URL === null) {
749
                                if (repo.gitHubStatus === GitHubStatus.NOT_PROVISIONED) {
8✔
750
                                        // key check: repo.URL is only set if the repo has been provisioned
751
                                        const futureTeams: Array<Promise<Team>> = repo.teamIds.map((teamId) => this.dbc.getTeam(teamId));
5✔
752
                                        const teams: Team[] = await Promise.all(futureTeams);
5✔
753
                                        Log.trace("AdminController::performProvision(..) - about to provision: " + repo.id);
5✔
754
                                        let success = await ghc.provisionRepository(repo.id, teams, importURL);
5✔
755
                                        success = success && (await cc.finalizeProvisionedRepo(repo, teams));
5✔
756
                                        Log.trace("AdminController::performProvision(..) - provisioned: " + repo.id + "; success: " + success);
5✔
757

758
                                        if (success === true) {
5!
759
                                                repo.URL = config.getProp(ConfigKey.githubHost) + "/" + config.getProp(ConfigKey.org) + "/" + repo.id;
5✔
760
                                                // repo.custom.githubCreated = true; // might not be necessary anymore; should just use repo.URL !== null
761
                                                repo.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED;
5✔
762
                                                await dbc.writeRepository(repo);
5✔
763
                                                Log.trace("AdminController::performProvision(..) - success: " + repo.id + "; URL: " + repo.URL);
5✔
764
                                                provisionedRepos.push(repo);
5✔
765
                                        } else {
766
                                                Log.warn("AdminController::performProvision(..) - provision FAILED: " + repo.id + "; URL: " + repo.URL);
×
767
                                        }
768

769
                                        // forced wait unnecessary with the transition to creating repo from template
770
                                        // Log.trace("AdminController::performProvision(..) - done provisioning: " + repo.id + "; forced wait");
771
                                        // await Util.delay(2 * 1000); // after any provisioning wait a bit
772
                                        // Log.info("AdminController::performProvision(..) - done for repo: " + repo.id + "; wait complete");
773
                                        Log.info("AdminController::performProvision(..) ***** DONE *****; repo: " + repo.id + "; took: " + Util.took(start));
5✔
774
                                } else {
775
                                        Log.info("AdminController::performProvision(..) - skipped; already provisioned: " + repo.id + "; URL: " + repo.URL);
3✔
776
                                }
777
                        } catch (err) {
778
                                Log.error("AdminController::performProvision(..) - FAILED: " + repo.id + "; URL: " + repo.URL + "; ERROR: " + err.message);
×
779
                                // would prefer not to rethrow, but the extra logging can be helpful
780
                                throw err;
×
781
                        }
782
                }
783

784
                const provisionedRepositoryTransport: RepositoryTransport[] = [];
4✔
785
                for (const repo of provisionedRepos) {
4✔
786
                        provisionedRepositoryTransport.push(RepositoryController.repositoryToTransport(repo));
5✔
787
                }
788
                return provisionedRepositoryTransport;
4✔
789
        }
790

791
        /**
792
         * Plans the releasing activity for attaching teams to their respective provisioned repositories.
793
         *
794
         * NOTE: this does _not_ provision the repos, or release them. It just creates a plan.
795
         *
796
         * @param {Deliverable} deliv
797
         * @returns {Promise<RepositoryTransport[]>}
798
         */
799
        public async planRelease(deliv: Deliverable): Promise<Repository[]> {
800
                Log.info("AdminController::planRelease( " + deliv.id + " ) - start");
2✔
801
                const cc = await Factory.getCourseController(this.gh);
2✔
802

803
                let allTeams: Team[] = await this.tc.getAllTeams();
2✔
804
                Log.trace("AdminController::planRelease( " + deliv.id + " ) - # teams: " + allTeams.length);
2✔
805

806
                // remove teams that have no people as they don't need to be released
807
                // just for logging, will remove with filter below
808
                for (const team of allTeams) {
2✔
809
                        if (team.personIds.length < 1) {
3!
810
                                Log.warn("AdminController::planRelease(..) - team has no people: " + team.id);
×
811
                        }
812
                }
813

814
                // remove teams that have no people
815
                allTeams = allTeams.filter((team) => team.personIds.length > 0);
3✔
816
                Log.info("AdminController::planRelease(..) - # teams after removing teams without people: " + allTeams.length);
2✔
817

818
                const delivTeams: Team[] = [];
2✔
819
                for (const team of allTeams) {
2✔
820
                        if (team === null || deliv === null || team.id === null || deliv.id === null) {
3!
821
                                // seeing this during 310 provisioning, need to figure this out
822
                                Log.error(
×
823
                                        "AdminController::planRelease(..) - ERROR! null team: " + JSON.stringify(team) + " or deliv: " + JSON.stringify(deliv)
824
                                );
825
                        } else {
826
                                if (team.delivId === deliv.id) {
3✔
827
                                        Log.trace("AdminController::planRelease(..) - adding team: " + team.id + " to delivTeams");
1✔
828
                                        delivTeams.push(team);
1✔
829
                                }
830
                        }
831
                }
832

833
                Log.info("AdminController::planRelease( " + deliv.id + " ) - # deliv teams: " + delivTeams.length);
2✔
834
                const reposToRelease: Repository[] = [];
2✔
835
                const reposAlreadyReleased: Repository[] = [];
2✔
836
                for (const team of delivTeams) {
2✔
837
                        try {
1✔
838
                                Log.trace("AdminController::planRelease( " + deliv.id + " ) - processing team: " + team.id);
1✔
839

840
                                // get repo for team
841
                                const people: Person[] = [];
1✔
842
                                for (const pId of team.personIds) {
1✔
843
                                        people.push(await this.dbc.getPerson(pId));
2✔
844
                                }
845
                                const names = await cc.computeNames(deliv, people);
1✔
846
                                const repo = await this.dbc.getRepository(names.repoName);
1✔
847

848
                                /* istanbul ignore else */
849
                                // if (typeof team.custom.githubAttached === "undefined" || team.custom.githubAttached === false) {
850
                                if (team.gitHubStatus === GitHubStatus.PROVISIONED_UNLINKED) {
1✔
851
                                        /* istanbul ignore else */
852
                                        // if (repo !== null && typeof repo.custom.githubCreated !== "undefined" && repo.custom.githubCreated === true) {
853
                                        if (repo !== null && repo.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
1✔
854
                                                // repo exists and has been provisioned: this is important as teams may have formed that have not been provisioned
855
                                                // aka only release provisioned repos
856
                                                reposToRelease.push(repo);
1✔
857
                                        } else {
858
                                                Log.info(
859
                                                        "AdminController::planRelease( " + deliv.id + " ) - repo not provisioned yet: " + JSON.stringify(team.personIds)
860
                                                );
861
                                        }
862
                                } else {
863
                                        Log.info("AdminController::planRelease( " + deliv.id + " ) - skipping team: " + team.id + "; already attached");
864
                                        reposAlreadyReleased.push(repo);
865
                                }
866
                        } catch (err) {
867
                                /* istanbul ignore next: curlies needed for ignore */
868
                                {
869
                                        Log.error("AdminController::planRelease(..) - ERROR: " + err.message);
870
                                        Log.exception(err);
871
                                }
872
                        }
873
                        Log.trace("AdminController::planRelease( " + deliv.id + " ) - done team processing: " + team.id);
1✔
874
                }
875

876
                Log.info("AdminController::planRelease( " + deliv.id + " ) - # repos in release plan: " + reposToRelease.length);
2✔
877

878
                // we want to know all repos whether they are released or not
879
                const allRepos: Repository[] = reposAlreadyReleased;
2✔
880
                for (const toReleaseRepo of reposToRelease) {
2✔
881
                        // toReleaseRepo.URL = null; // HACK, but denotes that it has not been released yet
882
                        toReleaseRepo.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED; // denotes that repo has not been released yet
1✔
883
                        allRepos.push(toReleaseRepo);
1✔
884
                }
885
                return allRepos;
2✔
886
        }
887

888
        public async performRelease(repos: Repository[]): Promise<RepositoryTransport[]> {
889
                const gha = GitHubActions.getInstance(true);
2✔
890
                const ghc = new GitHubController(gha);
2✔
891

892
                Log.info("AdminController::performRelease(..) - start; # repos: " + repos.length);
2✔
893
                const start = Date.now();
2✔
894

895
                const releasedRepos = [];
2✔
896
                for (const repo of repos) {
2✔
897
                        try {
2✔
898
                                const startRepo = Date.now();
2✔
899
                                // if (repo.URL !== null) {
900
                                // can only release repos that are provisioned
901
                                if (repo.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
2!
902
                                        const teams: Team[] = [];
2✔
903
                                        for (const teamId of repo.teamIds) {
2✔
904
                                                teams.push(await this.dbc.getTeam(teamId));
2✔
905
                                        }
906

907
                                        // actually release the repo
908
                                        const success = await ghc.releaseRepository(repo, teams, false);
2✔
909

910
                                        if (success === true) {
2!
911
                                                Log.info("AdminController::performRelease(..) - success: " + repo.id + "; took: " + Util.took(startRepo));
2✔
912
                                                releasedRepos.push(repo);
2✔
913
                                        } else {
914
                                                Log.warn("AdminController::performRelease(..) - FAILED: " + repo.id);
×
915
                                        }
916

917
                                        await Util.delay(200); // after any releasing wait a short bit
2✔
918
                                } else {
919
                                        Log.info("AdminController::performRelease(..) - skipped; repo not yet provisioned: " + repo.id); // + "; URL: " + repo.URL);
×
920
                                }
921
                        } catch (err) {
922
                                Log.error("AdminController::performRelease(..) - FAILED: " + repo.id + "; URL: " + repo.URL + "; ERROR: " + err.message);
×
923
                        }
924
                }
925

926
                const releasedRepositoryTransport: RepositoryTransport[] = [];
2✔
927
                for (const repo of releasedRepos) {
2✔
928
                        releasedRepositoryTransport.push(RepositoryController.repositoryToTransport(repo));
2✔
929
                }
930
                Log.info(
2✔
931
                        "AdminController::performRelease(..) - complete; # released: " +
932
                                releasedRepositoryTransport.length +
933
                                "; took: " +
934
                                Util.took(start)
935
                );
936

937
                return releasedRepositoryTransport;
2✔
938
        }
939

940
        public async makeReposReadOnly(deliv: Deliverable): Promise<RepositoryTransport[]> {
941
                Log.info("AdminController::makeReposReadOnly( " + deliv.id + " ) - start");
×
942
                return [];
×
943
        }
944

945
        public async makeReposWriteable(deliv: Deliverable): Promise<RepositoryTransport[]> {
946
                Log.info("AdminController::makeReposReadOnly( " + deliv.id + " ) - start");
×
947
                return [];
×
948
        }
949

950
        /* istanbul ignore next */
951
        /**
952
         * Synchronizes the database objects with GitHub. Does _NOT_ remove any DB objects, just makes
953
         * sure their properties match those in the GitHub org. This is useful if manual changes are made
954
         * to the org that you want to have updated in the repo as well.
955
         *
956
         * NOTE: team membership is _NOT_ currently read from GitHub and will not be synced.
957
         *
958
         * @param {boolean} dryRun
959
         * @returns {Promise<void>}
960
         */
961
        public async dbSanityCheck(dryRun: boolean): Promise<void> {
962
                Log.info("AdminController::dbSanityCheck() - start");
963
                const start = Date.now();
964

965
                const gha = GitHubActions.getInstance(true);
966
                const tc = new TeamController();
967
                const config = Config.getInstance();
968

969
                let repos = await this.dbc.getRepositories();
970
                for (const repo of repos) {
971
                        Log.info("AdminController::dbSanityCheck() - start; repo: " + repo.id);
972
                        const repoExists = await gha.repoExists(repo.id);
973
                        if (repoExists === true) {
974
                                // make sure repo is consistent
975
                                repo.URL = config.getProp(ConfigKey.githubHost) + "/" + config.getProp(ConfigKey.org) + "/" + repo.id;
976
                                // if (repo.custom.githubCreated !== true) {
977
                                //         Log.warn("AdminController::dbSanityCheck() - repo.custom.githubCreated should not be false for created: " + repo.id);
978
                                //         repo.custom.githubCreated = true;
979
                                // }
980
                                if (repo.gitHubStatus === GitHubStatus.NOT_PROVISIONED) {
981
                                        Log.warn("AdminController::dbSanityCheck() - gitHubStatus should be PROVISIONED for created: " + repo.id);
982
                                        repo.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED; // linking does not matter for repos
983
                                }
984
                        } else {
985
                                // if (repo.custom.githubCreated !== false) {
986
                                //         Log.warn("AdminController::dbSanityCheck() - repo.custom.githubCreated should not be true for !created: " + repo.id);
987
                                //         repo.custom.githubCreated = false; // does not exist, must not be created
988
                                // }
989
                                //
990
                                // if (repo.custom.githubReleased !== false) {
991
                                //         Log.warn("AdminController::dbSanityCheck() - repo.custom.githubReleased should not be true for !created: " + repo.id);
992
                                //         repo.custom.githubReleased = false; // does not exist, must not be released
993
                                // }
994

995
                                // repo does not exist
996
                                if (repo.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
997
                                        Log.warn("AdminController::dbSanityCheck() - gitHubStatus can only be NOT_PROVISIONED for !created: " + repo.id);
998
                                        repo.gitHubStatus = GitHubStatus.NOT_PROVISIONED;
999
                                }
1000

1001
                                if (repo.URL !== null) {
1002
                                        Log.warn("AdminController::dbSanityCheck() - repo.URL should be null for: " + repo.id);
1003
                                        repo.URL = null;
1004
                                }
1005
                        }
1006

1007
                        if (dryRun === false) {
1008
                                await this.dbc.writeRepository(repo);
1009
                        }
1010
                        Log.trace("AdminController::dbSanityCheck() - done; repo: " + repo.id);
1011
                }
1012

1013
                let teams = await tc.getAllTeams(); // not DBC because we want special teams filtered out
1014
                for (const team of teams) {
1015
                        Log.info("AdminController::dbSanityCheck() - start; team: " + team.id);
1016

1017
                        let teamNumber: number = -1;
1018
                        if (team.githubId !== null) {
1019
                                // use the cached team id if it exists and is correct (much faster)
1020
                                const tuple = await gha.getTeam(team.githubId);
1021
                                if (tuple !== null && tuple.githubTeamNumber === team.githubId && tuple.teamName === team.id) {
1022
                                        Log.info("AdminController::dbSanityCheck() - using cached gitHubId for team: " + team.id);
1023
                                        teamNumber = team.githubId;
1024
                                }
1025
                        }
1026

1027
                        if (teamNumber <= 0) {
1028
                                Log.info("AdminController::dbSanityCheck() - not using cached gitHubId for team: " + team.id);
1029
                                teamNumber = await gha.getTeamNumber(team.id);
1030
                        }
1031

1032
                        if (teamNumber >= 0) {
1033
                                if (team.githubId !== teamNumber) {
1034
                                        Log.warn("AdminController::dbSanityCheck() - team.githubId should match the GitHub id for: " + team.id);
1035
                                        team.githubId = teamNumber;
1036
                                }
1037
                        } else {
1038
                                if (team.githubId !== null) {
1039
                                        Log.warn("AdminController::dbSanityCheck() - team.githubId should be null: " + team.id);
1040
                                        team.githubId = null; // does not exist, must not have a number
1041
                                }
1042

1043
                                // if (team.custom.githubAttached !== false) {
1044
                                //         Log.warn("AdminController::dbSanityCheck() - team.custom.githubAttached should be false: " + team.id);
1045
                                //         team.custom.githubAttached = false; // does not exist, must not be attached
1046
                                // }
1047

1048
                                if (team.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
1049
                                        Log.warn("AdminController::dbSanityCheck() - team should not exist: " + team.id);
1050
                                        team.gitHubStatus = GitHubStatus.NOT_PROVISIONED; // does not exist, must not be attached
1051
                                }
1052
                        }
1053

1054
                        if (dryRun === false) {
1055
                                await this.dbc.writeTeam(team);
1056
                        }
1057
                        Log.trace("AdminController::dbSanityCheck() - done; team: " + team.id);
1058
                }
1059

1060
                repos = await this.dbc.getRepositories();
1061
                const checkedTeams: Team[] = [];
1062
                for (const repo of repos) {
1063
                        Log.info("AdminController::dbSanityCheck() - start; repo second pass: " + repo.id);
1064
                        let repoHasBeenChecked = false;
1065

1066
                        for (const teamId of repo.teamIds) {
1067
                                const team = await this.dbc.getTeam(teamId);
1068

1069
                                const teamsOnRepo = await gha.getTeamsOnRepo(repo.id);
1070
                                let isTeamOnRepo = false;
1071
                                for (const teamOnRepo of teamsOnRepo) {
1072
                                        if (teamOnRepo.teamName === teamId) {
1073
                                                // team is on repo
1074
                                                isTeamOnRepo = true;
1075
                                                repoHasBeenChecked = true;
1076
                                                checkedTeams.push(team);
1077
                                        }
1078
                                }
1079

1080
                                if (isTeamOnRepo === true) {
1081
                                        // if (repo.custom.githubReleased !== true) {
1082
                                        //         repo.custom.githubReleased = true;
1083
                                        //         Log.warn("AdminController::dbSanityCheck() - repo2.custom.githubReleased should be true: " + repo.id);
1084
                                        // }
1085

1086
                                        // if a team is on a repo, it must be provisioned and linked
1087
                                        if (repo.gitHubStatus !== GitHubStatus.PROVISIONED_LINKED) {
1088
                                                // repo.custom.githubReleased = true;
1089
                                                repo.gitHubStatus = GitHubStatus.PROVISIONED_LINKED;
1090
                                                Log.warn("AdminController::dbSanityCheck() - repo2.custom.githubReleased should be true: " + repo.id);
1091
                                        }
1092

1093
                                        // if (team.custom.githubAttached !== true) {
1094
                                        //         team.custom.githubAttached = true;
1095
                                        //         Log.warn("AdminController::dbSanityCheck() - team2.custom.githubAttached should be true: " + team.id);
1096
                                        // }
1097

1098
                                        if (team.gitHubStatus !== GitHubStatus.PROVISIONED_LINKED) {
1099
                                                team.gitHubStatus = GitHubStatus.PROVISIONED_LINKED;
1100
                                                Log.warn("AdminController::dbSanityCheck() - team.gitHubStatus should be PROVISIONED_LINKED: " + team.id);
1101
                                        }
1102

1103
                                        if (dryRun === false) {
1104
                                                await this.dbc.writeRepository(repo);
1105
                                                await this.dbc.writeTeam(team);
1106
                                        }
1107
                                }
1108
                        }
1109

1110
                        if (repoHasBeenChecked === false) {
1111
                                // repos that were not found to have teams must not be released
1112
                                // if (repo.custom.githubReleased !== false) {
1113
                                //         repo.custom.githubReleased = false; // was not found above, must be unreleased
1114
                                if (repo.gitHubStatus !== GitHubStatus.PROVISIONED_UNLINKED) {
1115
                                        repo.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED;
1116
                                        Log.warn("AdminController::dbSanityCheck() - repo.gitHubStatus should be PROVISIONED_UNLINKED: " + repo.gitHubStatus);
1117

1118
                                        if (dryRun === false) {
1119
                                                await this.dbc.writeRepository(repo);
1120
                                        }
1121
                                }
1122
                        }
1123
                }
1124

1125
                teams = await tc.getAllTeams(); // not DBC because we want special teams filtered out
1126
                for (const team of teams) {
1127
                        let checked = false;
1128
                        for (const checkedTeam of checkedTeams) {
1129
                                if (checkedTeam.id === team.id) {
1130
                                        checked = true;
1131
                                }
1132
                        }
1133
                        if (checked === false) {
1134
                                // teams that were not found with repos must not be attached
1135
                                // if (team.custom.githubAttached !== false) {
1136
                                //         team.custom.githubAttached = false;
1137
                                if (team.gitHubStatus !== GitHubStatus.PROVISIONED_UNLINKED) {
1138
                                        team.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED;
1139
                                        Log.warn("AdminController::dbSanityCheck() - team.gitHubStatus should be PROVISIONED_UNLINKED: " + team.id);
1140

1141
                                        if (dryRun === false) {
1142
                                                await this.dbc.writeTeam(team);
1143
                                        }
1144
                                }
1145
                        }
1146
                }
1147

1148
                Log.info("AdminController::dbSanityCheck() - done; took: " + Util.took(start));
1149
        }
1150

1151
        private async createDashboardTransport(result: Result): Promise<AutoTestDashboardTransport> {
1152
                const resultSummary = await this.clipAutoTestResult(result);
83✔
1153

1154
                let testPass: string[] = [];
83✔
1155
                let testFail: string[] = [];
83✔
1156
                let testSkip: string[] = [];
83✔
1157
                let testError: string[] = [];
83✔
1158

1159
                if (typeof result.output !== "undefined" && typeof result.output.report !== "undefined") {
83!
1160
                        const report: GradeReport = result.output.report;
83✔
1161
                        if (typeof report.passNames !== "undefined") {
83!
1162
                                testPass = report.passNames;
83✔
1163
                        }
1164
                        if (typeof report.failNames !== "undefined") {
83!
1165
                                testFail = report.failNames;
83✔
1166
                        }
1167
                        if (typeof report.skipNames !== "undefined") {
83!
1168
                                testSkip = report.skipNames;
83✔
1169
                        }
1170
                        if (typeof report.errorNames !== "undefined") {
83!
1171
                                testError = report.errorNames;
83✔
1172
                        }
1173
                }
1174

1175
                return {
83✔
1176
                        ...resultSummary,
1177
                        testPass: testPass,
1178
                        testFail: testFail,
1179
                        testError: testError,
1180
                        testSkip: testSkip,
1181
                        custom: {},
1182
                };
1183
        }
1184

1185
        /**
1186
         * Transforms a Result into an AutoTestResultSummaryTransport
1187
         */
1188
        private async clipAutoTestResult(result: Result): Promise<AutoTestResultSummaryTransport> {
1189
                const repoId = result.input.target.repoId;
141✔
1190
                const repoURL =
1191
                        Config.getInstance().getProp(ConfigKey.githubHost) + "/" + Config.getInstance().getProp(ConfigKey.org) + "/" + repoId;
141✔
1192

1193
                let scoreOverall = null;
141✔
1194
                let scoreCover = null;
141✔
1195
                let scoreTest = null;
141✔
1196

1197
                if (typeof result.output !== "undefined" && typeof result.output.report !== "undefined") {
141!
1198
                        const report = result.output.report;
141✔
1199
                        if (typeof report.scoreOverall !== "undefined") {
141!
1200
                                scoreOverall = report.scoreOverall;
141✔
1201
                        }
1202
                        if (typeof report.scoreTest !== "undefined") {
141!
1203
                                scoreTest = report.scoreTest;
141✔
1204
                        }
1205
                        if (typeof report.scoreCover !== "undefined") {
141!
1206
                                scoreCover = report.scoreCover;
141✔
1207
                        }
1208
                }
1209

1210
                const state = this.selectState(result);
141✔
1211

1212
                return {
141✔
1213
                        repoId: repoId,
1214
                        repoURL: repoURL,
1215
                        delivId: result.delivId,
1216
                        state: state,
1217
                        timestamp: result.output.timestamp,
1218
                        commitSHA: result.input.target.commitSHA,
1219
                        commitURL: result.input.target.commitURL,
1220
                        scoreOverall: scoreOverall,
1221
                        scoreCover: scoreCover,
1222
                        scoreTests: scoreTest,
1223
                        custom: {},
1224
                };
1225
        }
1226

1227
        // NOTE: the default implementation is currently broken; do not use it.
1228
        /**
1229
         * This is a method that subtypes can call from computeNames if they do not want to implement it themselves.
1230
         *
1231
         * @param {Deliverable} deliv
1232
         * @param {Person[]} people
1233
         * @returns {Promise<{teamName: string | null; repoName: string | null}>}
1234
         */
1235
        // public async computeNames(deliv: Deliverable, people: Person[]): Promise<{teamName: string | null, repoName: string | null}> {
1236
        //     Log.info("AdminController::computeNames(..) - start; # people: " + people.length);
1237
        //
1238
        //     // TODO: this code has a fatal flaw; if the team/repo exists already for the specified people,
1239
        //     // it is correct to return those.
1240
        //
1241
        //     let repoPrefix = "";
1242
        //     if (deliv.repoPrefix.length > 0) {
1243
        //         repoPrefix = deliv.repoPrefix;
1244
        //     } else {
1245
        //         repoPrefix = deliv.id;
1246
        //     }
1247
        //
1248
        //     let teamPrefix = "";
1249
        //     if (deliv.teamPrefix.length > 0) {
1250
        //         teamPrefix = deliv.teamPrefix;
1251
        //     } else {
1252
        //         teamPrefix = deliv.id;
1253
        //     }
1254
        //     // the repo name and the team name should be the same, so just use the repo name
1255
        //     const repos = await this.dbc.getRepositories();
1256
        //     let repoCount = 0;
1257
        //     for (const repo of repos) {
1258
        //         if (repo.id.startsWith(repoPrefix)) {
1259
        //             repoCount++;
1260
        //         }
1261
        //     }
1262
        //     let repoName = "";
1263
        //     let teamName = "";
1264
        //
1265
        //     let ready = false;
1266
        //     while (!ready) {
1267
        //         repoName = repoPrefix + "_" + repoCount;
1268
        //         teamName = teamPrefix + "_" + repoCount;
1269
        //         const r = await this.dbc.getRepository(repoName);
1270
        //         const t = await this.dbc.getTeam(teamName);
1271
        //         if (r === null && t === null) {
1272
        //             ready = true;
1273
        //         } else {
1274
        //             Log.warn("AdminController::computeNames(..) - name not available; r: " + repoName + "; t: " + teamName);
1275
        //             repoCount++; // try the next one
1276
        //         }
1277
        //     }
1278
        //     Log.info("AdminController::computeNames(..) - done; r: " + repoName + "; t: " + teamName);
1279
        //     return {teamName: teamName, repoName: repoName};
1280
        // }
1281

1282
        /**
1283
         * Takes a result, and if the VM was successful picks the state of the report.
1284
         *     else returns the state of the VM
1285
         * @param result
1286
         */
1287
        private selectState(result: Result): string {
1288
                // if the VM state is SUCCESS, return the report state
1289
                let state = "UNDEFINED";
141✔
1290
                if (typeof result.output !== "undefined" && typeof result.output.state !== "undefined") {
141!
1291
                        state = result.output.state.toString();
141✔
1292
                }
1293
                if (state === "SUCCESS" && typeof result.output.report.result !== "undefined") {
141!
1294
                        state = result.output.report.result;
141✔
1295
                }
1296
                return state;
141✔
1297
        }
1298
}
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