Skip to content

Commit 84743da

Browse files
committed
feat: indicate potential dropouts
potential dropouts are pisciners who haven't showed up in >60 hours
1 parent 85053b1 commit 84743da

File tree

6 files changed

+89
-8
lines changed

6 files changed

+89
-8
lines changed

src/handlers/filters.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ export const setupNunjucksFilters = function(app: Express): void {
101101
return '';
102102
});
103103

104+
// Filter to get the values from an object
105+
nunjucksEnv.addFilter('objectValues', (obj: Record<string, any>) => {
106+
return Object.values(obj);
107+
});
108+
109+
// Filter to filter a list by values
110+
nunjucksEnv.addFilter('filter', (list: any[], value: string) => {
111+
return list.filter(item => item.toString() === value.toString());
112+
});
113+
104114
// Debug function to display raw json data
105115
nunjucksEnv.addFilter('dump', (data: any) => {
106116
return JSON.stringify(data, null, 2);

src/handlers/piscine.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface CPiscineData {
1818
users: any[];
1919
logtimes: { [login: string]: CPiscineLogTimes };
2020
dropouts: { [login: string]: boolean };
21+
potentialDropouts: { [login: string]: boolean }; // Anyone who was last seen > 48 hours ago
2122
activeStudents: { [login: string]: boolean };
2223
projects: any[];
2324
};
@@ -83,6 +84,30 @@ export const getCPiscineData = async function(prisma: PrismaClient, year: number
8384
dropouts[user.login] = isCPiscineDropout(user.cursus_users[0]);
8485
}
8586

87+
// Check for each pisciner if they were last seen more than 60 hours ago
88+
// If so, mark them as a potential dropout
89+
// Only do this check if the piscine is currently ongoing, though
90+
let potentialDropouts: { [login: string]: boolean } = {};
91+
for (const user of users) {
92+
// If user is already for sure a dropout, skip this check
93+
if (dropouts[user.login]) {
94+
potentialDropouts[user.login] = false;
95+
continue;
96+
}
97+
// If user's piscine cursus has already ended, skip this check
98+
if (!user.cursus_users[0].end_at || user.cursus_users[0].end_at < new Date()) {
99+
potentialDropouts[user.login] = false;
100+
continue;
101+
}
102+
const lastSeen = (user.locations[0] ? (user.locations[0].end_at ? user.locations[0].end_at : new Date()) : new Date(0)); // Default to epoch if no location found, so they are considered a potential dropout
103+
const now = new Date();
104+
if ((now.getTime() - lastSeen.getTime()) > 60 * 60 * 60 * 1000) { // 60 hours in milliseconds
105+
potentialDropouts[user.login] = true;
106+
} else {
107+
potentialDropouts[user.login] = false;
108+
}
109+
}
110+
86111
// Sort users first by dropout status, then by name
87112
users.sort((a, b) => {
88113
if (dropouts[a.login] && !dropouts[b.login]) {
@@ -201,9 +226,9 @@ export const getCPiscineData = async function(prisma: PrismaClient, year: number
201226
}
202227

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

206-
return { users, logtimes, dropouts, activeStudents, projects };
231+
return { users, logtimes, dropouts, potentialDropouts, activeStudents, projects };
207232
};
208233

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

src/routes/piscines.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@ 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, activeStudents, projects } = await getCPiscineData(prisma, year, month);
31+
const { users, logtimes, dropouts, potentialDropouts, activeStudents, projects } = await getCPiscineData(prisma, year, month);
3232

33-
return res.render('piscines.njk', { piscines, projects, users, logtimes, dropouts, activeStudents, year, month, subtitle: `${year} ${numberToMonth(month)}` });
33+
return res.render('piscines.njk', { piscines, projects, users, logtimes, dropouts, potentialDropouts, 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, activeStudents, projects } = await getCPiscineData(prisma, year, month);
41+
const { users, logtimes, dropouts, potentialDropouts, activeStudents, projects } = await getCPiscineData(prisma, year, month);
4242

4343
const now = new Date();
4444
res.setHeader('Content-Type', 'text/csv');
@@ -48,6 +48,7 @@ export const setupPiscinesRoutes = function(app: Express, prisma: PrismaClient):
4848
'login',
4949
'active_student',
5050
'dropout',
51+
'potential_dropout',
5152
'last_login_at',
5253
'logtime_week_one',
5354
'logtime_week_two',
@@ -67,12 +68,14 @@ export const setupPiscinesRoutes = function(app: Express, prisma: PrismaClient):
6768
for (const user of users) {
6869
const logtime = logtimes[user.login];
6970
const dropout = dropouts[user.login] ? 'yes' : 'no';
71+
const potentialDropout = potentialDropouts[user.login] ? 'yes' : 'no';
7072
const activeStudent = activeStudents[user.login] ? 'yes' : 'no';
7173

7274
const row = [
7375
user.login,
7476
activeStudent,
7577
dropout,
78+
potentialDropout,
7679
formatDate(user.locations[0]?.begin_at),
7780
logtime.weekOne / 60 / 60, // Convert seconds to hours
7881
logtime.weekTwo / 60 / 60,

static/css/base.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,28 @@ nav a {
334334
color: var(--red);
335335
}
336336

337+
.userlist .user.potential-dropout {
338+
transition: 0.1s;
339+
opacity: 0.7;
340+
filter: grayscale(50%);
341+
-moz-filter: grayscale(50%);
342+
-webkit-filter: grayscale(50%);
343+
-o-filter: grayscale(50%);
344+
}
345+
346+
.userlist .user.potential-dropout:hover {
347+
transition: 0.03s;
348+
opacity: 0.9;
349+
filter: grayscale(25%);
350+
-moz-filter: grayscale(25%);
351+
-webkit-filter: grayscale(25%);
352+
-o-filter: grayscale(25%);
353+
}
354+
355+
.userlist .user.potential-dropout .level {
356+
color: var(--yellow);
357+
}
358+
337359
.user .name {
338360
font-weight: bold;
339361
font-size: x-large;

templates/disco.njk

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,15 @@
6767
<ul class="userlist piscine">
6868
{% for user in users %}
6969
<!-- display user information -->
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" }}">
70+
<li class="user piscine{{ " dropout" if dropouts[user.login] }}"
71+
data-firstname="{{ (user.usual_first_name | lower | e) if user.usual_first_name else (user.first_name | lower | e) }}"
72+
data-lastname="{{ user.last_name | lower | e }}"
73+
data-login="{{ user.login | e }}"
74+
data-lastseen="{{ (user.locations[0].begin_at | timestamp) if user.locations.length > 0 else 0 }}"
75+
data-totallogtime="{{ logtimes[user.login].total }}"
76+
data-level="{{ user.cursus_users[0].level | formatFloat }}"
77+
data-student="{{ "1" if activeStudents[user.login] else "0" }}" >
78+
<!-- big li element above, spanning multiple lines -->
7179
<div class="basic-info">
7280
<div class="name">{{ user.usual_full_name | e }}</div>
7381
<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>

templates/piscines.njk

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<option value="login">Sort by login</option>
3434
<option value="lastseen">Sort by last seen</option>
3535
<option value="totallogtime">Sort by total logtime</option>
36+
<option value="potentialdropout">Sort by potential dropout</option>
3637
<option value="level">Sort by level</option>
3738
<option value="student">Sort by active students</option>
3839
</select>
@@ -69,15 +70,27 @@
6970
const formattedDate = date.toISOString().slice(0, 10);
7071
csvDownloadLink.download = `piscine-{{ year | int }}-{{ month | int }}-export-${formattedDate}.csv`;
7172
</script>
73+
74+
{# get the list of dropouts filtered by values of true #}
75+
<span class="piscine-info"><b>Statistics:</b> candidates: {{ users | length }}, dropouts: {{ dropouts | objectValues | filter("true") | length }}, potential dropouts: {{ potentialDropouts | objectValues | filter("true") | length }}, remaining: {{ users | length - (dropouts | objectValues | filter("true") | length + potentialDropouts | objectValues | filter("true") | length) }}{# I know, too long of a line... #}</span>
7276
</div>
7377
<ul class="userlist piscine">
7478
{% for user in users %}
7579
<!-- display user information -->
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" }}">
80+
<li class="user piscine{{ " dropout" if dropouts[user.login] }}{{ " potential-dropout" if potentialDropouts[user.login] }}"
81+
data-firstname="{{ (user.usual_first_name | lower | e) if user.usual_first_name else (user.first_name | lower | e) }}"
82+
data-lastname="{{ user.last_name | lower | e }}"
83+
data-login="{{ user.login | e }}"
84+
data-lastseen="{{ (user.locations[0].begin_at | timestamp) if user.locations.length > 0 else 0 }}"
85+
data-totallogtime="{{ logtimes[user.login].total }}"
86+
data-level="{{ user.cursus_users[0].level | formatFloat }}"
87+
data-student="{{ "1" if activeStudents[user.login] else "0" }}"
88+
data-potentialdropout="{{ "1" if potentialDropouts[user.login] else "0" }}" >
89+
<!-- big li element above, spanning multiple lines -->
7790
<div class="basic-info">
7891
<div class="name">{{ user.usual_full_name | e }}</div>
7992
<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>
80-
<div class="level">{{ user.cursus_users[0].level | formatFloat }}{{ " (dropout)" if dropouts[user.login] }}</div>
93+
<div class="level">{{ user.cursus_users[0].level | formatFloat }}{{ " (dropout)" if dropouts[user.login] }}{{ " (potential dropout)" if potentialDropouts[user.login] }}</div>
8194
<img class="picture" src="{{ user.image if user.image else "/images/default.png" }}" loading="lazy" />
8295
</div>
8396

0 commit comments

Comments
 (0)