Skip to content

Commit 1ff6f5f

Browse files
committed
feat: add active student badges to pisc. overviews
1 parent ab5a525 commit 1ff6f5f

File tree

7 files changed

+82
-14
lines changed

7 files changed

+82
-14
lines changed

src/handlers/disco.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DISCO_PISCINE_AI_FUNDA_PROJECTS_ORDER, DISCO_PISCINE_AI_INTER_PROJECTS_
33
import { getPiscineProjects, getAllDiscoPiscines, getTimeSpentBehindComputer, isDiscoPiscineDropout } from '../utils';
44
import { piscineCache } from './cache';
55
import { SYNC_INTERVAL } from '../intra/base';
6+
import { REGULAR_CURSUS_IDS } from '../intra/cursus';
67

78
export interface DiscoPiscineLogTimes {
89
dayOne: number;
@@ -18,6 +19,7 @@ export interface DiscoPiscineData {
1819
users: any[];
1920
logtimes: { [login: string]: DiscoPiscineLogTimes };
2021
dropouts: { [login: string]: boolean };
22+
activeStudents: { [login: string]: boolean };
2123
projects: any[];
2224
};
2325

@@ -130,6 +132,27 @@ export const getDiscoPiscineData = async function(prisma: PrismaClient, year: nu
130132
return a.first_name.localeCompare(b.first_name) || a.last_name.localeCompare(b.last_name);
131133
});
132134

135+
// For each user, check if they are also a student in the regular cursus
136+
let activeStudents: { [login: string]: boolean } = {};
137+
for (const user of users) {
138+
const cursusUsers = await prisma.cursusUser.findMany({
139+
where: {
140+
user_id: user.id,
141+
cursus_id: {
142+
in: REGULAR_CURSUS_IDS,
143+
},
144+
begin_at: {
145+
lte: new Date(),
146+
},
147+
OR: [
148+
{ end_at: null }, // Currently enrolled
149+
{ end_at: { gt: new Date() } }, // Future end date
150+
],
151+
},
152+
});
153+
activeStudents[user.login] = cursusUsers.length > 0;
154+
}
155+
133156
// Get logtime for each day of the discovery piscine for each user
134157
let logtimes: { [login: string]: DiscoPiscineLogTimes } = {}
135158
for (const user of users) {
@@ -196,9 +219,9 @@ export const getDiscoPiscineData = async function(prisma: PrismaClient, year: nu
196219
}
197220

198221
// Cache the data for the remaining time of the sync interval
199-
piscineCache.set(cacheKey, { users, logtimes, dropouts, projects }, SYNC_INTERVAL * 60 * 1000);
222+
piscineCache.set(cacheKey, { users, logtimes, dropouts, activeStudents, projects }, SYNC_INTERVAL * 60 * 1000);
200223

201-
return { users, logtimes, dropouts, projects };
224+
return { users, logtimes, dropouts, activeStudents, projects };
202225
};
203226

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

src/handlers/piscine.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { C_PISCINE_PROJECTS_ORDER, DEPR_PISCINE_C_PROJECTS_ORDER } from '../intr
33
import { getPiscineProjects, getAllCPiscines, getTimeSpentBehindComputer, isCPiscineDropout } from '../utils';
44
import { piscineCache } from './cache';
55
import { SYNC_INTERVAL } from '../intra/base';
6-
import { PISCINE_CURSUS_IDS } from '../intra/cursus';
6+
import { PISCINE_CURSUS_IDS, REGULAR_CURSUS_IDS } from '../intra/cursus';
77

88
export interface CPiscineLogTimes {
99
weekOne: number;
@@ -18,6 +18,7 @@ export interface CPiscineData {
1818
users: any[];
1919
logtimes: { [login: string]: CPiscineLogTimes };
2020
dropouts: { [login: string]: boolean };
21+
activeStudents: { [login: string]: boolean };
2122
projects: any[];
2223
};
2324

@@ -93,6 +94,27 @@ export const getCPiscineData = async function(prisma: PrismaClient, year: number
9394
return a.first_name.localeCompare(b.first_name) || a.last_name.localeCompare(b.last_name);
9495
});
9596

