feat: Calculer automatiquement les moyennes après chaque saisie de notes
Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
This commit is contained in:
@@ -1,10 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { execSync } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
import { execWithRetry, runSql, clearCache, resolveDeterministicIds, createTestUser, composeFile } from './helpers';
|
||||
|
||||
const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4173';
|
||||
const urlMatch = baseUrl.match(/:(\d+)$/);
|
||||
@@ -15,48 +10,12 @@ const TEACHER_EMAIL = 'e2e-grade-teacher@example.com';
|
||||
const TEACHER_PASSWORD = 'GradeTest123';
|
||||
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
||||
|
||||
const projectRoot = join(__dirname, '../..');
|
||||
const composeFile = join(projectRoot, 'compose.yaml');
|
||||
|
||||
function runSql(sql: string) {
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "${sql}" 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
try {
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear paginated_queries.cache 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
} catch {
|
||||
// Cache pool may not exist
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDeterministicIds(): { schoolId: string; academicYearId: string } {
|
||||
const output = execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php -r '` +
|
||||
`require "/app/vendor/autoload.php"; ` +
|
||||
`$t="${TENANT_ID}"; $ns="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` +
|
||||
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"school-$t")->toString()."\\n"; ` +
|
||||
`$m=(int)date("n"); $s=$m>=9?(int)date("Y"):(int)date("Y")-1; $e=$s+1; ` +
|
||||
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"$t:$s-$e")->toString();` +
|
||||
`' 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
).trim();
|
||||
const [schoolId, academicYearId] = output.split('\n');
|
||||
return { schoolId: schoolId!, academicYearId: academicYearId! };
|
||||
}
|
||||
|
||||
async function loginAsTeacher(page: import('@playwright/test').Page) {
|
||||
await page.goto(`${ALPHA_URL}/login`);
|
||||
await page.locator('#email').fill(TEACHER_EMAIL);
|
||||
await page.locator('#password').fill(TEACHER_PASSWORD);
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/dashboard/, { timeout: 30000 }),
|
||||
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
|
||||
page.getByRole('button', { name: /se connecter/i }).click()
|
||||
]);
|
||||
}
|
||||
@@ -70,20 +29,16 @@ let student2Id: string;
|
||||
test.describe('Grade Input Grid (Story 6.2)', () => {
|
||||
test.beforeAll(async () => {
|
||||
// Create teacher user
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${TEACHER_EMAIL} --password=${TEACHER_PASSWORD} --role=ROLE_PROF 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
createTestUser('ecole-alpha', TEACHER_EMAIL, TEACHER_PASSWORD, 'ROLE_PROF');
|
||||
|
||||
const { schoolId, academicYearId } = resolveDeterministicIds();
|
||||
const { schoolId, academicYearId } = resolveDeterministicIds(TENANT_ID);
|
||||
|
||||
// Create test class
|
||||
const classOutput = execSync(
|
||||
const classOutput = execWithRetry(
|
||||
`docker compose -f "${composeFile}" exec -T php php -r '` +
|
||||
`require "/app/vendor/autoload.php"; ` +
|
||||
`echo Ramsey\\Uuid\\Uuid::uuid5("6ba7b814-9dad-11d1-80b4-00c04fd430c8","grade-class-${TENANT_ID}")->toString();` +
|
||||
`' 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
`' 2>&1`
|
||||
).trim();
|
||||
classId = classOutput;
|
||||
|
||||
@@ -93,12 +48,11 @@ test.describe('Grade Input Grid (Story 6.2)', () => {
|
||||
);
|
||||
|
||||
// Create test subject
|
||||
const subjectOutput = execSync(
|
||||
const subjectOutput = execWithRetry(
|
||||
`docker compose -f "${composeFile}" exec -T php php -r '` +
|
||||
`require "/app/vendor/autoload.php"; ` +
|
||||
`echo Ramsey\\Uuid\\Uuid::uuid5("6ba7b814-9dad-11d1-80b4-00c04fd430c8","grade-subject-${TENANT_ID}")->toString();` +
|
||||
`' 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
`' 2>&1`
|
||||
).trim();
|
||||
const subjectId = subjectOutput;
|
||||
|
||||
@@ -116,19 +70,12 @@ test.describe('Grade Input Grid (Story 6.2)', () => {
|
||||
);
|
||||
|
||||
// Create 2 test students
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=e2e-grade-student1@example.com --password=Student123 --role=ROLE_ELEVE --firstName=Alice --lastName=Durand 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=e2e-grade-student2@example.com --password=Student123 --role=ROLE_ELEVE --firstName=Bob --lastName=Martin 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
createTestUser('ecole-alpha', 'e2e-grade-student1@example.com', 'Student123', 'ROLE_ELEVE --firstName=Alice --lastName=Durand');
|
||||
createTestUser('ecole-alpha', 'e2e-grade-student2@example.com', 'Student123', 'ROLE_ELEVE --firstName=Bob --lastName=Martin');
|
||||
|
||||
// Assign students to class
|
||||
const studentIds = execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "SELECT id FROM users WHERE email IN ('e2e-grade-student1@example.com','e2e-grade-student2@example.com') AND tenant_id='${TENANT_ID}' ORDER BY email" 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
const studentIds = execWithRetry(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "SELECT id FROM users WHERE email IN ('e2e-grade-student1@example.com','e2e-grade-student2@example.com') AND tenant_id='${TENANT_ID}' ORDER BY email" 2>&1`
|
||||
);
|
||||
const idMatches = studentIds.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g);
|
||||
if (idMatches && idMatches.length >= 2) {
|
||||
@@ -147,12 +94,11 @@ test.describe('Grade Input Grid (Story 6.2)', () => {
|
||||
);
|
||||
|
||||
// Create test evaluation
|
||||
const evalOutput = execSync(
|
||||
const evalOutput = execWithRetry(
|
||||
`docker compose -f "${composeFile}" exec -T php php -r '` +
|
||||
`require "/app/vendor/autoload.php"; ` +
|
||||
`echo Ramsey\\Uuid\\Uuid::uuid5("6ba7b814-9dad-11d1-80b4-00c04fd430c8","grade-eval-${TENANT_ID}")->toString();` +
|
||||
`' 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
`' 2>&1`
|
||||
).trim();
|
||||
evaluationId = evalOutput;
|
||||
|
||||
@@ -236,9 +182,10 @@ test.describe('Grade Input Grid (Story 6.2)', () => {
|
||||
await expect(page.locator('.grade-input').first()).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const firstInput = page.locator('.grade-input').first();
|
||||
await firstInput.fill('/abs');
|
||||
await firstInput.clear();
|
||||
await firstInput.pressSequentially('/abs');
|
||||
|
||||
await expect(page.locator('.status-absent').first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.locator('.status-absent').first()).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
|
||||
test('/disp marks student as dispensed', async ({ page }) => {
|
||||
@@ -247,9 +194,10 @@ test.describe('Grade Input Grid (Story 6.2)', () => {
|
||||
await expect(page.locator('.grade-input').first()).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const firstInput = page.locator('.grade-input').first();
|
||||
await firstInput.fill('/disp');
|
||||
await firstInput.clear();
|
||||
await firstInput.pressSequentially('/disp');
|
||||
|
||||
await expect(page.locator('.status-dispensed').first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.locator('.status-dispensed').first()).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user