feat: Calculer automatiquement les moyennes après chaque saisie de notes
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

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:
2026-03-30 06:22:03 +02:00
parent b70d5ec2ad
commit b7dc27f2a5
786 changed files with 118783 additions and 316 deletions

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Scolarite\Domain\Model\Grade\AppreciationTemplate;
use App\Scolarite\Infrastructure\Api\Processor\CreateAppreciationTemplateProcessor;
use App\Scolarite\Infrastructure\Api\Processor\DeleteAppreciationTemplateProcessor;
use App\Scolarite\Infrastructure\Api\Processor\UpdateAppreciationTemplateProcessor;
use App\Scolarite\Infrastructure\Api\Provider\AppreciationTemplateCollectionProvider;
use DateTimeImmutable;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
shortName: 'AppreciationTemplate',
operations: [
new GetCollection(
uriTemplate: '/me/appreciation-templates',
provider: AppreciationTemplateCollectionProvider::class,
name: 'get_appreciation_templates',
),
new Post(
uriTemplate: '/me/appreciation-templates',
read: false,
processor: CreateAppreciationTemplateProcessor::class,
validationContext: ['groups' => ['Default', 'create']],
name: 'create_appreciation_template',
),
new Put(
uriTemplate: '/me/appreciation-templates/{id}',
read: false,
processor: UpdateAppreciationTemplateProcessor::class,
validationContext: ['groups' => ['Default', 'update']],
name: 'update_appreciation_template',
),
new Delete(
uriTemplate: '/me/appreciation-templates/{id}',
read: false,
processor: DeleteAppreciationTemplateProcessor::class,
name: 'delete_appreciation_template',
),
],
)]
final class AppreciationTemplateResource
{
#[ApiProperty(identifier: true)]
public ?string $id = null;
#[Assert\NotBlank(groups: ['create', 'update'])]
#[Assert\Length(max: 100, groups: ['create', 'update'])]
public ?string $title = null;
#[Assert\NotBlank(groups: ['create', 'update'])]
#[Assert\Length(max: 500, groups: ['create', 'update'])]
public ?string $content = null;
public ?string $category = null;
public ?int $usageCount = null;
public ?DateTimeImmutable $createdAt = null;
public ?DateTimeImmutable $updatedAt = null;
public static function fromDomain(AppreciationTemplate $template): self
{
$resource = new self();
$resource->id = (string) $template->id;
$resource->title = $template->title;
$resource->content = $template->content;
$resource->category = $template->category?->value;
$resource->usageCount = $template->usageCount;
$resource->createdAt = $template->createdAt;
$resource->updatedAt = $template->updatedAt;
return $resource;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\Scolarite\Infrastructure\Api\Provider\ClassStatisticsProvider;
#[ApiResource(
shortName: 'ClassStatistics',
operations: [
new Get(
uriTemplate: '/classes/{classId}/statistics',
provider: ClassStatisticsProvider::class,
name: 'get_class_statistics',
),
],
)]
final class ClassStatisticsResource
{
#[ApiProperty(identifier: true)]
public ?string $classId = null;
public ?string $periodId = null;
/** @var list<array{evaluationId: string, title: string, average: float|null, min: float|null, max: float|null, median: float|null, gradedCount: int}> */
public array $evaluations = [];
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use App\Scolarite\Infrastructure\Api\Processor\LinkCompetenciesToEvaluationProcessor;
use App\Scolarite\Infrastructure\Api\Provider\CompetencyEvaluationCollectionProvider;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
shortName: 'CompetencyEvaluation',
operations: [
new GetCollection(
uriTemplate: '/evaluations/{evaluationId}/competencies',
uriVariables: ['evaluationId'],
provider: CompetencyEvaluationCollectionProvider::class,
name: 'get_evaluation_competencies',
),
new Post(
uriTemplate: '/evaluations/{evaluationId}/competencies',
uriVariables: ['evaluationId'],
read: false,
processor: LinkCompetenciesToEvaluationProcessor::class,
validationContext: ['groups' => ['Default', 'create']],
name: 'link_competencies_to_evaluation',
),
],
)]
final class CompetencyEvaluationResource
{
#[ApiProperty(identifier: true)]
public ?string $id = null;
public ?string $evaluationId = null;
public ?string $competencyId = null;
public ?string $competencyCode = null;
public ?string $competencyName = null;
/** @var array<string>|null */
#[Assert\NotBlank(message: 'Les compétences sont requises.', groups: ['create'])]
public ?array $competencyIds = null;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use App\Scolarite\Infrastructure\Api\Provider\CompetencyLevelCollectionProvider;
#[ApiResource(
shortName: 'CompetencyLevel',
operations: [
new GetCollection(
uriTemplate: '/competency-levels',
provider: CompetencyLevelCollectionProvider::class,
name: 'get_competency_level_list',
),
],
)]
final class CompetencyLevelResource
{
#[ApiProperty(identifier: true)]
public ?string $code = null;
public ?string $name = null;
public ?string $color = null;
public int $sortOrder = 0;
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use App\Scolarite\Infrastructure\Api\Provider\CompetencyCollectionProvider;
#[ApiResource(
shortName: 'Competency',
operations: [
new GetCollection(
uriTemplate: '/competencies',
provider: CompetencyCollectionProvider::class,
name: 'get_competency_list',
),
],
)]
final class CompetencyResource
{
#[ApiProperty(identifier: true)]
public ?string $id = null;
public ?string $frameworkId = null;
public ?string $frameworkName = null;
public ?string $code = null;
public ?string $name = null;
public ?string $description = null;
public ?string $parentId = null;
public int $sortOrder = 0;
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Put;
use App\Scolarite\Infrastructure\Api\Processor\SaveCompetencyResultsProcessor;
use App\Scolarite\Infrastructure\Api\Provider\CompetencyResultCollectionProvider;
#[ApiResource(
shortName: 'CompetencyResult',
operations: [
new GetCollection(
uriTemplate: '/evaluations/{evaluationId}/competency-results',
uriVariables: ['evaluationId'],
provider: CompetencyResultCollectionProvider::class,
name: 'get_competency_result_list',
),
new Put(
uriTemplate: '/evaluations/{evaluationId}/competency-results',
uriVariables: ['evaluationId'],
read: false,
processor: SaveCompetencyResultsProcessor::class,
name: 'save_competency_results',
),
],
)]
final class CompetencyResultResource
{
#[ApiProperty(identifier: true)]
public ?string $id = null;
public ?string $evaluationId = null;
public ?string $competencyEvaluationId = null;
public ?string $competencyId = null;
public ?string $competencyCode = null;
public ?string $competencyName = null;
public ?string $studentId = null;
public ?string $studentName = null;
public ?string $levelCode = null;
public ?string $levelName = null;
/** @var array<array{studentId: string, competencyEvaluationId: string, levelCode: string}>|null */
public ?array $results = null;
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\Scolarite\Domain\Model\Evaluation\ClassStatistics;
use App\Scolarite\Infrastructure\Api\Provider\EvaluationStatisticsProvider;
#[ApiResource(
shortName: 'EvaluationStatistics',
operations: [
new Get(
uriTemplate: '/evaluations/{evaluationId}/statistics',
provider: EvaluationStatisticsProvider::class,
name: 'get_evaluation_statistics',
),
],
)]
final class EvaluationStatisticsResource
{
#[ApiProperty(identifier: true)]
public ?string $evaluationId = null;
public ?float $average = null;
public ?float $min = null;
public ?float $max = null;
public ?float $median = null;
public int $gradedCount = 0;
public static function fromStatistics(string $evaluationId, ClassStatistics $statistics): self
{
$resource = new self();
$resource->evaluationId = $evaluationId;
$resource->average = $statistics->average;
$resource->min = $statistics->min;
$resource->max = $statistics->max;
$resource->median = $statistics->median;
$resource->gradedCount = $statistics->gradedCount;
return $resource;
}
}

