Skip to content

Commit 210d3ed

Browse files
committed
feat: add Common Core overview
1 parent 09bf0dc commit 210d3ed

19 files changed

Lines changed: 558 additions & 12 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
build/*
33
dist/*
44
node_modules/*
5-
./database
5+
database/*
66
*.db
77
*.db-journal
88
*/.DS_Store
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `cursus_id` to the `Project` table without a default value. This is not possible if the table is not empty. Thus the projects table gets emptied and a resync of all projects is required. This will be done automatically.
5+
6+
*/
7+
-- Drop all projects
8+
DELETE FROM "Project";
9+
10+
-- Force resync of all projects to update the new `cursus_id` field
11+
UPDATE "Synchronization" SET "last_synced_at" = NULL WHERE "kind" = 'projects';
12+
13+
-- AlterTable
14+
ALTER TABLE "Project" ADD COLUMN "cursus_id" INTEGER NOT NULL;
15+
16+
-- AddForeignKey
17+
ALTER TABLE "Project" ADD CONSTRAINT "Project_cursus_id_fkey" FOREIGN KEY ("cursus_id") REFERENCES "Cursus"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `difficulty` to the `Project` table without a default value. This is not possible if the table is not empty. Thus the projects table gets emptied and a resync of all projects is required. This will be done automatically.
5+
6+
*/
7+
-- Drop all projects
8+
DELETE FROM "Project";
9+
10+
-- Force resync of all projects to update the new `difficulty` field
11+
UPDATE "Synchronization" SET "last_synced_at" = NULL WHERE "kind" = 'projects';
12+
13+
-- AlterTable
14+
ALTER TABLE "Project" ADD COLUMN "difficulty" INTEGER NOT NULL;

prisma/schema.prisma

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ model Cursus {
4848
4949
// Relations
5050
cursus_users CursusUser[]
51+
projects Project[]
5152
}
5253

5354
model CursusUser {
@@ -91,12 +92,15 @@ model Project {
9192
slug String
9293
description String
9394
exam Boolean
95+
cursus_id Int @map("cursus_id")
96+
difficulty Int
9497
9598
updated_at DateTime
9699
created_at DateTime
97100
98101
// Relations
99102
project_users ProjectUser[]
103+
cursus Cursus @relation(fields: [cursus_id], references: [id])
100104
}
101105

102106
model ProjectUser {

src/handlers/authentication.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const setupPassport = function(prisma: PrismaClient): void {
5353
isStudentOrStaff: await isStudentOrStaff(user),
5454
isCatOrStaff: await isCatOrStaff(user),
5555
image_url: user.image,
56+
alumnized_at: user.alumnized_at,
5657
};
5758
cb(null, intraUser);
5859
});

src/handlers/cache.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import NodeCache from 'node-cache';
22

33
export const piscineCache = new NodeCache();
4+
export const coreCache = new NodeCache();
45

56
export const invalidateAllCache = function() {
67
piscineCache.flushAll();
8+
coreCache.flushAll();
79
};