97+
// For each user, check if they are also a student in the regular cursus
98+
let activeStudents: { [login: string]: boolean } = {};
99+
for (const user of users) {
100+
const cursusUsers = await prisma.cursusUser.findMany({
101+
where: {
102+
user_id: user.id,
103+
cursus_id: {
104+
in: REGULAR_CURSUS_IDS,
105+
},
106+
begin_at: {
107+
lte: new Date(),
108+
},
109+
OR: [
110+
{ end_at: null }, // Currently enrolled
111+
{ end_at: { gt: new Date() } }, // Future end date
112+
],
113+
},
114+
});
115+
activeStudents[user.login] = cursusUsers.length > 0;
116+
}
117+
96118
// Get logtime for each week of the piscine for each user
97119
let logtimes: { [login: string]: CPiscineLogTimes } = {}
98120
for (const user of users) {
@@ -179,9 +201,9 @@ export const getCPiscineData = async function(prisma: PrismaClient, year: number
179201
}
180202

181203
// Cache the data for the remaining time of the sync interval
182-
piscineCache.set(cacheKey, { users, logtimes, dropouts, projects }, SYNC_INTERVAL * 60 * 1000);
204+
piscineCache.set(cacheKey, { users, logtimes, dropouts, activeStudents, projects }, SYNC_INTERVAL * 60 * 1000);
183205

184-
return { users, logtimes, dropouts, projects };
206+
return { users, logtimes, dropouts, activeStudents, projects };
185207
};
186208

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

src/routes/disco.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export const setupDiscoPiscineRoutes = function(app: Express, prisma: PrismaClie
5050
return;
5151
}
5252

53-
const { users, logtimes, dropouts, projects } = piscineData;
54-
return res.render('disco.njk', { discopiscines, projects, users, logtimes, dropouts, year, week, cursus_id, subtitle: `${year} Week ${week}` });
53+
const { users, logtimes, dropouts, activeStudents, projects } = piscineData;
54+
return res.render('disco.njk', { discopiscines, projects, users, logtimes, dropouts, activeStudents, year, week, cursus_id, subtitle: `${year} Week ${week}` });
5555
});
5656

