Skip to content

Commit 1fe1515

Browse files
committed
feat: add Discovery Piscine overviews
1 parent 0630b8b commit 1fe1515

File tree

15 files changed

+577
-81
lines changed

15 files changed

+577
-81
lines changed

src/handlers/disco.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { PrismaClient } from '@prisma/client';
2+
import { DISCO_PISCINE_AI_FUNDA_PROJECTS_ORDER, DISCO_PISCINE_AI_INTER_PROJECTS_ORDER, DISCO_PISCINE_CORE_PYTHON_PROJECTS_ORDER, DISCO_PISCINE_DEPR_PYTHON_PROJECTS_ORDER, DISCO_PISCINE_WEB_PRGM_ESS_PROJECTS_ORDER } from '../intra/projects';
3+
import { getPiscineProjects, getAllDiscoPiscines, getTimeSpentBehindComputer, isDiscoPiscineDropout } from '../utils';
4+
import { piscineCache } from './cache';
5+
import { SYNC_INTERVAL } from '../intra/base';
6+
import { DISCO_PISCINE_CURSUS_IDS } from '../intra/cursus';
7+
8+
export interface DiscoPiscineLogTimes {
9+
dayOne: number;
10+
dayTwo: number;
11+
dayThree: number;
12+
dayFour: number;
13+
dayFive: number;
14+
total: number;
15+
};
16+
17+
// TODO: define the types for the disco piscine data explicitly
18+
export interface DiscoPiscineData {
19+
users: any[];
20+
logtimes: { [login: string]: DiscoPiscineLogTimes };
21+
dropouts: { [login: string]: boolean };
22+
projects: any[];
23+
};
24+
25+
export const getDiscoPiscineData = async function(prisma: PrismaClient, year: number, week: number, cursus_id: number, noCache: boolean = false): Promise<DiscoPiscineData | null> {
26+
// Check if the data is already in the cache
27+
const cacheKey = `disco-${year}-${week}-${cursus_id}`;
28+
const cachedData = piscineCache.get(cacheKey);
29+
if (!noCache && cachedData) {
30+
return cachedData as DiscoPiscineData;
31+
}
32+
33+
// Find all possible piscines from the database
34+
const discopiscines = await getAllDiscoPiscines(prisma);
35+
36+
// Get the discovery piscine based on the year and week
37+
const discopiscine = discopiscines.find(p => p.year_num === year && p.week_num === week && p.cursus.id === cursus_id);
38+
if (!discopiscine) {
39+
console.log(`No discovery piscine found for year ${year}, week ${week} and cursus_id ${cursus_id}`);
40+
return null;
41+
}
42+
43+
// Get the project ids in order for the given discovery piscine cursus
44+
const piscineProjectIdsOrdered: number[] = [];
45+
switch (discopiscine.cursus.id) {
46+
case 77:
47+
piscineProjectIdsOrdered.push(...DISCO_PISCINE_AI_INTER_PROJECTS_ORDER);
48+
break;
49+
case 79:
50+
piscineProjectIdsOrdered.push(...DISCO_PISCINE_AI_FUNDA_PROJECTS_ORDER);
51+
break;
52+
case 80:
53+
piscineProjectIdsOrdered.push(...DISCO_PISCINE_CORE_PYTHON_PROJECTS_ORDER);
54+
break;
55+
case 69:
56+
piscineProjectIdsOrdered.push(...DISCO_PISCINE_DEPR_PYTHON_PROJECTS_ORDER);
57+
break;
58+
case 3:
59+
piscineProjectIdsOrdered.push(...DISCO_PISCINE_WEB_PRGM_ESS_PROJECTS_ORDER);
60+
break;
61+
default:
62+
console.warn(`Unknown discovery piscine cursus_id ${discopiscine.cursus.id}, no projects will be included.`);
63+
break;
64+
}
65+
66+
// Find all users with a discovery piscine that matches the end_at of the discopiscine in question
67+
const users = await prisma.user.findMany({
68+
where: {
69+
login: {
70+
not: {
71+
startsWith: '3b3-',
72+
},
73+
},
74+
kind: {
75+
not: "admin",
76+
},
77+
cursus_users: {
78+
some: {
79+
cursus_id: discopiscine.cursus.id,
80+
end_at: {
81+
in: discopiscine.end_ats,
82+
},
83+
},
84+
},
85+
},
86+
include: {
87+
project_users: {
88+
where: {
89+
project_id: {
90+
in: piscineProjectIdsOrdered,
91+
},
92+
},
93+
include: {
94+
project: true,
95+
}
96+
},
97+
cursus_users: {
98+
where: {
99+
cursus_id: discopiscine.cursus.id,
100+
end_at: {
101+
in: discopiscine.end_ats,
102+
},
103+
},
104+
},
105+
locations: {
106+
// Only the latest or current one
107+
take: 1,
108+
where: {
109+
primary: true,
110+
},
111+
orderBy: [
112+
{ begin_at: 'desc' },
113+
],
114+
},
115+
},
116+
});
117+
118+
let dropouts: { [login: string]: boolean } = {};
119+
for (const user of users) {
120+
dropouts[user.login] = isDiscoPiscineDropout(user.cursus_users[0]);
121+
}
122+
123+
// Sort users first by dropout status, then by name
124+
users.sort((a, b) => {
125+
if (dropouts[a.login] && !dropouts[b.login]) {
126+
return 1;
127+
}
128+
if (!dropouts[a.login] && dropouts[b.login]) {
129+
return -1;
130+
}
131+
return a.first_name.localeCompare(b.first_name) || a.last_name.localeCompare(b.last_name);
132+
});
133+
134+
// Get logtime for each day of the discovery piscine for each user
135+
let logtimes: { [login: string]: DiscoPiscineLogTimes } = {}
136+
for (const user of users) {
137+
const piscineBegin = user.cursus_users[0]?.begin_at;
138+
const dayTwo = new Date(piscineBegin.getTime() + 60 * 60 * 24 * 1 * 1000);
139+
const dayThree = new Date(piscineBegin.getTime() + 60 * 60 * 24 * 2 * 1000);
140+
const dayFour = new Date(piscineBegin.getTime() + 60 * 60 * 24 * 3 * 1000);
141+
const dayFive = new Date(piscineBegin.getTime() + 60 * 60 * 24 * 4 * 1000);
142+
const piscineEnd = new Date(piscineBegin.getTime() + 60 * 60 * 24 * 5 * 1000);
143+
144+
const locationsDuringPiscine = await prisma.location.findMany({
145+
where: {
146+
user_id: user.id,
147+
begin_at: {
148+
gte: piscineBegin,
149+
lte: piscineEnd,
150+
},
151+
},
152+
orderBy: [
153+
{ begin_at: 'asc' },
154+
],
155+
});
156+
157+
// Calculate seconds spent behind the computer on each day
158+
logtimes[user.login] = {
159+
dayOne: getTimeSpentBehindComputer(locationsDuringPiscine, piscineBegin, dayTwo),
160+
dayTwo: getTimeSpentBehindComputer(locationsDuringPiscine, dayTwo, dayThree),
161+
dayThree: getTimeSpentBehindComputer(locationsDuringPiscine, dayThree, dayFour),
162+
dayFour: getTimeSpentBehindComputer(locationsDuringPiscine, dayFour, dayFive),
163+
dayFive: getTimeSpentBehindComputer(locationsDuringPiscine, dayFive, piscineEnd),
164+
total: locationsDuringPiscine.reduce((acc, l) => acc + ((l.end_at ? l.end_at.getTime() : Date.now()) - l.begin_at.getTime()) / 1000, 0),
165+
};
166+
}
167+
168+
const projects = await getPiscineProjects(prisma, piscineProjectIdsOrdered);
169+
for (const user of users) {
170+
// Add the missing projects to the user
171+
for (const project_id of piscineProjectIdsOrdered) {
172+
if (!user.project_users.find((pu) => pu.project_id === project_id)) {
173+
user.project_users.push({
174+
id: 0,
175+
project_id: project_id,
176+
user_id: user.id,
177+
final_mark: null,
178+
status: 'not_started',
179+
validated: false,
180+
current_team_id: null,
181+
created_at: new Date(),
182+
updated_at: new Date(),
183+
marked_at: null,
184+
project: projects.find((p) => p.id === project_id)!,
185+
});
186+
}
187+
}
188+
189+
// Remove any projects that are not part of the piscine
190+
// Sometimes a user did both piscines and has projects from both, so we need to filter them out
191+
user.project_users = user.project_users.filter((pu) => piscineProjectIdsOrdered.includes(pu.project_id));
192+
193+
// Order each user's projects based on the order of project ids defined in piscineProjectIdsOrdered
194+
user.project_users.sort((a, b) => {
195+
return piscineProjectIdsOrdered.indexOf(a.project_id) - piscineProjectIdsOrdered.indexOf(b.project_id);
196+
});
197+
}
198+
199+
// Cache the data for the remaining time of the sync interval
200+
piscineCache.set(cacheKey, { users, logtimes, dropouts, projects }, SYNC_INTERVAL * 60 * 1000);
201+
202+
return { users, logtimes, dropouts, projects };
203+
};
204+
205+
export const buildDiscoPiscineCache = async function(prisma: PrismaClient) {
206+
const discoPiscines = await getAllDiscoPiscines(prisma);
207+
for (const discoPiscine of discoPiscines) {
208+
console.debug(`Building cache for Discovery Piscine ${discoPiscine.year} week ${discoPiscine.week} with cursus_id ${discoPiscine.cursus.id}...`);
209+
await getDiscoPiscineData(prisma, discoPiscine.year_num, discoPiscine.week_num, discoPiscine.cursus.id, true);
210+
}
211+
};