src/handlers/core.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { PrismaClient } from "@prisma/client/extension";
2+
import { coreCache } from "./cache";
3+
import { Logtimes, Stat, UserListData } from "./userlist";
4+
import { getAllCohorts, getCommonCoreProjects, getTimeSpentBehindComputer, isCoreDropout } from "../utils";
5+
import { User } from "@prisma/client";
6+
import { SYNC_INTERVAL } from "../intra/base";
7+
import { COMMON_CORE_PROJECTS_ORDER } from "../intra/projects";
8+
9+
export interface CommonCoreLogtimes extends Logtimes {
10+
pastWeek: number;
11+
pastMonth: number;
12+
pastQuarter: number;
13+
};
14+
15+
export interface CommonCoreStat extends Stat {
16+
// No additions
17+
};
18+
19+
export interface CommonCoreData extends UserListData {
20+
alumni: { [login: string]: boolean };
21+
};
22+
23+
export const getCommonCoreCohortData = async function(prisma: PrismaClient, year: number, noCache: boolean = false): Promise<CommonCoreData> {
24+
// Check if the data is already in the cache
25+
const cacheKey = `core-${year}`;
26+
const cachedData = coreCache.get(cacheKey);
27+
if (!noCache && cachedData) {
28+
return cachedData as CommonCoreData;
29+
}
30+
31+
// Initiate statistics array
32+
const stats: CommonCoreStat[] = [];
33+
34+
// Find all users for the given year
35+
const users = await prisma.user.findMany({
36+
where: {
37+
login: {
38+
not: {
39+
startsWith: '3b3-',
40+
},
41+
},
42+
kind: {
43+
not: "admin",
44+
},
45+
cursus_users: {
46+
some: {
47+
cursus_id: 21,
48+
begin_at: {
49+
gte: new Date(`${year}-01-01`),
50+
lt: new Date(`${year + 1}-01-01`),
51+
},
52+
},
53+
},
54+
// Include dropouts and alumni for the year overview
55+
},
56+
include: {
57+
project_users: {
58+
where: {
59+
project: {
60+
cursus_id: 21,
61+
},
62+
},
63+
include: {
64+
project: true,
65+
},
66+
},
67+
cursus_users: {
68+
where: {
69+
cursus_id: 21,
70+
},
71+
},
72+
locations: {
73+
// Only the latest or current one
74+
take: 1,
75+
where: {
76+
primary: true,
77+
},
78+
orderBy: [
79+
{ begin_at: 'desc' },
80+
],
81+
},
82+
},
83+
orderBy: [
84+
{ usual_full_name: 'asc' }
85+
],
86+
});
87+
stats.push({
88+
label: 'Total students',
89+
value: users.length,
90+
unit: null,
91+
});
92+
93+
// Check for each student if they are a dropout
94+
let dropouts: { [login: string]: boolean } = {};
95+
for (const user of users) {
96+
dropouts[user.login] = isCoreDropout(user.cursus_users[0], user);
97+
}
98+
stats.push({
99+
label: 'Dropouts',
100+
value: Object.values(dropouts).filter(isDropout => isDropout).length,
101+
unit: null,
102+
});
103+
104+
// Check for each student if they are alumni
105+
let alumni: { [login: string]: boolean } = {};
106+
for (const user of users) {
107+
alumni[user.login] = user.alumnized_at !== null;
108+
}
109+
stats.push({
110+
label: 'Alumni',
111+
value: Object.values(alumni).filter(isAlumnus => isAlumnus).length,
112+
unit: null,
113+
});
114+
stats.push({
115+
label: 'Remaining',
116+
value: users.length - (Object.values(dropouts).filter(isDropout => isDropout).length + Object.values(alumni).filter(isAlumnus => isAlumnus).length),
117+
unit: null,
118+
});
119+
120+
// Sort users first by dropout status, then by name
121+
users.sort((a: User, b: User) => {
122+
if (dropouts[a.login] && !dropouts[b.login]) {
123+
return 1;
124+
}
125+
if (!dropouts[a.login] && dropouts[b.login]) {
126+
return -1;
127+
}
128+
return a.first_name.localeCompare(b.first_name) || a.last_name.localeCompare(b.last_name);
129+
});
130+
131+
// Get total logtime for each user
132+
let logtimes: { [login: string]: CommonCoreLogtimes } = {};
133+
for (const user of users) {
134+
const cursusStart = user.cursus_users[0].begin_at;
135+
const cursusEndOrNow = user.cursus_users[0].end_at || new Date();
136+
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
137+
const oneMonthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
138+
const threeMonthsAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
139+
const locations = await prisma.location.findMany({
140+
where: {
141+
user_id: user.id,
142+
begin_at: {
143+
gte: cursusStart,
144+
lte: cursusEndOrNow,
145+
},
146+
},
147+
orderBy: [
148+
{ begin_at: 'asc' },
149+
],
150+
});
151+
logtimes[user.login] = {
152+
total: getTimeSpentBehindComputer(locations, cursusStart, cursusEndOrNow),
153+
pastWeek: getTimeSpentBehindComputer(locations, oneWeekAgo, cursusEndOrNow),
154+
pastMonth: getTimeSpentBehindComputer(locations, oneMonthAgo, cursusEndOrNow),
155+
pastQuarter: getTimeSpentBehindComputer(locations, threeMonthsAgo, cursusEndOrNow),
156+
};
157+
}
158+
159+
const projects = await getCommonCoreProjects(prisma, COMMON_CORE_PROJECTS_ORDER);
160+
for (const user of users) {
161+
// Remove any projects that are not part of the common core
162+
user.project_users = user.project_users.filter((pu: any) => COMMON_CORE_PROJECTS_ORDER.includes(pu.project_id));
163+
164+
// Order each user's projects based on the order of project ids defined in COMMON_CORE_PROJECTS_ORDER
165+
user.project_users.sort((a: any, b: any) => {
166+
return COMMON_CORE_PROJECTS_ORDER.indexOf(a.project_id) - COMMON_CORE_PROJECTS_ORDER.indexOf(b.project_id);
167+
});
168+
}
169+
170+
// Cache the data for the remaining time of the sync interval
171+
coreCache.set(cacheKey, { users, stats, logtimes, dropouts, alumni, projects }, SYNC_INTERVAL * 60 * 1000);
172+
173+
return {
174+
users,
175+
stats,
176+
logtimes,
177+
dropouts,
178+
alumni,
179+
projects,
180+
} as CommonCoreData;
181+
}
182+
183+
export const buildCommonCoreCache = async function(prisma: PrismaClient) {
184+
const cohorts = await getAllCohorts(prisma);
185+
for (const cohort of cohorts) {
186+
console.debug(`Building cache for Cohort ${cohort.year}...`);
187+
await getCommonCoreCohortData(prisma, cohort.year_num, true);
188+
}
189+
};