View File

@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Scolarite\Domain\Model\Grade\Grade;
use App\Scolarite\Infrastructure\Api\Processor\PublishGradesProcessor;
use App\Scolarite\Infrastructure\Api\Processor\SaveAppreciationProcessor;
use App\Scolarite\Infrastructure\Api\Processor\SaveGradesProcessor;
use App\Scolarite\Infrastructure\Api\Provider\GradeCollectionProvider;
use DateTimeImmutable;
@@ -32,6 +33,12 @@ use DateTimeImmutable;
processor: SaveGradesProcessor::class,
name: 'save_evaluation_grades',
),
new Put(
uriTemplate: '/grades/{id}/appreciation',
read: false,
processor: SaveAppreciationProcessor::class,
name: 'save_grade_appreciation',
),
new Post(
uriTemplate: '/evaluations/{evaluationId}/publish',
uriVariables: ['evaluationId'],
@@ -56,6 +63,8 @@ final class GradeResource
public ?string $status = null;
public ?string $appreciation = null;
public ?DateTimeImmutable $createdAt = null;
public ?DateTimeImmutable $updatedAt = null;
@@ -76,6 +85,7 @@ final class GradeResource
$resource->studentName = $studentName;
$resource->value = $grade->value?->value;
$resource->status = $grade->status->value;
$resource->appreciation = $grade->appreciation;
$resource->createdAt = $grade->createdAt;
$resource->updatedAt = $grade->updatedAt;

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\Scolarite\Infrastructure\Api\Provider\StudentAveragesProvider;
#[ApiResource(
shortName: 'StudentAverages',
operations: [
new Get(
uriTemplate: '/students/{studentId}/averages',
provider: StudentAveragesProvider::class,
name: 'get_student_averages',
),
],
)]
final class StudentAveragesResource
{
#[ApiProperty(identifier: true)]
public ?string $studentId = null;
public ?string $periodId = null;
/** @var list<array{subjectId: string, subjectName: string|null, average: float, gradeCount: int}> */
public array $subjectAverages = [];
public ?float $generalAverage = null;
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use App\Scolarite\Infrastructure\Api\Provider\StudentCompetencyProgressProvider;
#[ApiResource(
shortName: 'StudentCompetencyProgress',
operations: [
new GetCollection(
uriTemplate: '/students/{studentId}/competency-progress',
uriVariables: ['studentId'],
provider: StudentCompetencyProgressProvider::class,
name: 'get_student_competency_progress',
),
],
)]
final class StudentCompetencyProgressResource
{
#[ApiProperty(identifier: true)]
public ?string $competencyId = null;
public ?string $competencyCode = null;
public ?string $competencyName = null;
public ?string $currentLevelCode = null;
public ?string $currentLevelName = null;
/** @var array<array{date: string, levelCode: string, evaluationTitle: string}> */
public array $history = [];
}