5757
app.get('/disco/:year/:month/:cursus_id/csv', passport.authenticate('session'), checkIfStudentOrStaff, checkIfCatOrStaff, checkIfPiscineHistoryAccess, async (req, res) => {
@@ -73,6 +73,7 @@ export const setupDiscoPiscineRoutes = function(app: Express, prisma: PrismaClie
7373

7474
const headers = [
7575
'login',
76+
'active_student',
7677
'dropout',
7778
'last_login_at',
7879
'logtime_day_one',
@@ -94,9 +95,11 @@ export const setupDiscoPiscineRoutes = function(app: Express, prisma: PrismaClie
9495
for (const user of piscineData.users) {
9596
const logtime = piscineData.logtimes[user.login];
9697
const dropout = piscineData.dropouts[user.login] ? 'yes' : 'no';
98+
const activeStudent = piscineData.activeStudents[user.login] ? 'yes' : 'no';
9799

98100
const row = [
99101
user.login,
102+
activeStudent,
100103
dropout,
101104
formatDate(user.locations[0]?.begin_at),
102105
logtime.dayOne / 60 / 60, // Convert seconds to hours

src/routes/piscines.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,25 @@ export const setupPiscinesRoutes = function(app: Express, prisma: PrismaClient):
2828
// Find all possible piscines from the database (if not staff, limit to the current year)
2929
const piscines = await getAllCPiscines(prisma, hasLimitedPiscineHistoryAccess(req.user as IntraUser));
3030

31-
const { users, logtimes, dropouts, projects } = await getCPiscineData(prisma, year, month);
31+
const { users, logtimes, dropouts, activeStudents, projects } = await getCPiscineData(prisma, year, month);
3232

33-
return res.render('piscines.njk', { piscines, projects, users, logtimes, dropouts, year, month, subtitle: `${year} ${numberToMonth(month)}` });
33+
return res.render('piscines.njk', { piscines, projects, users, logtimes, dropouts, activeStudents, year, month, subtitle: `${year} ${numberToMonth(month)}` });
3434
});
3535

3636
app.get('/piscines/:year/:month/csv', passport.authenticate('session'), checkIfStudentOrStaff, checkIfCatOrStaff, checkIfPiscineHistoryAccess, async (req, res) => {
3737
// Parse parameters
3838
const year = parseInt(req.params.year);
3939
const month = parseInt(req.params.month);
4040

41-
const { users, logtimes, dropouts, projects } = await getCPiscineData(prisma, year, month);
41+
const { users, logtimes, dropouts, activeStudents, projects } = await getCPiscineData(prisma, year, month);
4242

4343
const now = new Date();
4444
res.setHeader('Content-Type', 'text/csv');
4545
res.setHeader('Content-Disposition', `attachment; filename="piscine-${year}-${month}-export-${formatDate(now).replace(' ', '-')}.csv"`);
4646

4747
const headers = [
4848
'login',
49+
'active_student',
4950
'dropout',
5051
'last_login_at',
5152
'logtime_week_one',
@@ -66,9 +67,11 @@ export const setupPiscinesRoutes = function(app: Express, prisma: PrismaClient):
6667
for (const user of users) {
6768
const logtime = logtimes[user.login];
6869
const dropout = dropouts[user.login] ? 'yes' : 'no';
70+
const activeStudent = activeStudents[user.login] ? 'yes' : 'no';
6971

7072
const row = [
7173
user.login,
74+
activeStudent,
7275
dropout,
7376
formatDate(user.locations[0]?.begin_at),
7477
logtime.weekOne / 60 / 60, // Convert seconds to hours

static/css/base.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,21 @@ nav a {
348348
font-size: 16px;
349349
}
350350

351+
.user .login > * {
352+
vertical-align: middle;
353+
}
354+
355+
.user .badge {
356+
display: inline-block;
357+
margin: 4px 0;
358+
padding: 2px 6px;
359+
font-size: small;
360+
color: var(--white);
361+
background-color: var(--blue);
362+
border-radius: 4px;
363+
text-transform: uppercase;
364+
}
365+
351366
.user .pool {
352367
font-size: small;
353368
}

templates/disco.njk

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<option value="lastseen">Sort by last seen</option>
2929
<option value="totallogtime">Sort by total logtime</option>
3030
<option value="level">Sort by level</option>
31+
<option value="student">Sort by active students</option>
3132
</select>
3233
<script>
3334
document.getElementById('sort-dropdown').addEventListener('change', function() {
@@ -66,10 +67,10 @@
6667
<ul class="userlist piscine">
6768
{% for user in users %}
6869
<!-- display user information -->
69-
<li class="user piscine{{ " dropout" if dropouts[user.login] }}" data-firstname="{{ (user.usual_first_name | lower | e) if user.usual_first_name else (user.first_name | lower | e) }}" data-lastname="{{ user.last_name | lower | e }}" data-login="{{ user.login | e }}" data-lastseen="{{ (user.locations[0].begin_at | timestamp) if user.locations.length > 0 else 0 }}" data-totallogtime="{{ logtimes[user.login].total }}" data-level="{{ user.cursus_users[0].level | formatFloat }}">
70+
<li class="user piscine{{ " dropout" if dropouts[user.login] }}" data-firstname="{{ (user.usual_first_name | lower | e) if user.usual_first_name else (user.first_name | lower | e) }}" data-lastname="{{ user.last_name | lower | e }}" data-login="{{ user.login | e }}" data-lastseen="{{ (user.locations[0].begin_at | timestamp) if user.locations.length > 0 else 0 }}" data-totallogtime="{{ logtimes[user.login].total }}" data-level="{{ user.cursus_users[0].level | formatFloat }}" data-student="{{ "1" if activeStudents[user.login] else "0" }}">
7071
<div class="basic-info">
7172
<div class="name">{{ user.usual_full_name | e }}</div>
72-
<div class="login" title="User ID {{ user.id | int }}"><a class="external" target="_blank" href="https://profile.intra.42.fr/users/{{ user.login | e }}/">{{ user.login | e }}</a></div>
73+
<div class="login" title="User ID {{ user.id | int }}"><a class="external" target="_blank" href="https://profile.intra.42.fr/users/{{ user.login | e }}/">{{ user.login | e }}</a>{% if activeStudents[user.login] %} <span class="badge" title="User is currently an active student">Student</span>{% endif %}</div>
7374
<div class="level">{{ user.cursus_users[0].level | formatFloat }}{{ " (dropout)" if dropouts[user.login] }}</div>
7475
<img class="picture" src="{{ user.image if user.image else "/images/default.png" }}" loading="lazy" />
7576
</div>

templates/piscines.njk

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<option value="lastseen">Sort by last seen</option>
3535
<option value="totallogtime">Sort by total logtime</option>
3636
<option value="level">Sort by level</option>
37+
<option value="student">Sort by active students</option>
3738
</select>
3839
<script>
3940
document.getElementById('sort-dropdown').addEventListener('change', function() {
@@ -72,10 +73,10 @@
7273
<ul class="userlist piscine">
7374
{% for user in users %}
7475
<!-- display user information -->
75-
<li class="user piscine{{ " dropout" if dropouts[user.login] }}" data-firstname="{{ (user.usual_first_name | lower | e) if user.usual_first_name else (user.first_name | lower | e) }}" data-lastname="{{ user.last_name | lower | e }}" data-login="{{ user.login | e }}" data-lastseen="{{ (user.locations[0].begin_at | timestamp) if user.locations.length > 0 else 0 }}" data-totallogtime="{{ logtimes[user.login].total }}" data-level="{{ user.cursus_users[0].level | formatFloat }}">
76+
<li class="user piscine{{ " dropout" if dropouts[user.login] }}" data-firstname="{{ (user.usual_first_name | lower | e) if user.usual_first_name else (user.first_name | lower | e) }}" data-lastname="{{ user.last_name | lower | e }}" data-login="{{ user.login | e }}" data-lastseen="{{ (user.locations[0].begin_at | timestamp) if user.locations.length > 0 else 0 }}" data-totallogtime="{{ logtimes[user.login].total }}" data-level="{{ user.cursus_users[0].level | formatFloat }}" data-student="{{ "1" if activeStudents[user.login] else "0" }}">
7677
<div class="basic-info">
7778
<div class="name">{{ user.usual_full_name | e }}</div>
78-
<div class="login" title="User ID {{ user.id | int }}"><a class="external" target="_blank" href="https://profile.intra.42.fr/users/{{ user.login | e }}/">{{ user.login | e }}</a></div>
79+
<div class="login" title="User ID {{ user.id | int }}"><a class="external" target="_blank" href="https://profile.intra.42.fr/users/{{ user.login | e }}/">{{ user.login | e }}</a>{% if activeStudents[user.login] %} <span class="badge" title="User is currently an active student">Student</span>{% endif %}</div>
7980
<div class="level">{{ user.cursus_users[0].level | formatFloat }}{{ " (dropout)" if dropouts[user.login] }}</div>
8081
<img class="picture" src="{{ user.image if user.image else "/images/default.png" }}" loading="lazy" />
8182
</div>

0 commit comments

Comments
 (0)