src/handlers/disco.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface DiscoPiscineStat extends CPiscineStat {
2121

2222
export interface DiscoPiscineData extends UserListData {
2323
logtimes: { [login: string]: DiscoPiscineLogTimes };
24+
activeStudents: { [login: string]: boolean };
25+
potentialDropouts: { [login: string]: boolean };
2426
};
2527

2628
export const getDiscoPiscineData = async function(prisma: PrismaClient, year: number, week: number, cursus_id: number, noCache: boolean = false): Promise<DiscoPiscineData | null> {
@@ -284,7 +286,15 @@ export const getDiscoPiscineData = async function(prisma: PrismaClient, year: nu
284286
// Cache the data for the remaining time of the sync interval
285287
piscineCache.set(cacheKey, { users, stats, logtimes, dropouts, potentialDropouts, activeStudents, projects }, SYNC_INTERVAL * 60 * 1000);
286288

287-
return { users, stats, logtimes, dropouts, potentialDropouts, activeStudents, projects };
289+
return {
290+
users,
291+
stats,
292+
logtimes,
293+
dropouts,
294+
potentialDropouts,
295+
activeStudents,
296+
projects
297+
} as DiscoPiscineData;
288298
};
289299

290300
export const buildDiscoPiscineCache = async function(prisma: PrismaClient) {

src/handlers/piscine.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface CPiscineStat extends Stat{
1919

2020
export interface CPiscineData extends UserListData {
2121
logtimes: { [login: string]: CPiscineLogTimes };
22+
activeStudents: { [login: string]: boolean };
23+
potentialDropouts: { [login: string]: boolean };
2224
};
2325

2426
export const getCPiscineData = async function(prisma: PrismaClient, year: number, month: number, noCache: boolean = false): Promise<CPiscineData> {
@@ -265,7 +267,15 @@ export const getCPiscineData = async function(prisma: PrismaClient, year: number
265267
// Cache the data for the remaining time of the sync interval
266268
piscineCache.set(cacheKey, { users, stats, logtimes, dropouts, potentialDropouts, activeStudents, projects }, SYNC_INTERVAL * 60 * 1000);
267269

268-
return { users, stats, logtimes, dropouts, potentialDropouts, activeStudents, projects };
270+
return {
271+
users,
272+
stats,
273+
logtimes,
274+
dropouts,
275+
potentialDropouts,
276+
activeStudents,
277+
projects
278+
} as CPiscineData;
269279
};
270280

271281
export const buildCPiscineCache = async function(prisma: PrismaClient) {

src/handlers/userlist.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,5 @@ export interface UserListData {
1414
stats: Stat[];
1515
logtimes: { [login: string]: Logtimes };
1616
dropouts: { [login: string]: boolean };
17-
potentialDropouts: { [login: string]: boolean };
18-
activeStudents: { [login: string]: boolean };
1917
projects: any[];
2018
};

0 commit comments

Comments
 (0)