src/handlers/filters.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Express } from 'express';
22
import nunjucks from 'nunjucks';
33
import { CursusUser, ProjectUser } from '@prisma/client';
44
import { DISCO_PISCINE_CURSUS_IDS, PISCINE_CURSUS_IDS } from '../intra/cursus';
5-
import { isDiscoPiscineDropout, isPiscineDropout, projectStatusToString } from '../utils';
5+
import { isDiscoPiscineDropout, isCPiscineDropout, projectStatusToString, shortenDiscoPiscineCursusName } from '../utils';
66

77
export const setupNunjucksFilters = function(app: Express): void {
88
const nunjucksEnv = nunjucks.configure('templates', {
@@ -57,8 +57,18 @@ export const setupNunjucksFilters = function(app: Express): void {
5757
});
5858

5959
// Add formatting to remove the prefix "C Piscine" from project names
60-
nunjucksEnv.addFilter('removePiscinePrefix', (name: string) => {
61-
return name.replace(/^C Piscine /, '');
60+
nunjucksEnv.addFilter('removeCPiscinePrefix', (name: string) => {
61+
return name.replace(/^C Piscine /, '')
62+
});
63+
64+
// Add formatting to remove the prefixes from several Discovery Piscine project names
65+
nunjucksEnv.addFilter('removeDiscoPiscinePrefix', (name: string) => {
66+
return name.replace(/^Disco Piscine - /, '').replace(/^Module/, '').replace(/^Cellule/, '').replace(/^disco-ai-/, '').replace(/^Python - /, '');
67+
});
68+
69+
// Add formatting to remove the prefix "Discovery Piscine" from cursus names
70+
nunjucksEnv.addFilter('removeDiscoPiscineCursusPrefix', (name: string) => {
71+
return shortenDiscoPiscineCursusName(name);
6272
});
6373

6474
// Add formatting for status field of a projectuser
@@ -80,7 +90,7 @@ export const setupNunjucksFilters = function(app: Express): void {
8090
if (cursusUser.end_at && cursusUser.end_at < now) {
8191
// Special C Piscine handling
8292
if (PISCINE_CURSUS_IDS.includes(cursusUser.cursus_id)) {
83-
return (isPiscineDropout(cursusUser) ? 'dropout' : '');
93+
return (isCPiscineDropout(cursusUser) ? 'dropout' : '');
8494
}
8595
// Special Discovery Piscine handling
8696
if (DISCO_PISCINE_CURSUS_IDS.includes(cursusUser.cursus_id)) {

src/handlers/piscine.ts

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { PrismaClient } from '@prisma/client';
22
import { C_PISCINE_PROJECTS_ORDER, DEPR_PISCINE_C_PROJECTS_ORDER } from '../intra/projects';
3-
import { getAllPiscines, getTimeSpentBehindComputer, isPiscineDropout } from '../utils';
3+
import { getPiscineProjects, getAllCPiscines, getTimeSpentBehindComputer, isCPiscineDropout } from '../utils';
44
import { piscineCache } from './cache';
55
import { SYNC_INTERVAL } from '../intra/base';
66
import { PISCINE_CURSUS_IDS } from '../intra/cursus';
77

8-
export interface PiscineLogTimes {
8+
export interface CPiscineLogTimes {
99
weekOne: number;
1010
weekTwo: number;
1111
weekThree: number;
@@ -14,19 +14,19 @@ export interface PiscineLogTimes {
1414
};
1515

1616
// TODO: define the types for the piscine data explicitly
17-
export interface PiscineData {
17+
export interface CPiscineData {
1818
users: any[];
19-
logtimes: { [login: string]: PiscineLogTimes };
19+
logtimes: { [login: string]: CPiscineLogTimes };
2020
dropouts: { [login: string]: boolean };
2121
projects: any[];
2222
};
2323

24-
export const getPiscineData = async function(year: number, month: number, prisma: PrismaClient): Promise<PiscineData> {
24+
export const getCPiscineData = async function(prisma: PrismaClient, year: number, month: number, noCache: boolean = false): Promise<CPiscineData> {
2525
// Check if the data is already in the cache
2626
const cacheKey = `piscine-${year}-${month}`;
2727
const cachedData = piscineCache.get(cacheKey);
28-
if (cachedData) {
29-
return cachedData as PiscineData;
28+
if (!noCache && cachedData) {
29+
return cachedData as CPiscineData;
3030
}
3131

3232
// Find all users for the given year and month
@@ -77,9 +77,9 @@ export const getPiscineData = async function(year: number, month: number, prisma
7777
});
7878

7979
// Check for each pisciner if they are a dropout
80-
let dropouts: { [login: string]: boolean } = {}
80+
let dropouts: { [login: string]: boolean } = {};
8181
for (const user of users) {
82-
dropouts[user.login] = isPiscineDropout(user.cursus_users[0]);
82+
dropouts[user.login] = isCPiscineDropout(user.cursus_users[0]);
8383
}
8484

8585
// Sort users first by dropout status, then by name
@@ -94,7 +94,7 @@ export const getPiscineData = async function(year: number, month: number, prisma
9494
});
9595

9696
// Get logtime for each week of the piscine for each user
97-
let logtimes: { [login: string]: PiscineLogTimes } = {}
97+
let logtimes: { [login: string]: CPiscineLogTimes } = {}
9898
for (const user of users) {
9999
const piscineBegin = user.cursus_users[0]?.begin_at;
100100
const weekTwo = new Date(piscineBegin.getTime() + 60 * 60 * 24 * 7 * 1000);
@@ -115,7 +115,7 @@ export const getPiscineData = async function(year: number, month: number, prisma
115115
],
116116
});
117117

118-
// Calculate seconds spent in each week behind the computer
118+
// Calculate seconds spent behind the computer in each week
119119
logtimes[user.login] = {
120120
weekOne: getTimeSpentBehindComputer(locationsDuringPiscine, piscineBegin, weekTwo),
121121
weekTwo: getTimeSpentBehindComputer(locationsDuringPiscine, weekTwo, weekThree),
@@ -147,20 +147,7 @@ export const getPiscineData = async function(year: number, month: number, prisma
147147
}
148148
const piscineProjectIdsOrdered = (piscineType === 'new' ? C_PISCINE_PROJECTS_ORDER : DEPR_PISCINE_C_PROJECTS_ORDER);
149149

150-
// Fetch all projects for the piscine
151-
const projects = await prisma.project.findMany({
152-
where: {
153-
id: {
154-
in: piscineProjectIdsOrdered,
155-
},
156-
},
157-
});
158-
159-
// Order projects based on the order of project ids defined in piscineProjectIdsOrdered
160-
projects.sort((a, b) => {
161-
return piscineProjectIdsOrdered.indexOf(a.id) - piscineProjectIdsOrdered.indexOf(b.id);
162-
});
163-
150+
const projects = await getPiscineProjects(prisma, piscineProjectIdsOrdered);
164151
for (const user of users) {
165152
// Add the missing projects to the user
166153
for (const project_id of piscineProjectIdsOrdered) {
@@ -197,10 +184,10 @@ export const getPiscineData = async function(year: number, month: number, prisma
197184
return { users, logtimes, dropouts, projects };
198185
};
199186

200-
export const buildPiscineCache = async function(prisma: PrismaClient) {
201-
const piscines = await getAllPiscines(prisma);
187+
export const buildCPiscineCache = async function(prisma: PrismaClient) {
188+
const piscines = await getAllCPiscines(prisma);
202189
for (const piscine of piscines) {
203-
console.debug(`Building cache for piscine ${piscine.year}-${piscine.month}...`);
204-
await getPiscineData(piscine.year_num, piscine.month_num, prisma);
190+
console.debug(`Building cache for C Piscine ${piscine.month} ${piscine.year}...`);
191+
await getCPiscineData(prisma, piscine.year_num, piscine.month_num, true);
205192
}
206193
};

src/intra/base.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { syncProjectsUsers } from "./projects_users";
77
import { syncLocations } from "./locations";
88
import { DEV_DAYS_LIMIT, NODE_ENV } from "../env";
99
import { cleanupDB } from "./cleanup";
10-
import { buildPiscineCache } from "../handlers/piscine";
10+
import { buildCPiscineCache } from "../handlers/piscine";
1111
import { syncGroups } from "./groups";
1212
import { syncGroupsUsers } from "./groups_users";
13+
import { buildDiscoPiscineCache } from "../handlers/disco";
1314

1415
export const prisma = new PrismaClient();
1516
export const SYNC_INTERVAL = 10; // minutes
@@ -181,9 +182,10 @@ export const syncWithIntra = async function(api: Fast42): Promise<void> {
181182
// Clear the server-side cache (should not be needed because of the cache rebuilding below)
182183
// await invalidateAllCache();
183184

184-
// Rebuild cache for all piscines
185+
// Rebuild cache for all C Piscines and Discovery Piscines
185186
// Don't wait for this to finish, as it can take a long time. Serve the site while this is running.
186-
buildPiscineCache(prisma);
187+
buildCPiscineCache(prisma);
188+
buildDiscoPiscineCache(prisma);
187189

188190
console.info(`Intra synchronization completed at ${new Date().toISOString()}.`);
189191
};

src/intra/projects.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ import Fast42 from '@codam/fast42';
22
import { prisma, syncData } from './base';
33
import { CURSUS_IDS } from './cursus';
44

5-
export const C_PISCINE_PROJECTS_ORDER = [1255, 1256, 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1270, 1264, 1265, 1266, 1267, 1268, 1271, 1308, 1310, 1309, 1305, 1301, 1302, 1303, 1304];
6-
export const DEPR_PISCINE_C_PROJECTS_ORDER = [154, 155, 156, 157, 158, 159, 160, 161, 162, 167, 163, 164, 165, 166, 168, 169, 170, 171, 172, 173, 404, 405, 406, 407];
5+
export const C_PISCINE_PROJECTS_ORDER = [1255, 1256, 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1270, 1264, 1265, 1266, 1267, 1268, 1271, 1308, 1310, 1309, 1305, 1301, 1302, 1303, 1304]; // Cursus ID 9
6+
export const DEPR_PISCINE_C_PROJECTS_ORDER = [154, 155, 156, 157, 158, 159, 160, 161, 162, 167, 163, 164, 165, 166, 168, 169, 170, 171, 172, 173, 404, 405, 406, 407]; // Cursus ID 4
7+
export const DISCO_PISCINE_AI_INTER_PROJECTS_ORDER = [2601, 2602, 2603, 2604, 2605]; // Cursus ID 77
8+
export const DISCO_PISCINE_AI_FUNDA_PROJECTS_ORDER = [2608, 2609, 2610, 2611]; // Cursus ID 79
9+
export const DISCO_PISCINE_CORE_PYTHON_PROJECTS_ORDER = [2612, 2613, 2614, 2615, 2616, 2617, 2618, 2619, 2620, 2621]; // Cursus ID 80
10+
export const DISCO_PISCINE_WEB_PRGM_ESS_PROJECTS_ORDER = [2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034]; // Cursus ID 3
11+
export const DISCO_PISCINE_DEPR_PYTHON_PROJECTS_ORDER = []; // Not implemented, cursus ID 69
712

813
export const syncProjects = async function(api: Fast42, syncDate: Date): Promise<void> {
914
// Fetch the last synchronization date from the database

0 commit comments

Comments
 (0)