feat: Permettre à l'élève de consulter ses devoirs
L'élève n'avait aucun moyen de voir les devoirs assignés à sa classe. Cette fonctionnalité ajoute la consultation complète : liste triée par échéance, détail avec pièces jointes, filtrage par matière, et marquage personnel « fait » en localStorage. Le dashboard élève affiche désormais les devoirs à venir avec ouverture du détail en modale, et un lien vers la page complète. L'accès API est sécurisé par vérification de la classe de l'élève (pas d'IDOR) et validation du chemin des pièces jointes (pas de path traversal).
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
final readonly class AttachmentDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $filename,
|
||||
public int $fileSize,
|
||||
public string $mimeType,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
use function usort;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetStudentHomeworkHandler
|
||||
{
|
||||
public function __construct(
|
||||
private StudentClassReader $studentClassReader,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkAttachmentRepository $attachmentRepository,
|
||||
private ScheduleDisplayReader $displayReader,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<StudentHomeworkDto> */
|
||||
public function __invoke(GetStudentHomeworkQuery $query): array
|
||||
{
|
||||
$tenantId = TenantId::fromString($query->tenantId);
|
||||
|
||||
$classId = $this->studentClassReader->currentClassId($query->studentId, $tenantId);
|
||||
|
||||
if ($classId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$homeworks = $this->homeworkRepository->findByClass(ClassId::fromString($classId), $tenantId);
|
||||
|
||||
if ($query->subjectId !== null) {
|
||||
$filterSubjectId = $query->subjectId;
|
||||
$homeworks = array_values(array_filter(
|
||||
$homeworks,
|
||||
static fn (Homework $h): bool => (string) $h->subjectId === $filterSubjectId,
|
||||
));
|
||||
}
|
||||
|
||||
usort($homeworks, static fn (Homework $a, Homework $b): int => $a->dueDate <=> $b->dueDate);
|
||||
|
||||
return $this->enrichHomeworks($homeworks, $query->tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Homework> $homeworks
|
||||
*
|
||||
* @return array<StudentHomeworkDto>
|
||||
*/
|
||||
private function enrichHomeworks(array $homeworks, string $tenantId): array
|
||||
{
|
||||
if ($homeworks === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$subjectIds = array_values(array_unique(
|
||||
array_map(static fn (Homework $h): string => (string) $h->subjectId, $homeworks),
|
||||
));
|
||||
$teacherIds = array_values(array_unique(
|
||||
array_map(static fn (Homework $h): string => (string) $h->teacherId, $homeworks),
|
||||
));
|
||||
|
||||
$subjects = $this->displayReader->subjectDisplay($tenantId, ...$subjectIds);
|
||||
$teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds);
|
||||
|
||||
$homeworkIds = array_map(static fn (Homework $h): HomeworkId => $h->id, $homeworks);
|
||||
$attachmentMap = $this->attachmentRepository->hasAttachments(...$homeworkIds);
|
||||
|
||||
return array_map(
|
||||
static fn (Homework $h): StudentHomeworkDto => StudentHomeworkDto::fromDomain(
|
||||
$h,
|
||||
$subjects[(string) $h->subjectId]['name'] ?? '',
|
||||
$subjects[(string) $h->subjectId]['color'] ?? null,
|
||||
$teacherNames[(string) $h->teacherId] ?? '',
|
||||
$attachmentMap[(string) $h->id] ?? false,
|
||||
),
|
||||
$homeworks,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
final readonly class GetStudentHomeworkQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $studentId,
|
||||
public string $tenantId,
|
||||
public ?string $subjectId = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachment;
|
||||
|
||||
use function array_map;
|
||||
|
||||
final readonly class StudentHomeworkDetailDto
|
||||
{
|
||||
/**
|
||||
* @param array<AttachmentDto> $attachments
|
||||
*/
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $subjectId,
|
||||
public string $subjectName,
|
||||
public ?string $subjectColor,
|
||||
public string $teacherId,
|
||||
public string $teacherName,
|
||||
public string $title,
|
||||
public ?string $description,
|
||||
public string $dueDate,
|
||||
public string $createdAt,
|
||||
public array $attachments,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<HomeworkAttachment> $attachments
|
||||
*/
|
||||
public static function fromDomain(
|
||||
Homework $homework,
|
||||
string $subjectName,
|
||||
?string $subjectColor,
|
||||
string $teacherName,
|
||||
array $attachments,
|
||||
): self {
|
||||
return new self(
|
||||
id: (string) $homework->id,
|
||||
subjectId: (string) $homework->subjectId,
|
||||
subjectName: $subjectName,
|
||||
subjectColor: $subjectColor,
|
||||
teacherId: (string) $homework->teacherId,
|
||||
teacherName: $teacherName,
|
||||
title: $homework->title,
|
||||
description: $homework->description,
|
||||
dueDate: $homework->dueDate->format('Y-m-d'),
|
||||
createdAt: $homework->createdAt->format('Y-m-d\TH:i:sP'),
|
||||
attachments: array_map(
|
||||
static fn (HomeworkAttachment $a): AttachmentDto => new AttachmentDto(
|
||||
id: (string) $a->id,
|
||||
filename: $a->filename,
|
||||
fileSize: $a->fileSize,
|
||||
mimeType: $a->mimeType,
|
||||
),
|
||||
$attachments,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
|
||||
final readonly class StudentHomeworkDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $subjectId,
|
||||
public string $subjectName,
|
||||
public ?string $subjectColor,
|
||||
public string $teacherId,
|
||||
public string $teacherName,
|
||||
public string $title,
|
||||
public ?string $description,
|
||||
public string $dueDate,
|
||||
public string $createdAt,
|
||||
public bool $hasAttachments,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromDomain(
|
||||
Homework $homework,
|
||||
string $subjectName,
|
||||
?string $subjectColor,
|
||||
string $teacherName,
|
||||
bool $hasAttachments,
|
||||
): self {
|
||||
return new self(
|
||||
id: (string) $homework->id,
|
||||
subjectId: (string) $homework->subjectId,
|
||||
subjectName: $subjectName,
|
||||
subjectColor: $subjectColor,
|
||||
teacherId: (string) $homework->teacherId,
|
||||
teacherName: $teacherName,
|
||||
title: $homework->title,
|
||||
description: $homework->description,
|
||||
dueDate: $homework->dueDate->format('Y-m-d'),
|
||||
createdAt: $homework->createdAt->format('Y-m-d\TH:i:sP'),
|
||||
hasAttachments: $hasAttachments,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,10 @@ interface HomeworkAttachmentRepository
|
||||
/** @return array<HomeworkAttachment> */
|
||||
public function findByHomeworkId(HomeworkId $homeworkId): array;
|
||||
|
||||
/**
|
||||
* @return array<string, bool> Map homeworkId => hasAttachments
|
||||
*/
|
||||
public function hasAttachments(HomeworkId ...$homeworkIds): array;
|
||||
|
||||
public function save(HomeworkId $homeworkId, HomeworkAttachment $attachment): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Controller;
|
||||
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkHandler;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkQuery;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDetailDto;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDto;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Scolarite\Infrastructure\Security\HomeworkStudentVoter;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_map;
|
||||
use function is_string;
|
||||
use function realpath;
|
||||
use function str_starts_with;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted(HomeworkStudentVoter::VIEW)]
|
||||
final readonly class StudentHomeworkController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private GetStudentHomeworkHandler $handler,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkAttachmentRepository $attachmentRepository,
|
||||
private ScheduleDisplayReader $displayReader,
|
||||
private StudentClassReader $studentClassReader,
|
||||
#[Autowire('%kernel.project_dir%/var/uploads')]
|
||||
private string $uploadsDir,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/api/me/homework', name: 'api_student_homework_list', methods: ['GET'])]
|
||||
public function list(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
|
||||
$subjectId = $request->query->get('subjectId');
|
||||
|
||||
$result = ($this->handler)(new GetStudentHomeworkQuery(
|
||||
studentId: $user->userId(),
|
||||
tenantId: $user->tenantId(),
|
||||
subjectId: is_string($subjectId) && $subjectId !== '' ? $subjectId : null,
|
||||
));
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => array_map($this->serializeListItem(...), $result),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/me/homework/{id}', name: 'api_student_homework_detail', methods: ['GET'])]
|
||||
public function detail(string $id): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
|
||||
$homework = $this->homeworkRepository->findById(HomeworkId::fromString($id), $tenantId);
|
||||
|
||||
if ($homework === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
$this->assertHomeworkBelongsToStudentClass($user, $homework);
|
||||
|
||||
$attachments = $this->attachmentRepository->findByHomeworkId($homework->id);
|
||||
|
||||
$subjects = $this->displayReader->subjectDisplay($user->tenantId(), (string) $homework->subjectId);
|
||||
$teacherNames = $this->displayReader->teacherNames($user->tenantId(), (string) $homework->teacherId);
|
||||
|
||||
$detail = StudentHomeworkDetailDto::fromDomain(
|
||||
$homework,
|
||||
$subjects[(string) $homework->subjectId]['name'] ?? '',
|
||||
$subjects[(string) $homework->subjectId]['color'] ?? null,
|
||||
$teacherNames[(string) $homework->teacherId] ?? '',
|
||||
$attachments,
|
||||
);
|
||||
|
||||
return new JsonResponse(['data' => $this->serializeDetail($detail)]);
|
||||
}
|
||||
|
||||
#[Route('/api/me/homework/{homeworkId}/attachments/{attachmentId}', name: 'api_student_homework_attachment', methods: ['GET'])]
|
||||
public function downloadAttachment(string $homeworkId, string $attachmentId): BinaryFileResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
|
||||
$homework = $this->homeworkRepository->findById(HomeworkId::fromString($homeworkId), $tenantId);
|
||||
|
||||
if ($homework === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
$this->assertHomeworkBelongsToStudentClass($user, $homework);
|
||||
|
||||
$attachments = $this->attachmentRepository->findByHomeworkId($homework->id);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if ((string) $attachment->id === $attachmentId) {
|
||||
$realPath = realpath($attachment->filePath);
|
||||
$realUploadsDir = realpath($this->uploadsDir);
|
||||
|
||||
if ($realPath === false || $realUploadsDir === false || !str_starts_with($realPath, $realUploadsDir)) {
|
||||
throw new NotFoundHttpException('Pièce jointe non trouvée.');
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($realPath);
|
||||
$response->setContentDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_INLINE,
|
||||
$attachment->filename,
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException('Pièce jointe non trouvée.');
|
||||
}
|
||||
|
||||
private function assertHomeworkBelongsToStudentClass(SecurityUser $user, Homework $homework): void
|
||||
{
|
||||
$classId = $this->studentClassReader->currentClassId(
|
||||
$user->userId(),
|
||||
TenantId::fromString($user->tenantId()),
|
||||
);
|
||||
|
||||
if ($classId === null || (string) $homework->classId !== $classId) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
}
|
||||
|
||||
private function getSecurityUser(): SecurityUser
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new AccessDeniedHttpException('Authentification requise.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function serializeListItem(StudentHomeworkDto $dto): array
|
||||
{
|
||||
return [
|
||||
'id' => $dto->id,
|
||||
'subjectId' => $dto->subjectId,
|
||||
'subjectName' => $dto->subjectName,
|
||||
'subjectColor' => $dto->subjectColor,
|
||||
'teacherId' => $dto->teacherId,
|
||||
'teacherName' => $dto->teacherName,
|
||||
'title' => $dto->title,
|
||||
'description' => $dto->description,
|
||||
'dueDate' => $dto->dueDate,
|
||||
'createdAt' => $dto->createdAt,
|
||||
'hasAttachments' => $dto->hasAttachments,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function serializeDetail(StudentHomeworkDetailDto $dto): array
|
||||
{
|
||||
return [
|
||||
'id' => $dto->id,
|
||||
'subjectId' => $dto->subjectId,
|
||||
'subjectName' => $dto->subjectName,
|
||||
'subjectColor' => $dto->subjectColor,
|
||||
'teacherId' => $dto->teacherId,
|
||||
'teacherName' => $dto->teacherName,
|
||||
'title' => $dto->title,
|
||||
'description' => $dto->description,
|
||||
'dueDate' => $dto->dueDate,
|
||||
'createdAt' => $dto->createdAt,
|
||||
'attachments' => array_map(
|
||||
static fn ($a): array => [
|
||||
'id' => $a->id,
|
||||
'filename' => $a->filename,
|
||||
'fileSize' => $a->fileSize,
|
||||
'mimeType' => $a->mimeType,
|
||||
],
|
||||
$dto->attachments,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,11 @@ use App\Scolarite\Domain\Model\Homework\HomeworkAttachmentId;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function array_map;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
@@ -33,6 +35,33 @@ final readonly class DoctrineHomeworkAttachmentRepository implements HomeworkAtt
|
||||
return array_map($this->hydrate(...), $rows);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function hasAttachments(HomeworkId ...$homeworkIds): array
|
||||
{
|
||||
if ($homeworkIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = array_map(static fn (HomeworkId $id): string => (string) $id, $homeworkIds);
|
||||
|
||||
/** @var array<array{homework_id: string}> $rows */
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT DISTINCT homework_id FROM homework_attachments WHERE homework_id IN (:ids)',
|
||||
['ids' => $ids],
|
||||
['ids' => ArrayParameterType::STRING],
|
||||
);
|
||||
|
||||
$result = array_fill_keys($ids, false);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
/** @var string $hwId */
|
||||
$hwId = $row['homework_id'];
|
||||
$result[$hwId] = true;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(HomeworkId $homeworkId, HomeworkAttachment $attachment): void
|
||||
{
|
||||
|
||||
@@ -7,6 +7,10 @@ namespace App\Scolarite\Infrastructure\Persistence\InMemory;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachment;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function array_map;
|
||||
|
||||
use Override;
|
||||
|
||||
final class InMemoryHomeworkAttachmentRepository implements HomeworkAttachmentRepository
|
||||
@@ -20,6 +24,19 @@ final class InMemoryHomeworkAttachmentRepository implements HomeworkAttachmentRe
|
||||
return $this->byHomeworkId[(string) $homeworkId] ?? [];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function hasAttachments(HomeworkId ...$homeworkIds): array
|
||||
{
|
||||
$ids = array_map(static fn (HomeworkId $id): string => (string) $id, $homeworkIds);
|
||||
$result = array_fill_keys($ids, false);
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$result[$id] = isset($this->byHomeworkId[$id]) && $this->byHomeworkId[$id] !== [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(HomeworkId $homeworkId, HomeworkAttachment $attachment): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Security;
|
||||
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
|
||||
use function in_array;
|
||||
|
||||
use Override;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* Voter pour la consultation des devoirs par l'élève.
|
||||
*
|
||||
* @extends Voter<string, null>
|
||||
*/
|
||||
final class HomeworkStudentVoter extends Voter
|
||||
{
|
||||
public const string VIEW = 'HOMEWORK_STUDENT_VIEW';
|
||||
|
||||
#[Override]
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return $attribute === self::VIEW;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(Role::ELEVE->value, $user->getRoles(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
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\ScheduleDisplayReader;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkHandler;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkQuery;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachment;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachmentId;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkAttachmentRepository;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class GetStudentHomeworkHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440050';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string SUBJECT_MATH_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string SUBJECT_FRENCH_ID = '550e8400-e29b-41d4-a716-446655440031';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemoryHomeworkRepository $homeworkRepository;
|
||||
private InMemoryHomeworkAttachmentRepository $attachmentRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->homeworkRepository = new InMemoryHomeworkRepository();
|
||||
$this->attachmentRepository = new InMemoryHomeworkAttachmentRepository();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsEmptyWhenStudentHasNoClass(): void
|
||||
{
|
||||
$handler = $this->createHandler(classId: null);
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsHomeworkForStudentClass(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Exercices chapitre 5', dueDate: '2026-04-15');
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Exercices chapitre 5', $result[0]->title);
|
||||
self::assertSame('2026-04-15', $result[0]->dueDate);
|
||||
self::assertSame('Mathématiques', $result[0]->subjectName);
|
||||
self::assertSame('#3b82f6', $result[0]->subjectColor);
|
||||
self::assertSame('Jean Dupont', $result[0]->teacherName);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSortsByDueDateAscending(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Devoir lointain', dueDate: '2026-05-20');
|
||||
$this->givenHomework(title: 'Devoir proche', dueDate: '2026-04-10');
|
||||
$this->givenHomework(title: 'Devoir milieu', dueDate: '2026-04-25');
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(3, $result);
|
||||
self::assertSame('Devoir proche', $result[0]->title);
|
||||
self::assertSame('Devoir milieu', $result[1]->title);
|
||||
self::assertSame('Devoir lointain', $result[2]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itFiltersDeletedHomework(): void
|
||||
{
|
||||
$homework = $this->givenHomework(title: 'Devoir supprimé', dueDate: '2026-04-15');
|
||||
$homework->supprimer(new DateTimeImmutable('2026-03-20'));
|
||||
$this->homeworkRepository->save($homework);
|
||||
|
||||
$this->givenHomework(title: 'Devoir actif', dueDate: '2026-04-20');
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Devoir actif', $result[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itFiltersBySubjectWhenProvided(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Maths', dueDate: '2026-04-15', subjectId: self::SUBJECT_MATH_ID);
|
||||
$this->givenHomework(title: 'Français', dueDate: '2026-04-16', subjectId: self::SUBJECT_FRENCH_ID);
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
subjectId: self::SUBJECT_MATH_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Maths', $result[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itExcludesHomeworkFromOtherClasses(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Mon devoir', dueDate: '2026-04-15', classId: self::CLASS_ID);
|
||||
|
||||
$otherClassId = '550e8400-e29b-41d4-a716-446655440099';
|
||||
$this->givenHomework(title: 'Autre classe', dueDate: '2026-04-16', classId: $otherClassId);
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Mon devoir', $result[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itIndicatesWhenHomeworkHasAttachments(): void
|
||||
{
|
||||
$homework = $this->givenHomework(title: 'Avec pièce jointe', dueDate: '2026-04-15');
|
||||
|
||||
$attachment = new HomeworkAttachment(
|
||||
id: HomeworkAttachmentId::generate(),
|
||||
filename: 'exercice.pdf',
|
||||
filePath: '/uploads/exercice.pdf',
|
||||
fileSize: 1024,
|
||||
mimeType: 'application/pdf',
|
||||
uploadedAt: new DateTimeImmutable('2026-03-12'),
|
||||
);
|
||||
$this->attachmentRepository->save($homework->id, $attachment);
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertTrue($result[0]->hasAttachments);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itIndicatesWhenHomeworkHasNoAttachments(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Sans pièce jointe', dueDate: '2026-04-15');
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertFalse($result[0]->hasAttachments);
|
||||
}
|
||||
|
||||
private function createHandler(?string $classId = self::CLASS_ID): GetStudentHomeworkHandler
|
||||
{
|
||||
$studentClassReader = new class($classId) implements StudentClassReader {
|
||||
public function __construct(private readonly ?string $classId)
|
||||
{
|
||||
}
|
||||
|
||||
public function currentClassId(string $studentId, TenantId $tenantId): ?string
|
||||
{
|
||||
return $this->classId;
|
||||
}
|
||||
};
|
||||
|
||||
$displayReader = new class implements ScheduleDisplayReader {
|
||||
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($subjectIds as $id) {
|
||||
$map[$id] = ['name' => 'Mathématiques', 'color' => '#3b82f6'];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($teacherIds as $id) {
|
||||
$map[$id] = 'Jean Dupont';
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
};
|
||||
|
||||
return new GetStudentHomeworkHandler(
|
||||
$studentClassReader,
|
||||
$this->homeworkRepository,
|
||||
$this->attachmentRepository,
|
||||
$displayReader,
|
||||
);
|
||||
}
|
||||
|
||||
private function givenHomework(
|
||||
string $title,
|
||||
string $dueDate,
|
||||
string $classId = self::CLASS_ID,
|
||||
string $subjectId = self::SUBJECT_MATH_ID,
|
||||
): Homework {
|
||||
$homework = Homework::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString($classId),
|
||||
subjectId: SubjectId::fromString($subjectId),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
title: $title,
|
||||
description: null,
|
||||
dueDate: new DateTimeImmutable($dueDate),
|
||||
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
||||
);
|
||||
|
||||
$this->homeworkRepository->save($homework);
|
||||
|
||||
return $homework;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user