Skip to content

Commit

Permalink
fix(api,admin): 3245 - Fix de plusieurs Bug CLE (#4292)
Browse files Browse the repository at this point in the history
  • Loading branch information
C2Chandelier authored Sep 11, 2024
1 parent a75e8ac commit 74d0d20
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 28 deletions.
15 changes: 11 additions & 4 deletions admin/src/scenes/classe/components/GeneralInfos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export default function GeneralInfos({ classe, setClasse, edit, setEdit, errors,
}
};

const isUserCLE = [ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(user.role);
const isUserAdminOrReferent = [ROLES.ADMIN, ROLES.REFERENT_REGION, ROLES.REFERENT_DEPARTMENT].includes(user.role);

const linkPath = isUserCLE ? "/mes-eleves" : "/inscription";
const showButton = (isUserCLE && classe.schoolYear === "2024-2025") || isUserAdminOrReferent;

return (
<Container title="Informations générales" actions={containerActionList({ edit, setEdit, canEdit: rights.canEdit })}>
<div className="flex items-stretch justify-stretch">
Expand Down Expand Up @@ -187,10 +193,11 @@ export default function GeneralInfos({ classe, setClasse, edit, setEdit, errors,
</Link>
</>
)}
<Link key="list-students" to={`${[ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(user.role) ? "/mes-eleves" : "/inscription"}?classeId=${classe._id}`}>
<Button type="tertiary" title="Voir la liste des élèves" className="w-full max-w-none mt-3" />
</Link>

{showButton && (
<Link key="list-students" to={`${linkPath}?classeId=${classe._id}`}>
<Button type="tertiary" title="Voir la liste des élèves" className="w-full max-w-none mt-3" />
</Link>
)}
{edit && [ROLES.ADMIN, ROLES.ADMINISTRATEUR_CLE].includes(user.role) && <WithdrawButton classe={classe} setIsLoading={setIsLoading} />}
</div>
</div>
Expand Down
13 changes: 10 additions & 3 deletions admin/src/scenes/classe/components/StatsInfos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ interface Props {
}

export default function StatsInfos({ classe, user, studentStatus, validatedYoung }: Props) {
const isUserCLE = [ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(user.role);
const isUserAdminOrReferent = [ROLES.ADMIN, ROLES.REFERENT_REGION, ROLES.REFERENT_DEPARTMENT].includes(user.role);

const linkPath = isUserCLE ? "/mes-eleves" : "/inscription";
const showButton = (isUserCLE && classe.schoolYear === "2024-2025") || isUserAdminOrReferent;
return (
<Container
title="Suivi de la classe"
actions={[
<Link key="list-students" to={`${[ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(user.role) ? "/mes-eleves" : "/inscription"}?classeId=${classe._id}`}>
<Button type="tertiary" title="Voir la liste des élèves" />
</Link>,
showButton && (
<Link key="list-students" to={`${linkPath}?classeId=${classe._id}`}>
<Button type="tertiary" title="Voir la liste des élèves" className="w-full max-w-none mt-3" />
</Link>
),
]}>
<div className="flex justify-between">
<table className="flex-1">
Expand Down
9 changes: 8 additions & 1 deletion admin/src/scenes/classe/header/ButtonLinkInvite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState } from "react";
import { BsSend } from "react-icons/bs";
import { MdContentCopy } from "react-icons/md";

import plausibleEvent from "@/services/plausible";
import { Modal, Button } from "@snu/ds/admin";
import { ProfilePic } from "@snu/ds";
import { copyToClipboard } from "@/utils";
Expand All @@ -15,7 +16,13 @@ export default function ButtonLinkInvite({ url }: Props) {
const [showModal, setShowModal] = useState(false);
return (
<>
<button type="button" className="flex items-center justify-start w-full text-sm leading-5 font-normal" onClick={() => setShowModal(true)}>
<button
type="button"
className="flex items-center justify-start w-full text-sm leading-5 font-normal"
onClick={() => {
plausibleEvent("Inscriptions/CTA - Ouverture modale Inscription via lien CLE");
setShowModal(true);
}}>
Inviter les élèves via un lien
</button>
<Modal
Expand Down
2 changes: 1 addition & 1 deletion admin/src/scenes/classe/header/ButtonManualInvite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function ButtonManualInvite({ id }) {
const history = useHistory();

const onInscription = () => {
plausibleEvent("Inscriptions/CTA - Nouvelle inscription");
plausibleEvent("Inscriptions/CTA - Nouvelle inscription CLE Manuelle");

history.push(`/volontaire/create?classeId=${id}`);
};
Expand Down
5 changes: 2 additions & 3 deletions admin/src/scenes/classe/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,12 @@ export const statusClassForBadge = (status) => {
return statusClasse;
};

export function getRights(user: User, classe?: Pick<ClasseType, "status">, cohort?: CohortDto) {
export function getRights(user: User, classe?: Pick<ClasseType, "status" | "schoolYear">, cohort?: CohortDto) {
if (!user || !classe) return {};
return {
canEdit:
([ROLES.ADMIN, ROLES.REFERENT_REGION].includes(user.role) && classe?.status !== STATUS_CLASSE.WITHDRAWN) ||
(([STATUS_CLASSE.CREATED, STATUS_CLASSE.VERIFIED] as (keyof typeof STATUS_CLASSE)[]).includes(classe?.status) &&
[ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(user.role)),
(classe?.status !== STATUS_CLASSE.WITHDRAWN && classe?.schoolYear === "2024-2025" && [ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(user.role)),
canEditEstimatedSeats: canEditEstimatedSeats(user),
canEditTotalSeats: canEditTotalSeats(user),
canEditColoration: [ROLES.ADMIN, ROLES.REFERENT_REGION].includes(user.role),
Expand Down
26 changes: 26 additions & 0 deletions api/migrations/20240910084532-fix-young-inscriptionStep2023.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { YoungModel } = require("../src/models");

module.exports = {
async up() {
const inscriptionStep = await YoungModel.updateMany(
{
inscriptionStep2023: "WAITING_CONSENT",
parentAllowSNU: "true",
$or: [{ parent1AllowSNU: "true" }, { parent2AllowSNU: "true" }],
},
{
$set: { inscriptionStep2023: "DONE" },
},
);
const reinscriptionStep2023 = await YoungModel.updateMany(
{
reinscriptionStep2023: "WAITING_CONSENT",
parentAllowSNU: "true",
$or: [{ parent1AllowSNU: "true" }, { parent2AllowSNU: "true" }],
},
{
$set: { reinscriptionStep2023: "DONE" },
},
);
},
};
37 changes: 35 additions & 2 deletions api/src/__tests__/referent.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fakerFR as faker } from "@faker-js/faker";
import request from "supertest";

import { ROLES, SENDINBLUE_TEMPLATES, YOUNG_STATUS } from "snu-lib";
import { ROLES, SENDINBLUE_TEMPLATES, YOUNG_STATUS, STATUS_CLASSE } from "snu-lib";

import { CohortModel, ReferentModel, YoungModel } from "../models";

Expand All @@ -26,6 +26,7 @@ import { createFixtureEtablissement } from "./fixtures/etablissement";
import { createCohortHelper } from "./helpers/cohort";
import getNewCohortFixture from "./fixtures/cohort";
import { ObjectId } from "bson";
import passport from "passport";

jest.mock("../utils", () => ({
...jest.requireActual("../utils"),
Expand Down Expand Up @@ -183,11 +184,43 @@ describe("Referent", () => {
.send({ youngIds, status: YOUNG_STATUS.VALIDATED });
expect(res.statusCode).toEqual(404);
});
it("should return 403 if user cannot validate youngs", async () => {
const userId = "123";
const etablissement = await createEtablissement(createFixtureEtablissement());
const cohort = await createCohortHelper(getNewCohortFixture({ name: "Juillet 2023" }));
const classe: any = await createClasse(
createFixtureClasse({ etablissementId: etablissement._id, referentClasseIds: [userId], cohort: cohort.name, cohortId: cohort._id, status: STATUS_CLASSE.OPEN }),
);
const young: any = await createYoungHelper(getNewYoungFixture({ source: "CLE", classeId: classe._id, cohort: classe.cohort, cohortId: cohort._id }));

const youngIds = [young._id.toString()];
const res = await request(getAppHelper({ role: ROLES.RESPONSIBLE }))
.put(`/referent/youngs`)
.send({ youngIds, status: YOUNG_STATUS.VALIDATED });
expect(res.statusCode).toEqual(403);
});
it("should return 403 if classe is not open", async () => {
const userId = "123";
const etablissement = await createEtablissement(createFixtureEtablissement());
const cohort = await createCohortHelper(getNewCohortFixture({ name: "Juillet 2023" }));
const classe: any = await createClasse(
createFixtureClasse({ etablissementId: etablissement._id, referentClasseIds: [userId], cohort: cohort.name, cohortId: cohort._id, status: STATUS_CLASSE.CLOSED }),
);
const young: any = await createYoungHelper(getNewYoungFixture({ source: "CLE", classeId: classe._id, cohort: classe.cohort, cohortId: cohort._id }));

const youngIds = [young._id.toString()];
const res = await request(getAppHelper({ role: ROLES.ADMINISTRATEUR_CLE }))
.put(`/referent/youngs`)
.send({ youngIds, status: YOUNG_STATUS.VALIDATED });
expect(res.statusCode).toEqual(403);
});
it("should return 200 if youngs updated", async () => {
const userId = "123";
const etablissement = await createEtablissement(createFixtureEtablissement());
const cohort = await createCohortHelper(getNewCohortFixture({ name: "Juillet 2023" }));
const classe: any = await createClasse(createFixtureClasse({ etablissementId: etablissement._id, referentClasseIds: [userId], cohort: cohort.name, cohortId: cohort._id }));
const classe: any = await createClasse(
createFixtureClasse({ etablissementId: etablissement._id, referentClasseIds: [userId], cohort: cohort.name, cohortId: cohort._id, status: STATUS_CLASSE.OPEN }),
);
const young: any = await createYoungHelper(getNewYoungFixture({ source: "CLE", classeId: classe._id, cohort: classe.cohort, cohortId: cohort._id }));

const youngIds = [young._id.toString()];
Expand Down
4 changes: 2 additions & 2 deletions api/src/cle/classe/classeService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,13 +415,13 @@ describe("canEditTotalSeats", () => {

it("should return false if user is ADMIN and date is before LIMIT_DATES_ESTIMATED_SEATS", () => {
const user = { role: ROLES.ADMIN };
jest.setSystemTime(new Date(LIMIT_DATE_ESTIMATED_SEATS.getTime() - 24 * 60 * 60 * 1000));
jest.setSystemTime(new Date(LIMIT_DATE_TOTAL_SEATS.getTime() - 24 * 60 * 60 * 1000));
expect(canEditTotalSeats(user)).toBe(false);
});

it("should return true if user is ADMIN and date is after LIMIT_DATES_ESTIMATED_SEATS", () => {
const user = { role: ROLES.ADMIN };
jest.setSystemTime(new Date(LIMIT_DATE_ESTIMATED_SEATS.getTime() + 24 * 60 * 60 * 1000));
jest.setSystemTime(new Date(LIMIT_DATE_TOTAL_SEATS.getTime() + 24 * 60 * 60 * 1000));
expect(canEditTotalSeats(user)).toBe(true);
});

Expand Down
7 changes: 5 additions & 2 deletions api/src/controllers/elasticsearch/cle/young.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ async function buildYoungCleContext(user) {
if (user.role === ROLES.ADMINISTRATEUR_CLE) {
const etablissement = await EtablissementModel.findOne({ $or: [{ coordinateurIds: user._id }, { referentEtablissementIds: user._id }] });
if (!etablissement) return { youngCleContextError: { status: 404, body: { ok: false, code: ERRORS.NOT_FOUND } } };
contextFilters.push({ term: { "etablissementId.keyword": etablissement._id.toString() } });
const classes = await ClasseModel.find({ etablissementId: etablissement._id, schoolYear: "2024-2025" });
if (!classes) return { youngCleContextError: { status: 404, body: { ok: false, code: ERRORS.NOT_FOUND } } };
contextFilters.push({ terms: { "classeId.keyword": classes.map((c) => c._id.toString()) } });
}

if (user.role === ROLES.REFERENT_CLASSE) {
const classes = await ClasseModel.find({ referentClasseIds: user._id });
const classes = await ClasseModel.find({ referentClasseIds: user._id, schoolYear: "2024-2025" });
if (!classes) return { youngCleContextError: { status: 404, body: { ok: false, code: ERRORS.NOT_FOUND } } };
contextFilters.push({ terms: { "classeId.keyword": classes.map((c) => c._id.toString()) } });
}

Expand Down
31 changes: 27 additions & 4 deletions api/src/referent/referentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ import {
EQUIVALENCE_STATUS,
YOUNG_SITUATIONS,
CLE_FILIERE,
canValidateMultipleYoungsInClass,
canValidateYoungInClass,
} from "snu-lib";
import { getFilteredSessions, getAllSessions } from "../utils/cohort";
import scanFile from "../utils/virusScanner";
Expand Down Expand Up @@ -587,6 +589,21 @@ router.put("/young/:id", passport.authenticate("referent", { session: false, fai
}
}

if (newYoung.status === YOUNG_STATUS.VALIDATED && !young.reinscriptionStep2023) {
newYoung.inscriptionStep2023 = "DONE";
}
if (newYoung.status === YOUNG_STATUS.VALIDATED && young.reinscriptionStep2023) {
newYoung.reinscriptionStep2023 = "DONE";
}

if (newYoung.status === YOUNG_STATUS.VALIDATED && young.source === YOUNG_SOURCE.CLE) {
const classe = await ClasseModel.findById(young.classeId);
if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND });
if (!canValidateYoungInClass(req.user, classe)) {
return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED });
}
}

young.set(newYoung);
await young.save({ fromUser: req.user });

Expand Down Expand Up @@ -629,13 +646,19 @@ router.put("/youngs", passport.authenticate("referent", { session: false, failWi

if (error) return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS });

if (![ROLES.REFERENT_CLASSE, ROLES.ADMINISTRATEUR_CLE].includes(req.user.role)) {
return res.status(403).send({ ok: false, code: ERRORS.FORBIDDEN });
}

const youngs = await YoungModel.find({ _id: { $in: payload.youngIds }, source: "CLE" });
if (!youngs) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND });
if (youngs.length !== payload.youngIds.length) return res.status(404).send({ ok: false, code: ERRORS.BAD_REQUEST });

const classeId = youngs[0].classeId;

const classe = await ClasseModel.findById(classeId);
if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND });

if (!canValidateMultipleYoungsInClass(req.user, classe)) {
return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED });
}

for (const young of youngs) {
young.set({ status: payload.status });
await young.save({ fromUser: req.user });
Expand Down
18 changes: 15 additions & 3 deletions packages/lib/src/roles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ReferentDto, UserDto } from "./dto";
import { region2department } from "./region-and-departments";
import { isNowBetweenDates } from "./utils/date";
import { LIMIT_DATE_ESTIMATED_SEATS, LIMIT_DATE_TOTAL_SEATS } from "./constants/constants";
import { LIMIT_DATE_ESTIMATED_SEATS, LIMIT_DATE_TOTAL_SEATS, STATUS_CLASSE } from "./constants/constants";
import { ClasseType } from "./mongoSchema";

const DURATION_BEFORE_EXPIRATION_2FA_MONCOMPTE_MS = 1000 * 60 * 15; // 15 minutes
const DURATION_BEFORE_EXPIRATION_2FA_ADMIN_MS = 1000 * 60 * 10; // 10 minutes
Expand Down Expand Up @@ -1001,8 +1002,8 @@ function canEditEstimatedSeats(actor) {
function canEditTotalSeats(actor) {
if (actor.role === ROLES.ADMIN) {
const now = new Date();
const limitDateEstimatedSeat = new Date(LIMIT_DATE_ESTIMATED_SEATS);
if (now <= limitDateEstimatedSeat) {
const limitDateTotalSeat = new Date(LIMIT_DATE_TOTAL_SEATS);
if (now <= limitDateTotalSeat) {
return false;
} else {
return true;
Expand All @@ -1028,6 +1029,15 @@ function canCreateEtablissement(user: UserDto) {
return [ROLES.ADMIN].includes(user.role);
}

//CLE
function canValidateMultipleYoungsInClass(actor: UserDto, classe: ClasseType) {
return [ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(actor.role) && classe.status === STATUS_CLASSE.OPEN;
}
function canValidateYoungInClass(actor: UserDto, classe: ClasseType) {
if (isAdmin(actor)) return true;
return [ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION].includes(actor.role) && classe.status === STATUS_CLASSE.OPEN;
}

export {
ROLES,
SUB_ROLES,
Expand Down Expand Up @@ -1181,4 +1191,6 @@ export {
canManageMig,
canUpdateReferentClasse,
canCreateEtablissement,
canValidateMultipleYoungsInClass,
canValidateYoungInClass,
};
2 changes: 1 addition & 1 deletion packages/lib/src/routes/cohort/get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CohortDto } from "src/dto";
import { CohortDto } from "../../dto";
import { BasicRoute, RouteResponseBody } from "..";

export interface GetOneCohortRoute extends BasicRoute {
Expand Down
4 changes: 2 additions & 2 deletions packages/lib/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export interface BasicRoute {

export type RouteResponseBody<T> = { ok: boolean; data?: T; code?: string; message?: string };

export { CohortsRoutes } from "./cohort";
export { ClassesRoutes } from "./cle/classe";
export type { CohortsRoutes } from "./cohort";
export type { ClassesRoutes } from "./cle/classe";

0 comments on commit 74d0d20

Please sign in to comment.