feat: Permettre à l'enseignant de créer et gérer ses évaluations
Les enseignants avaient besoin de définir les critères de notation (barème, coefficient) avant de pouvoir saisir des notes. Sans cette brique, le module Notes & Évaluations (Epic 6) ne pouvait pas démarrer. L'évaluation est un agrégat du bounded context Scolarité avec deux Value Objects (GradeScale 1-100, Coefficient 0.1-10). Le barème est verrouillé dès qu'une note existe pour éviter les incohérences. Un port EvaluationGradesChecker (stub pour l'instant) sera branché sur le repository de notes dans la story 6.2.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\CreateEvaluation;
|
||||
|
||||
final readonly class CreateEvaluationCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $teacherId,
|
||||
public string $title,
|
||||
public ?string $description,
|
||||
public string $evaluationDate,
|
||||
public int $gradeScale = 20,
|
||||
public float $coefficient = 1.0,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\CreateEvaluation;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
|
||||
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class CreateEvaluationHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EvaluationRepository $evaluationRepository,
|
||||
private EnseignantAffectationChecker $affectationChecker,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(CreateEvaluationCommand $command): Evaluation
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$classId = ClassId::fromString($command->classId);
|
||||
$subjectId = SubjectId::fromString($command->subjectId);
|
||||
$teacherId = UserId::fromString($command->teacherId);
|
||||
$now = $this->clock->now();
|
||||
|
||||
if (!$this->affectationChecker->estAffecte($teacherId, $classId, $subjectId, $tenantId)) {
|
||||
throw EnseignantNonAffecteException::pourClasseEtMatiere($teacherId, $classId, $subjectId);
|
||||
}
|
||||
|
||||
$evaluation = Evaluation::creer(
|
||||
tenantId: $tenantId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
teacherId: $teacherId,
|
||||
title: $command->title,
|
||||
description: $command->description,
|
||||
evaluationDate: new DateTimeImmutable($command->evaluationDate),
|
||||
gradeScale: new GradeScale($command->gradeScale),
|
||||
coefficient: new Coefficient($command->coefficient),
|
||||
now: $now,
|
||||
);
|
||||
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\DeleteEvaluation;
|
||||
|
||||
final readonly class DeleteEvaluationCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $evaluationId,
|
||||
public string $teacherId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\DeleteEvaluation;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class DeleteEvaluationHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EvaluationRepository $evaluationRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(DeleteEvaluationCommand $command): Evaluation
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$evaluationId = EvaluationId::fromString($command->evaluationId);
|
||||
|
||||
$evaluation = $this->evaluationRepository->get($evaluationId, $tenantId);
|
||||
|
||||
$teacherId = UserId::fromString($command->teacherId);
|
||||
if ((string) $evaluation->teacherId !== (string) $teacherId) {
|
||||
throw NonProprietaireDeLEvaluationException::withId($evaluationId);
|
||||
}
|
||||
|
||||
$evaluation->supprimer($this->clock->now());
|
||||
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\UpdateEvaluation;
|
||||
|
||||
final readonly class UpdateEvaluationCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $evaluationId,
|
||||
public string $teacherId,
|
||||
public string $title,
|
||||
public ?string $description,
|
||||
public string $evaluationDate,
|
||||
public float $coefficient = 1.0,
|
||||
public ?int $gradeScale = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\UpdateEvaluation;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Application\Port\EvaluationGradesChecker;
|
||||
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class UpdateEvaluationHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EvaluationRepository $evaluationRepository,
|
||||
private EvaluationGradesChecker $gradesChecker,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(UpdateEvaluationCommand $command): Evaluation
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$evaluationId = EvaluationId::fromString($command->evaluationId);
|
||||
$now = $this->clock->now();
|
||||
|
||||
$evaluation = $this->evaluationRepository->get($evaluationId, $tenantId);
|
||||
|
||||
$teacherId = UserId::fromString($command->teacherId);
|
||||
if ((string) $evaluation->teacherId !== (string) $teacherId) {
|
||||
throw NonProprietaireDeLEvaluationException::withId($evaluationId);
|
||||
}
|
||||
|
||||
$hasGrades = $this->gradesChecker->hasGrades($evaluationId, $tenantId);
|
||||
|
||||
$evaluation->modifier(
|
||||
title: $command->title,
|
||||
description: $command->description,
|
||||
coefficient: new Coefficient($command->coefficient),
|
||||
evaluationDate: new DateTimeImmutable($command->evaluationDate),
|
||||
gradeScale: $command->gradeScale !== null ? new GradeScale($command->gradeScale) : null,
|
||||
hasGrades: $hasGrades,
|
||||
now: $now,
|
||||
);
|
||||
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Port;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface EvaluationGradesChecker
|
||||
{
|
||||
public function hasGrades(EvaluationId $evaluationId, TenantId $tenantId): bool;
|
||||
}
|
||||
37
backend/src/Scolarite/Domain/Event/EvaluationCreee.php
Normal file
37
backend/src/Scolarite/Domain/Event/EvaluationCreee.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EvaluationCreee implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EvaluationId $evaluationId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $teacherId,
|
||||
public string $title,
|
||||
public DateTimeImmutable $evaluationDate,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->evaluationId->value;
|
||||
}
|
||||
}
|
||||
34
backend/src/Scolarite/Domain/Event/EvaluationModifiee.php
Normal file
34
backend/src/Scolarite/Domain/Event/EvaluationModifiee.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EvaluationModifiee implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EvaluationId $evaluationId,
|
||||
public string $title,
|
||||
public DateTimeImmutable $evaluationDate,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->evaluationId->value;
|
||||
}
|
||||
}
|
||||
32
backend/src/Scolarite/Domain/Event/EvaluationSupprimee.php
Normal file
32
backend/src/Scolarite/Domain/Event/EvaluationSupprimee.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EvaluationSupprimee implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EvaluationId $evaluationId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->evaluationId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class BaremeInvalideException extends DomainException
|
||||
{
|
||||
public static function avecValeur(int $maxValue): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le barème doit être compris entre 1 et 100, %d donné.',
|
||||
$maxValue,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class BaremeNonModifiableException extends DomainException
|
||||
{
|
||||
public static function carNotesExistantes(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le barème de l\'évaluation "%s" ne peut pas être modifié car des notes ont déjà été saisies.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class CoefficientInvalideException extends DomainException
|
||||
{
|
||||
public static function avecValeur(float $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le coefficient doit être compris entre 0.1 et 10, %s donné.',
|
||||
$value,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EvaluationDejaSupprimeeException extends DomainException
|
||||
{
|
||||
public static function withId(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'évaluation avec l\'ID "%s" est déjà supprimée.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EvaluationNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'évaluation avec l\'ID "%s" n\'a pas été trouvée.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class NonProprietaireDeLEvaluationException extends DomainException
|
||||
{
|
||||
public static function withId(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Vous n\'êtes pas le propriétaire de l\'évaluation "%s".',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
|
||||
|
||||
final readonly class Coefficient
|
||||
{
|
||||
public function __construct(
|
||||
public float $value,
|
||||
) {
|
||||
if ($value < 0.1 || $value > 10) {
|
||||
throw CoefficientInvalideException::avecValeur($value);
|
||||
}
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
168
backend/src/Scolarite/Domain/Model/Evaluation/Evaluation.php
Normal file
168
backend/src/Scolarite/Domain/Model/Evaluation/Evaluation.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Event\EvaluationCreee;
|
||||
use App\Scolarite\Domain\Event\EvaluationModifiee;
|
||||
use App\Scolarite\Domain\Event\EvaluationSupprimee;
|
||||
use App\Scolarite\Domain\Exception\BaremeNonModifiableException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class Evaluation extends AggregateRoot
|
||||
{
|
||||
public private(set) DateTimeImmutable $updatedAt;
|
||||
|
||||
private function __construct(
|
||||
public private(set) EvaluationId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) ClassId $classId,
|
||||
public private(set) SubjectId $subjectId,
|
||||
public private(set) UserId $teacherId,
|
||||
public private(set) string $title,
|
||||
public private(set) ?string $description,
|
||||
public private(set) DateTimeImmutable $evaluationDate,
|
||||
public private(set) GradeScale $gradeScale,
|
||||
public private(set) Coefficient $coefficient,
|
||||
public private(set) EvaluationStatus $status,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
$this->updatedAt = $createdAt;
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
TenantId $tenantId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
UserId $teacherId,
|
||||
string $title,
|
||||
?string $description,
|
||||
DateTimeImmutable $evaluationDate,
|
||||
GradeScale $gradeScale,
|
||||
Coefficient $coefficient,
|
||||
DateTimeImmutable $now,
|
||||
): self {
|
||||
$evaluation = new self(
|
||||
id: EvaluationId::generate(),
|
||||
tenantId: $tenantId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
teacherId: $teacherId,
|
||||
title: $title,
|
||||
description: $description,
|
||||
evaluationDate: $evaluationDate,
|
||||
gradeScale: $gradeScale,
|
||||
coefficient: $coefficient,
|
||||
status: EvaluationStatus::PUBLISHED,
|
||||
createdAt: $now,
|
||||
);
|
||||
|
||||
$evaluation->recordEvent(new EvaluationCreee(
|
||||
evaluationId: $evaluation->id,
|
||||
classId: (string) $classId,
|
||||
subjectId: (string) $subjectId,
|
||||
teacherId: (string) $teacherId,
|
||||
title: $title,
|
||||
evaluationDate: $evaluationDate,
|
||||
occurredOn: $now,
|
||||
));
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
|
||||
public function modifier(
|
||||
string $title,
|
||||
?string $description,
|
||||
Coefficient $coefficient,
|
||||
DateTimeImmutable $evaluationDate,
|
||||
?GradeScale $gradeScale,
|
||||
bool $hasGrades,
|
||||
DateTimeImmutable $now,
|
||||
): void {
|
||||
if ($this->status === EvaluationStatus::DELETED) {
|
||||
throw EvaluationDejaSupprimeeException::withId($this->id);
|
||||
}
|
||||
|
||||
if ($gradeScale !== null && !$this->gradeScale->equals($gradeScale) && $hasGrades) {
|
||||
throw BaremeNonModifiableException::carNotesExistantes($this->id);
|
||||
}
|
||||
|
||||
$this->title = $title;
|
||||
$this->description = $description;
|
||||
$this->coefficient = $coefficient;
|
||||
$this->evaluationDate = $evaluationDate;
|
||||
|
||||
if ($gradeScale !== null && !$hasGrades) {
|
||||
$this->gradeScale = $gradeScale;
|
||||
}
|
||||
|
||||
$this->updatedAt = $now;
|
||||
|
||||
$this->recordEvent(new EvaluationModifiee(
|
||||
evaluationId: $this->id,
|
||||
title: $title,
|
||||
evaluationDate: $evaluationDate,
|
||||
occurredOn: $now,
|
||||
));
|
||||
}
|
||||
|
||||
public function supprimer(DateTimeImmutable $now): void
|
||||
{
|
||||
if ($this->status === EvaluationStatus::DELETED) {
|
||||
throw EvaluationDejaSupprimeeException::withId($this->id);
|
||||
}
|
||||
|
||||
$this->status = EvaluationStatus::DELETED;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
$this->recordEvent(new EvaluationSupprimee(
|
||||
evaluationId: $this->id,
|
||||
occurredOn: $now,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
EvaluationId $id,
|
||||
TenantId $tenantId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
UserId $teacherId,
|
||||
string $title,
|
||||
?string $description,
|
||||
DateTimeImmutable $evaluationDate,
|
||||
GradeScale $gradeScale,
|
||||
Coefficient $coefficient,
|
||||
EvaluationStatus $status,
|
||||
DateTimeImmutable $createdAt,
|
||||
DateTimeImmutable $updatedAt,
|
||||
): self {
|
||||
$evaluation = new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
teacherId: $teacherId,
|
||||
title: $title,
|
||||
description: $description,
|
||||
evaluationDate: $evaluationDate,
|
||||
gradeScale: $gradeScale,
|
||||
coefficient: $coefficient,
|
||||
status: $status,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$evaluation->updatedAt = $updatedAt;
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class EvaluationId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
enum EvaluationStatus: string
|
||||
{
|
||||
case PUBLISHED = 'published';
|
||||
case DELETED = 'deleted';
|
||||
}
|
||||
30
backend/src/Scolarite/Domain/Model/Evaluation/GradeScale.php
Normal file
30
backend/src/Scolarite/Domain/Model/Evaluation/GradeScale.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Scolarite\Domain\Exception\BaremeInvalideException;
|
||||
|
||||
use function round;
|
||||
|
||||
final readonly class GradeScale
|
||||
{
|
||||
public function __construct(
|
||||
public int $maxValue,
|
||||
) {
|
||||
if ($maxValue < 1 || $maxValue > 100) {
|
||||
throw BaremeInvalideException::avecValeur($maxValue);
|
||||
}
|
||||
}
|
||||
|
||||
public function convertTo20(float $grade): float
|
||||
{
|
||||
return round(($grade / $this->maxValue) * 20, 2);
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->maxValue === $other->maxValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface EvaluationRepository
|
||||
{
|
||||
public function save(Evaluation $evaluation): void;
|
||||
|
||||
/** @throws EvaluationNotFoundException */
|
||||
public function get(EvaluationId $id, TenantId $tenantId): Evaluation;
|
||||
|
||||
public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation;
|
||||
|
||||
/** @return array<Evaluation> */
|
||||
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array;
|
||||
|
||||
/** @return array<Evaluation> */
|
||||
public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array;
|
||||
|
||||
/** @return array<Evaluation> */
|
||||
public function findByClass(ClassId $classId, TenantId $tenantId): array;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Command\CreateEvaluation\CreateEvaluationCommand;
|
||||
use App\Scolarite\Application\Command\CreateEvaluation\CreateEvaluationHandler;
|
||||
use App\Scolarite\Domain\Exception\BaremeInvalideException;
|
||||
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
|
||||
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<EvaluationResource, EvaluationResource>
|
||||
*/
|
||||
final readonly class CreateEvaluationProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CreateEvaluationHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EvaluationResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
try {
|
||||
$command = new CreateEvaluationCommand(
|
||||
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
|
||||
classId: $data->classId ?? '',
|
||||
subjectId: $data->subjectId ?? '',
|
||||
teacherId: $user->userId(),
|
||||
title: $data->title ?? '',
|
||||
description: $data->description,
|
||||
evaluationDate: $data->evaluationDate ?? '',
|
||||
gradeScale: $data->gradeScale ?? 20,
|
||||
coefficient: $data->coefficient ?? 1.0,
|
||||
);
|
||||
|
||||
$evaluation = ($this->handler)($command);
|
||||
|
||||
foreach ($evaluation->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return EvaluationResource::fromDomain($evaluation);
|
||||
} catch (EnseignantNonAffecteException|BaremeInvalideException|CoefficientInvalideException $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
} catch (InvalidUuidStringException $e) {
|
||||
throw new BadRequestHttpException('UUID invalide : ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Command\DeleteEvaluation\DeleteEvaluationCommand;
|
||||
use App\Scolarite\Application\Command\DeleteEvaluation\DeleteEvaluationHandler;
|
||||
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<EvaluationResource, EvaluationResource>
|
||||
*/
|
||||
final readonly class DeleteEvaluationProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DeleteEvaluationHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EvaluationResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
/** @var string $id */
|
||||
$id = $uriVariables['id'];
|
||||
|
||||
try {
|
||||
$command = new DeleteEvaluationCommand(
|
||||
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
|
||||
evaluationId: $id,
|
||||
teacherId: $user->userId(),
|
||||
);
|
||||
|
||||
$evaluation = ($this->handler)($command);
|
||||
|
||||
foreach ($evaluation->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return EvaluationResource::fromDomain($evaluation);
|
||||
} catch (EvaluationNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
} catch (NonProprietaireDeLEvaluationException $e) {
|
||||
throw new AccessDeniedHttpException($e->getMessage());
|
||||
} catch (EvaluationDejaSupprimeeException $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Command\UpdateEvaluation\UpdateEvaluationCommand;
|
||||
use App\Scolarite\Application\Command\UpdateEvaluation\UpdateEvaluationHandler;
|
||||
use App\Scolarite\Domain\Exception\BaremeInvalideException;
|
||||
use App\Scolarite\Domain\Exception\BaremeNonModifiableException;
|
||||
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<EvaluationResource, EvaluationResource>
|
||||
*/
|
||||
final readonly class UpdateEvaluationProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UpdateEvaluationHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EvaluationResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
/** @var string $id */
|
||||
$id = $uriVariables['id'];
|
||||
|
||||
try {
|
||||
$command = new UpdateEvaluationCommand(
|
||||
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
|
||||
evaluationId: $id,
|
||||
teacherId: $user->userId(),
|
||||
title: $data->title ?? '',
|
||||
description: $data->description,
|
||||
evaluationDate: $data->evaluationDate ?? '',
|
||||
coefficient: $data->coefficient ?? 1.0,
|
||||
gradeScale: $data->gradeScale,
|
||||
);
|
||||
|
||||
$evaluation = ($this->handler)($command);
|
||||
|
||||
foreach ($evaluation->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return EvaluationResource::fromDomain($evaluation);
|
||||
} catch (EvaluationNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
} catch (NonProprietaireDeLEvaluationException $e) {
|
||||
throw new AccessDeniedHttpException($e->getMessage());
|
||||
} catch (EvaluationDejaSupprimeeException|BaremeNonModifiableException|BaremeInvalideException|CoefficientInvalideException $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
|
||||
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
use function is_string;
|
||||
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<EvaluationResource>
|
||||
*/
|
||||
final readonly class EvaluationCollectionProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TenantContext $tenantContext,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<EvaluationResource> */
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
$teacherId = $user->userId();
|
||||
|
||||
/** @var array<string, mixed> $filters */
|
||||
$filters = $context['filters'] ?? [];
|
||||
$classIdFilter = $filters['classId'] ?? null;
|
||||
$search = $filters['search'] ?? null;
|
||||
|
||||
$sql = 'SELECT e.*, c.name AS class_name, s.name AS subject_name
|
||||
FROM evaluations e
|
||||
LEFT JOIN school_classes c ON c.id = e.class_id
|
||||
LEFT JOIN subjects s ON s.id = e.subject_id
|
||||
WHERE e.teacher_id = :teacher_id
|
||||
AND e.tenant_id = :tenant_id
|
||||
AND e.status != :deleted';
|
||||
|
||||
$params = [
|
||||
'teacher_id' => $teacherId,
|
||||
'tenant_id' => $tenantId,
|
||||
'deleted' => EvaluationStatus::DELETED->value,
|
||||
];
|
||||
|
||||
if (is_string($classIdFilter) && $classIdFilter !== '') {
|
||||
try {
|
||||
ClassId::fromString($classIdFilter);
|
||||
} catch (InvalidUuidStringException $e) {
|
||||
throw new BadRequestHttpException('UUID de classe invalide : ' . $e->getMessage());
|
||||
}
|
||||
$sql .= ' AND e.class_id = :class_id';
|
||||
$params['class_id'] = $classIdFilter;
|
||||
}
|
||||
|
||||
if (is_string($search) && $search !== '') {
|
||||
$sql .= ' AND e.title ILIKE :search';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY e.evaluation_date DESC';
|
||||
|
||||
$rows = $this->connection->fetchAllAssociative($sql, $params);
|
||||
|
||||
return array_map(static function (array $row): EvaluationResource {
|
||||
/** @var string $className */
|
||||
$className = $row['class_name'] ?? null;
|
||||
/** @var string $subjectName */
|
||||
$subjectName = $row['subject_name'] ?? null;
|
||||
|
||||
$evaluation = self::hydrateEvaluation($row);
|
||||
|
||||
return EvaluationResource::fromDomain($evaluation, $className, $subjectName);
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row */
|
||||
private static function hydrateEvaluation(array $row): Evaluation
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $row['id'];
|
||||
/** @var string $tenantId */
|
||||
$tenantId = $row['tenant_id'];
|
||||
/** @var string $classId */
|
||||
$classId = $row['class_id'];
|
||||
/** @var string $subjectId */
|
||||
$subjectId = $row['subject_id'];
|
||||
/** @var string $teacherId */
|
||||
$teacherId = $row['teacher_id'];
|
||||
/** @var string $title */
|
||||
$title = $row['title'];
|
||||
/** @var string|null $description */
|
||||
$description = $row['description'];
|
||||
/** @var string $evaluationDate */
|
||||
$evaluationDate = $row['evaluation_date'];
|
||||
/** @var string|int $gradeScaleRaw */
|
||||
$gradeScaleRaw = $row['grade_scale'];
|
||||
/** @var string|float $coefficientRaw */
|
||||
$coefficientRaw = $row['coefficient'];
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
/** @var string $createdAt */
|
||||
$createdAt = $row['created_at'];
|
||||
/** @var string $updatedAt */
|
||||
$updatedAt = $row['updated_at'];
|
||||
|
||||
return Evaluation::reconstitute(
|
||||
id: EvaluationId::fromString($id),
|
||||
tenantId: TenantId::fromString($tenantId),
|
||||
classId: ClassId::fromString($classId),
|
||||
subjectId: SubjectId::fromString($subjectId),
|
||||
teacherId: UserId::fromString($teacherId),
|
||||
title: $title,
|
||||
description: $description,
|
||||
evaluationDate: new DateTimeImmutable($evaluationDate),
|
||||
gradeScale: new GradeScale((int) $gradeScaleRaw),
|
||||
coefficient: new Coefficient((float) $coefficientRaw),
|
||||
status: EvaluationStatus::from($status),
|
||||
createdAt: new DateTimeImmutable($createdAt),
|
||||
updatedAt: new DateTimeImmutable($updatedAt),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Administration\Domain\Repository\ClassRepository;
|
||||
use App\Administration\Domain\Repository\SubjectRepository;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<EvaluationResource>
|
||||
*/
|
||||
final readonly class EvaluationItemProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EvaluationRepository $evaluationRepository,
|
||||
private TenantContext $tenantContext,
|
||||
private Security $security,
|
||||
private ClassRepository $classRepository,
|
||||
private SubjectRepository $subjectRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
/** @var string $id */
|
||||
$id = $uriVariables['id'];
|
||||
|
||||
$evaluation = $this->evaluationRepository->findById(
|
||||
EvaluationId::fromString($id),
|
||||
$this->tenantContext->getCurrentTenantId(),
|
||||
);
|
||||
|
||||
if ($evaluation === null) {
|
||||
throw new NotFoundHttpException('Évaluation non trouvée.');
|
||||
}
|
||||
|
||||
if ((string) $evaluation->teacherId !== $user->userId()) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas le propriétaire de cette évaluation.');
|
||||
}
|
||||
|
||||
$class = $this->classRepository->findById($evaluation->classId);
|
||||
$subject = $this->subjectRepository->findById($evaluation->subjectId);
|
||||
|
||||
return EvaluationResource::fromDomain(
|
||||
$evaluation,
|
||||
$class !== null ? (string) $class->name : null,
|
||||
$subject !== null ? (string) $subject->name : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Provider;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
|
||||
final readonly class EvaluationWithNames
|
||||
{
|
||||
public function __construct(
|
||||
public Evaluation $evaluation,
|
||||
public ?string $className,
|
||||
public ?string $subjectName,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Infrastructure\Api\Processor\CreateEvaluationProcessor;
|
||||
use App\Scolarite\Infrastructure\Api\Processor\DeleteEvaluationProcessor;
|
||||
use App\Scolarite\Infrastructure\Api\Processor\UpdateEvaluationProcessor;
|
||||
use App\Scolarite\Infrastructure\Api\Provider\EvaluationCollectionProvider;
|
||||
use App\Scolarite\Infrastructure\Api\Provider\EvaluationItemProvider;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Evaluation',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/evaluations',
|
||||
provider: EvaluationCollectionProvider::class,
|
||||
name: 'get_evaluation_list',
|
||||
),
|
||||
new Get(
|
||||
uriTemplate: '/evaluations/{id}',
|
||||
provider: EvaluationItemProvider::class,
|
||||
name: 'get_evaluation',
|
||||
),
|
||||
new Post(
|
||||
uriTemplate: '/evaluations',
|
||||
processor: CreateEvaluationProcessor::class,
|
||||
validationContext: ['groups' => ['Default', 'create']],
|
||||
name: 'create_evaluation',
|
||||
),
|
||||
new Patch(
|
||||
uriTemplate: '/evaluations/{id}',
|
||||
provider: EvaluationItemProvider::class,
|
||||
processor: UpdateEvaluationProcessor::class,
|
||||
validationContext: ['groups' => ['Default', 'update']],
|
||||
name: 'update_evaluation',
|
||||
),
|
||||
new Delete(
|
||||
uriTemplate: '/evaluations/{id}',
|
||||
provider: EvaluationItemProvider::class,
|
||||
processor: DeleteEvaluationProcessor::class,
|
||||
name: 'delete_evaluation',
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class EvaluationResource
|
||||
{
|
||||
#[ApiProperty(identifier: true)]
|
||||
public ?string $id = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'La classe est requise.', groups: ['create'])]
|
||||
#[Assert\Uuid(message: 'L\'identifiant de la classe doit être un UUID valide.', groups: ['create'])]
|
||||
public ?string $classId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'La matière est requise.', groups: ['create'])]
|
||||
#[Assert\Uuid(message: 'L\'identifiant de la matière doit être un UUID valide.', groups: ['create'])]
|
||||
public ?string $subjectId = null;
|
||||
|
||||
public ?string $teacherId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Le titre est requis.', groups: ['create', 'update'])]
|
||||
#[Assert\Length(max: 255, maxMessage: 'Le titre ne peut pas dépasser 255 caractères.')]
|
||||
public ?string $title = null;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'La date d\'évaluation est requise.', groups: ['create', 'update'])]
|
||||
public ?string $evaluationDate = null;
|
||||
|
||||
#[Assert\Range(min: 1, max: 100, notInRangeMessage: 'Le barème doit être compris entre 1 et 100.')]
|
||||
public ?int $gradeScale = null;
|
||||
|
||||
#[Assert\Range(min: 0.1, max: 10, notInRangeMessage: 'Le coefficient doit être compris entre 0.1 et 10.')]
|
||||
public ?float $coefficient = null;
|
||||
|
||||
public ?string $status = null;
|
||||
|
||||
public ?string $className = null;
|
||||
|
||||
public ?string $subjectName = null;
|
||||
|
||||
public ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
public ?DateTimeImmutable $updatedAt = null;
|
||||
|
||||
public static function fromDomain(
|
||||
Evaluation $evaluation,
|
||||
?string $className = null,
|
||||
?string $subjectName = null,
|
||||
): self {
|
||||
$resource = new self();
|
||||
$resource->id = (string) $evaluation->id;
|
||||
$resource->classId = (string) $evaluation->classId;
|
||||
$resource->subjectId = (string) $evaluation->subjectId;
|
||||
$resource->teacherId = (string) $evaluation->teacherId;
|
||||
$resource->title = $evaluation->title;
|
||||
$resource->description = $evaluation->description;
|
||||
$resource->evaluationDate = $evaluation->evaluationDate->format('Y-m-d');
|
||||
$resource->gradeScale = $evaluation->gradeScale->maxValue;
|
||||
$resource->coefficient = $evaluation->coefficient->value;
|
||||
$resource->status = $evaluation->status->value;
|
||||
$resource->className = $className;
|
||||
$resource->subjectName = $subjectName;
|
||||
$resource->createdAt = $evaluation->createdAt;
|
||||
$resource->updatedAt = $evaluation->updatedAt;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
|
||||
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineEvaluationRepository implements EvaluationRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(Evaluation $evaluation): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO evaluations (id, tenant_id, class_id, subject_id, teacher_id, title, description, evaluation_date, grade_scale, coefficient, status, created_at, updated_at)
|
||||
VALUES (:id, :tenant_id, :class_id, :subject_id, :teacher_id, :title, :description, :evaluation_date, :grade_scale, :coefficient, :status, :created_at, :updated_at)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
description = EXCLUDED.description,
|
||||
evaluation_date = EXCLUDED.evaluation_date,
|
||||
grade_scale = EXCLUDED.grade_scale,
|
||||
coefficient = EXCLUDED.coefficient,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = EXCLUDED.updated_at',
|
||||
[
|
||||
'id' => (string) $evaluation->id,
|
||||
'tenant_id' => (string) $evaluation->tenantId,
|
||||
'class_id' => (string) $evaluation->classId,
|
||||
'subject_id' => (string) $evaluation->subjectId,
|
||||
'teacher_id' => (string) $evaluation->teacherId,
|
||||
'title' => $evaluation->title,
|
||||
'description' => $evaluation->description,
|
||||
'evaluation_date' => $evaluation->evaluationDate->format('Y-m-d'),
|
||||
'grade_scale' => $evaluation->gradeScale->maxValue,
|
||||
'coefficient' => $evaluation->coefficient->value,
|
||||
'status' => $evaluation->status->value,
|
||||
'created_at' => $evaluation->createdAt->format(DateTimeImmutable::ATOM),
|
||||
'updated_at' => $evaluation->updatedAt->format(DateTimeImmutable::ATOM),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(EvaluationId $id, TenantId $tenantId): Evaluation
|
||||
{
|
||||
$evaluation = $this->findById($id, $tenantId);
|
||||
|
||||
if ($evaluation === null) {
|
||||
throw EvaluationNotFoundException::withId($id);
|
||||
}
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM evaluations WHERE id = :id AND tenant_id = :tenant_id',
|
||||
['id' => (string) $id, 'tenant_id' => (string) $tenantId],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT e.*, c.name AS class_name, s.name AS subject_name
|
||||
FROM evaluations e
|
||||
LEFT JOIN school_classes c ON c.id = e.class_id
|
||||
LEFT JOIN subjects s ON s.id = e.subject_id
|
||||
WHERE e.teacher_id = :teacher_id
|
||||
AND e.tenant_id = :tenant_id
|
||||
AND e.status != :deleted
|
||||
ORDER BY e.evaluation_date DESC',
|
||||
[
|
||||
'teacher_id' => (string) $teacherId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'deleted' => EvaluationStatus::DELETED->value,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map($this->hydrate(...), $rows);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT e.*, c.name AS class_name, s.name AS subject_name
|
||||
FROM evaluations e
|
||||
LEFT JOIN school_classes c ON c.id = e.class_id
|
||||
LEFT JOIN subjects s ON s.id = e.subject_id
|
||||
WHERE e.teacher_id = :teacher_id
|
||||
AND e.class_id = :class_id
|
||||
AND e.tenant_id = :tenant_id
|
||||
AND e.status != :deleted
|
||||
ORDER BY e.evaluation_date DESC',
|
||||
[
|
||||
'teacher_id' => (string) $teacherId,
|
||||
'class_id' => (string) $classId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'deleted' => EvaluationStatus::DELETED->value,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map($this->hydrate(...), $rows);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByClass(ClassId $classId, TenantId $tenantId): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT e.*, c.name AS class_name, s.name AS subject_name
|
||||
FROM evaluations e
|
||||
LEFT JOIN school_classes c ON c.id = e.class_id
|
||||
LEFT JOIN subjects s ON s.id = e.subject_id
|
||||
WHERE e.class_id = :class_id
|
||||
AND e.tenant_id = :tenant_id
|
||||
AND e.status != :deleted
|
||||
ORDER BY e.evaluation_date DESC',
|
||||
[
|
||||
'class_id' => (string) $classId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'deleted' => EvaluationStatus::DELETED->value,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map($this->hydrate(...), $rows);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row */
|
||||
private function hydrate(array $row): Evaluation
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $row['id'];
|
||||
/** @var string $tenantId */
|
||||
$tenantId = $row['tenant_id'];
|
||||
/** @var string $classId */
|
||||
$classId = $row['class_id'];
|
||||
/** @var string $subjectId */
|
||||
$subjectId = $row['subject_id'];
|
||||
/** @var string $teacherId */
|
||||
$teacherId = $row['teacher_id'];
|
||||
/** @var string $title */
|
||||
$title = $row['title'];
|
||||
/** @var string|null $description */
|
||||
$description = $row['description'];
|
||||
/** @var string $evaluationDate */
|
||||
$evaluationDate = $row['evaluation_date'];
|
||||
/** @var string|int $gradeScaleRaw */
|
||||
$gradeScaleRaw = $row['grade_scale'];
|
||||
$gradeScale = (int) $gradeScaleRaw;
|
||||
/** @var string|float $coefficientRaw */
|
||||
$coefficientRaw = $row['coefficient'];
|
||||
$coefficient = (float) $coefficientRaw;
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
/** @var string $createdAt */
|
||||
$createdAt = $row['created_at'];
|
||||
/** @var string $updatedAt */
|
||||
$updatedAt = $row['updated_at'];
|
||||
|
||||
return Evaluation::reconstitute(
|
||||
id: EvaluationId::fromString($id),
|
||||
tenantId: TenantId::fromString($tenantId),
|
||||
classId: ClassId::fromString($classId),
|
||||
subjectId: SubjectId::fromString($subjectId),
|
||||
teacherId: UserId::fromString($teacherId),
|
||||
title: $title,
|
||||
description: $description,
|
||||
evaluationDate: new DateTimeImmutable($evaluationDate),
|
||||
gradeScale: new GradeScale($gradeScale),
|
||||
coefficient: new Coefficient($coefficient),
|
||||
status: EvaluationStatus::from($status),
|
||||
createdAt: new DateTimeImmutable($createdAt),
|
||||
updatedAt: new DateTimeImmutable($updatedAt),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Persistence\InMemory;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
|
||||
use Override;
|
||||
|
||||
final class InMemoryEvaluationRepository implements EvaluationRepository
|
||||
{
|
||||
/** @var array<string, Evaluation> */
|
||||
private array $byId = [];
|
||||
|
||||
#[Override]
|
||||
public function save(Evaluation $evaluation): void
|
||||
{
|
||||
$this->byId[(string) $evaluation->id] = $evaluation;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(EvaluationId $id, TenantId $tenantId): Evaluation
|
||||
{
|
||||
$evaluation = $this->findById($id, $tenantId);
|
||||
|
||||
if ($evaluation === null) {
|
||||
throw EvaluationNotFoundException::withId($id);
|
||||
}
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation
|
||||
{
|
||||
$evaluation = $this->byId[(string) $id] ?? null;
|
||||
|
||||
if ($evaluation === null || !$evaluation->tenantId->equals($tenantId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->byId,
|
||||
static fn (Evaluation $e): bool => $e->teacherId->equals($teacherId)
|
||||
&& $e->tenantId->equals($tenantId)
|
||||
&& $e->status !== EvaluationStatus::DELETED,
|
||||
));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByClass(ClassId $classId, TenantId $tenantId): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->byId,
|
||||
static fn (Evaluation $e): bool => $e->classId->equals($classId)
|
||||
&& $e->tenantId->equals($tenantId)
|
||||
&& $e->status !== EvaluationStatus::DELETED,
|
||||
));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->byId,
|
||||
static fn (Evaluation $e): bool => $e->teacherId->equals($teacherId)
|
||||
&& $e->classId->equals($classId)
|
||||
&& $e->tenantId->equals($tenantId)
|
||||
&& $e->status !== EvaluationStatus::DELETED,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Service;
|
||||
|
||||
use App\Scolarite\Application\Port\EvaluationGradesChecker;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Override;
|
||||
|
||||
final readonly class NoGradesEvaluationGradesChecker implements EvaluationGradesChecker
|
||||
{
|
||||
#[Override]
|
||||
public function hasGrades(EvaluationId $evaluationId, TenantId $tenantId): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user