feat: Permettre à l'élève de rendre un devoir avec réponse texte et pièces jointes
L'élève peut désormais répondre à un devoir via un éditeur WYSIWYG, joindre des fichiers (PDF, JPEG, PNG, DOCX), sauvegarder un brouillon et soumettre définitivement son rendu. Le système détecte automatiquement les soumissions en retard par rapport à la date d'échéance. Côté enseignant, une page dédiée affiche la liste complète des élèves avec leur statut (soumis, en retard, brouillon, non rendu), le détail de chaque rendu avec ses pièces jointes téléchargeables, et les statistiques de rendus par classe.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\SaveDraftSubmission;
|
||||
|
||||
final readonly class SaveDraftSubmissionCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $homeworkId,
|
||||
public string $studentId,
|
||||
public ?string $responseHtml,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\SaveDraftSubmission;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Application\Port\HtmlSanitizer;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Domain\Exception\EleveNonAffecteAuDevoirException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class SaveDraftSubmissionHandler
|
||||
{
|
||||
public function __construct(
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkSubmissionRepository $submissionRepository,
|
||||
private StudentClassReader $studentClassReader,
|
||||
private HtmlSanitizer $htmlSanitizer,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(SaveDraftSubmissionCommand $command): HomeworkSubmission
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$homeworkId = HomeworkId::fromString($command->homeworkId);
|
||||
$studentId = UserId::fromString($command->studentId);
|
||||
|
||||
$homework = $this->homeworkRepository->get($homeworkId, $tenantId);
|
||||
|
||||
$classId = $this->studentClassReader->currentClassId($command->studentId, $tenantId);
|
||||
|
||||
if ($classId === null || $classId !== (string) $homework->classId) {
|
||||
throw EleveNonAffecteAuDevoirException::pourEleve($studentId, $homeworkId);
|
||||
}
|
||||
|
||||
$sanitizedHtml = $command->responseHtml !== null
|
||||
? $this->htmlSanitizer->sanitize($command->responseHtml)
|
||||
: null;
|
||||
|
||||
$now = $this->clock->now();
|
||||
|
||||
$existing = $this->submissionRepository->findByHomeworkAndStudent($homeworkId, $studentId, $tenantId);
|
||||
|
||||
if ($existing !== null) {
|
||||
$existing->modifierBrouillon($sanitizedHtml, $now);
|
||||
$this->submissionRepository->save($existing);
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$submission = HomeworkSubmission::creerBrouillon(
|
||||
tenantId: $tenantId,
|
||||
homeworkId: $homeworkId,
|
||||
studentId: $studentId,
|
||||
responseHtml: $sanitizedHtml,
|
||||
now: $now,
|
||||
);
|
||||
|
||||
$this->submissionRepository->save($submission);
|
||||
|
||||
return $submission;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\SubmitHomework;
|
||||
|
||||
final readonly class SubmitHomeworkCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $homeworkId,
|
||||
public string $studentId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\SubmitHomework;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Domain\Exception\EleveNonAffecteAuDevoirException;
|
||||
use App\Scolarite\Domain\Exception\RenduNonTrouveException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class SubmitHomeworkHandler
|
||||
{
|
||||
public function __construct(
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkSubmissionRepository $submissionRepository,
|
||||
private StudentClassReader $studentClassReader,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(SubmitHomeworkCommand $command): HomeworkSubmission
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$homeworkId = HomeworkId::fromString($command->homeworkId);
|
||||
$studentId = UserId::fromString($command->studentId);
|
||||
|
||||
$homework = $this->homeworkRepository->get($homeworkId, $tenantId);
|
||||
|
||||
$classId = $this->studentClassReader->currentClassId($command->studentId, $tenantId);
|
||||
|
||||
if ($classId === null || $classId !== (string) $homework->classId) {
|
||||
throw EleveNonAffecteAuDevoirException::pourEleve($studentId, $homeworkId);
|
||||
}
|
||||
|
||||
$submission = $this->submissionRepository->findByHomeworkAndStudent($homeworkId, $studentId, $tenantId);
|
||||
|
||||
if ($submission === null) {
|
||||
throw RenduNonTrouveException::pourDevoirEtEleve($homeworkId, $studentId);
|
||||
}
|
||||
|
||||
$now = $this->clock->now();
|
||||
$submission->soumettre(dueDate: $homework->dueDate, now: $now);
|
||||
|
||||
$this->submissionRepository->save($submission);
|
||||
|
||||
return $submission;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\UploadSubmissionAttachment;
|
||||
|
||||
final readonly class UploadSubmissionAttachmentCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $submissionId,
|
||||
public string $filename,
|
||||
public string $mimeType,
|
||||
public int $fileSize,
|
||||
public string $tempFilePath,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\UploadSubmissionAttachment;
|
||||
|
||||
use App\Scolarite\Application\Port\FileStorage;
|
||||
use App\Scolarite\Domain\Exception\PieceJointeInvalideException;
|
||||
use App\Scolarite\Domain\Exception\RenduDejaSoumisException;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachment;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachmentId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function file_get_contents;
|
||||
use function sprintf;
|
||||
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class UploadSubmissionAttachmentHandler
|
||||
{
|
||||
public function __construct(
|
||||
private HomeworkSubmissionRepository $submissionRepository,
|
||||
private FileStorage $fileStorage,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(UploadSubmissionAttachmentCommand $command): SubmissionAttachment
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$submissionId = HomeworkSubmissionId::fromString($command->submissionId);
|
||||
|
||||
$submission = $this->submissionRepository->get($submissionId, $tenantId);
|
||||
|
||||
if (!$submission->status->estModifiable()) {
|
||||
throw RenduDejaSoumisException::pourRendu($submissionId);
|
||||
}
|
||||
|
||||
$attachmentId = SubmissionAttachmentId::generate();
|
||||
$storagePath = sprintf(
|
||||
'submissions/%s/%s/%s/%s',
|
||||
$command->tenantId,
|
||||
$command->submissionId,
|
||||
(string) $attachmentId,
|
||||
$command->filename,
|
||||
);
|
||||
|
||||
$content = file_get_contents($command->tempFilePath);
|
||||
|
||||
if ($content === false) {
|
||||
throw PieceJointeInvalideException::lectureFichierImpossible($command->filename);
|
||||
}
|
||||
|
||||
$this->fileStorage->upload($storagePath, $content, $command->mimeType);
|
||||
|
||||
return new SubmissionAttachment(
|
||||
id: $attachmentId,
|
||||
filename: $command->filename,
|
||||
filePath: $storagePath,
|
||||
fileSize: $command->fileSize,
|
||||
mimeType: $command->mimeType,
|
||||
uploadedAt: $this->clock->now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Port;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
/**
|
||||
* Port pour lire les élèves affectés à une classe.
|
||||
*/
|
||||
interface ClassStudentsReader
|
||||
{
|
||||
/**
|
||||
* @return array<array{id: string, name: string}> Liste des élèves avec ID et nom complet
|
||||
*/
|
||||
public function studentsInClass(string $classId, TenantId $tenantId): array;
|
||||
}
|
||||
@@ -5,12 +5,14 @@ declare(strict_types=1);
|
||||
namespace App\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
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\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_filter;
|
||||
@@ -29,6 +31,7 @@ final readonly class GetStudentHomeworkHandler
|
||||
private StudentClassReader $studentClassReader,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkAttachmentRepository $attachmentRepository,
|
||||
private HomeworkSubmissionRepository $submissionRepository,
|
||||
private ScheduleDisplayReader $displayReader,
|
||||
) {
|
||||
}
|
||||
@@ -56,7 +59,7 @@ final readonly class GetStudentHomeworkHandler
|
||||
|
||||
usort($homeworks, static fn (Homework $a, Homework $b): int => $a->dueDate <=> $b->dueDate);
|
||||
|
||||
return $this->enrichHomeworks($homeworks, $query->tenantId);
|
||||
return $this->enrichHomeworks($homeworks, $query->studentId, $query->tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +67,7 @@ final readonly class GetStudentHomeworkHandler
|
||||
*
|
||||
* @return array<StudentHomeworkDto>
|
||||
*/
|
||||
private function enrichHomeworks(array $homeworks, string $tenantId): array
|
||||
private function enrichHomeworks(array $homeworks, string $studentId, string $tenantId): array
|
||||
{
|
||||
if ($homeworks === []) {
|
||||
return [];
|
||||
@@ -83,6 +86,14 @@ final readonly class GetStudentHomeworkHandler
|
||||
$homeworkIds = array_map(static fn (Homework $h): HomeworkId => $h->id, $homeworks);
|
||||
$attachmentMap = $this->attachmentRepository->hasAttachments(...$homeworkIds);
|
||||
|
||||
$studentUserId = UserId::fromString($studentId);
|
||||
$tenantIdObj = TenantId::fromString($tenantId);
|
||||
$submissionStatusMap = $this->submissionRepository->findStatusesByStudent(
|
||||
$studentUserId,
|
||||
$tenantIdObj,
|
||||
...$homeworkIds,
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (Homework $h): StudentHomeworkDto => StudentHomeworkDto::fromDomain(
|
||||
$h,
|
||||
@@ -90,6 +101,7 @@ final readonly class GetStudentHomeworkHandler
|
||||
$subjects[(string) $h->subjectId]['color'] ?? null,
|
||||
$teacherNames[(string) $h->teacherId] ?? '',
|
||||
$attachmentMap[(string) $h->id] ?? false,
|
||||
$submissionStatusMap[(string) $h->id] ?? null,
|
||||
),
|
||||
$homeworks,
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ final readonly class StudentHomeworkDto
|
||||
public string $dueDate,
|
||||
public string $createdAt,
|
||||
public bool $hasAttachments,
|
||||
public ?string $submissionStatus = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ final readonly class StudentHomeworkDto
|
||||
?string $subjectColor,
|
||||
string $teacherName,
|
||||
bool $hasAttachments,
|
||||
?string $submissionStatus = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: (string) $homework->id,
|
||||
@@ -42,6 +44,7 @@ final readonly class StudentHomeworkDto
|
||||
dueDate: $homework->dueDate->format('Y-m-d'),
|
||||
createdAt: $homework->createdAt->format('Y-m-d\TH:i:sP'),
|
||||
hasAttachments: $hasAttachments,
|
||||
submissionStatus: $submissionStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
38
backend/src/Scolarite/Domain/Event/DevoirRendu.php
Normal file
38
backend/src/Scolarite/Domain/Event/DevoirRendu.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class DevoirRendu implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public HomeworkSubmissionId $submissionId,
|
||||
public HomeworkId $homeworkId,
|
||||
public UserId $studentId,
|
||||
public TenantId $tenantId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->submissionId->value;
|
||||
}
|
||||
}
|
||||
38
backend/src/Scolarite/Domain/Event/DevoirRenduEnRetard.php
Normal file
38
backend/src/Scolarite/Domain/Event/DevoirRenduEnRetard.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class DevoirRenduEnRetard implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public HomeworkSubmissionId $submissionId,
|
||||
public HomeworkId $homeworkId,
|
||||
public UserId $studentId,
|
||||
public TenantId $tenantId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->submissionId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EleveNonAffecteAuDevoirException extends DomainException
|
||||
{
|
||||
public static function pourEleve(UserId $studentId, HomeworkId $homeworkId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'élève "%s" n\'appartient pas à la classe du devoir "%s".',
|
||||
$studentId,
|
||||
$homeworkId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class RenduDejaSoumisException extends DomainException
|
||||
{
|
||||
public static function pourRendu(HomeworkSubmissionId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le rendu "%s" a déjà été soumis et ne peut plus être modifié.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class RenduNonTrouveException extends DomainException
|
||||
{
|
||||
public static function withId(HomeworkSubmissionId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le rendu avec l\'ID "%s" n\'a pas été trouvé.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
|
||||
public static function pourDevoirEtEleve(HomeworkId $homeworkId, UserId $studentId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Aucun rendu trouvé pour le devoir "%s" et l\'élève "%s".',
|
||||
$homeworkId,
|
||||
$studentId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\HomeworkSubmission;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Event\DevoirRendu;
|
||||
use App\Scolarite\Domain\Event\DevoirRenduEnRetard;
|
||||
use App\Scolarite\Domain\Exception\RenduDejaSoumisException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class HomeworkSubmission extends AggregateRoot
|
||||
{
|
||||
public private(set) DateTimeImmutable $updatedAt;
|
||||
|
||||
private function __construct(
|
||||
public private(set) HomeworkSubmissionId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) HomeworkId $homeworkId,
|
||||
public private(set) UserId $studentId,
|
||||
public private(set) ?string $responseHtml,
|
||||
public private(set) SubmissionStatus $status,
|
||||
public private(set) ?DateTimeImmutable $submittedAt,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
$this->updatedAt = $createdAt;
|
||||
}
|
||||
|
||||
public static function creerBrouillon(
|
||||
TenantId $tenantId,
|
||||
HomeworkId $homeworkId,
|
||||
UserId $studentId,
|
||||
?string $responseHtml,
|
||||
DateTimeImmutable $now,
|
||||
): self {
|
||||
return new self(
|
||||
id: HomeworkSubmissionId::generate(),
|
||||
tenantId: $tenantId,
|
||||
homeworkId: $homeworkId,
|
||||
studentId: $studentId,
|
||||
responseHtml: $responseHtml,
|
||||
status: SubmissionStatus::DRAFT,
|
||||
submittedAt: null,
|
||||
createdAt: $now,
|
||||
);
|
||||
}
|
||||
|
||||
public function modifierBrouillon(?string $responseHtml, DateTimeImmutable $now): void
|
||||
{
|
||||
if (!$this->status->estModifiable()) {
|
||||
throw RenduDejaSoumisException::pourRendu($this->id);
|
||||
}
|
||||
|
||||
$this->responseHtml = $responseHtml;
|
||||
$this->updatedAt = $now;
|
||||
}
|
||||
|
||||
public function soumettre(DateTimeImmutable $dueDate, DateTimeImmutable $now): void
|
||||
{
|
||||
if (!$this->status->estModifiable()) {
|
||||
throw RenduDejaSoumisException::pourRendu($this->id);
|
||||
}
|
||||
|
||||
$this->submittedAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
$estEnRetard = $now > $dueDate;
|
||||
|
||||
$this->status = $estEnRetard
|
||||
? SubmissionStatus::LATE
|
||||
: SubmissionStatus::SUBMITTED;
|
||||
|
||||
if ($estEnRetard) {
|
||||
$this->recordEvent(new DevoirRenduEnRetard(
|
||||
submissionId: $this->id,
|
||||
homeworkId: $this->homeworkId,
|
||||
studentId: $this->studentId,
|
||||
tenantId: $this->tenantId,
|
||||
occurredOn: $now,
|
||||
));
|
||||
} else {
|
||||
$this->recordEvent(new DevoirRendu(
|
||||
submissionId: $this->id,
|
||||
homeworkId: $this->homeworkId,
|
||||
studentId: $this->studentId,
|
||||
tenantId: $this->tenantId,
|
||||
occurredOn: $now,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
HomeworkSubmissionId $id,
|
||||
TenantId $tenantId,
|
||||
HomeworkId $homeworkId,
|
||||
UserId $studentId,
|
||||
?string $responseHtml,
|
||||
SubmissionStatus $status,
|
||||
?DateTimeImmutable $submittedAt,
|
||||
DateTimeImmutable $createdAt,
|
||||
DateTimeImmutable $updatedAt,
|
||||
): self {
|
||||
$submission = new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
homeworkId: $homeworkId,
|
||||
studentId: $studentId,
|
||||
responseHtml: $responseHtml,
|
||||
status: $status,
|
||||
submittedAt: $submittedAt,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$submission->updatedAt = $updatedAt;
|
||||
|
||||
return $submission;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\HomeworkSubmission;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class HomeworkSubmissionId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\HomeworkSubmission;
|
||||
|
||||
use App\Scolarite\Domain\Exception\PieceJointeInvalideException;
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function in_array;
|
||||
|
||||
final class SubmissionAttachment
|
||||
{
|
||||
private const int MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 Mo
|
||||
|
||||
private const array ALLOWED_MIME_TYPES = [
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
public private(set) SubmissionAttachmentId $id,
|
||||
public private(set) string $filename,
|
||||
public private(set) string $filePath,
|
||||
public private(set) int $fileSize {
|
||||
set(int $fileSize) {
|
||||
if ($fileSize > self::MAX_FILE_SIZE) {
|
||||
throw PieceJointeInvalideException::fichierTropGros($fileSize, self::MAX_FILE_SIZE);
|
||||
}
|
||||
$this->fileSize = $fileSize;
|
||||
}
|
||||
},
|
||||
public private(set) string $mimeType {
|
||||
set(string $mimeType) {
|
||||
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) {
|
||||
throw PieceJointeInvalideException::typeFichierNonAutorise($mimeType);
|
||||
}
|
||||
$this->mimeType = $mimeType;
|
||||
}
|
||||
},
|
||||
public private(set) DateTimeImmutable $uploadedAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\HomeworkSubmission;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class SubmissionAttachmentId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\HomeworkSubmission;
|
||||
|
||||
enum SubmissionStatus: string
|
||||
{
|
||||
case DRAFT = 'draft';
|
||||
case SUBMITTED = 'submitted';
|
||||
case LATE = 'late';
|
||||
|
||||
public function estModifiable(): bool
|
||||
{
|
||||
return $this === self::DRAFT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\RenduNonTrouveException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface HomeworkSubmissionRepository
|
||||
{
|
||||
public function save(HomeworkSubmission $submission): void;
|
||||
|
||||
/** @throws RenduNonTrouveException */
|
||||
public function get(HomeworkSubmissionId $id, TenantId $tenantId): HomeworkSubmission;
|
||||
|
||||
public function findById(HomeworkSubmissionId $id, TenantId $tenantId): ?HomeworkSubmission;
|
||||
|
||||
public function findByHomeworkAndStudent(
|
||||
HomeworkId $homeworkId,
|
||||
UserId $studentId,
|
||||
TenantId $tenantId,
|
||||
): ?HomeworkSubmission;
|
||||
|
||||
/** @return array<HomeworkSubmission> */
|
||||
public function findByHomework(HomeworkId $homeworkId, TenantId $tenantId): array;
|
||||
|
||||
/**
|
||||
* @return array<string, string|null> Map homeworkId => submission status value (or null)
|
||||
*/
|
||||
public function findStatusesByStudent(UserId $studentId, TenantId $tenantId, HomeworkId ...$homeworkIds): array;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachment;
|
||||
|
||||
interface SubmissionAttachmentRepository
|
||||
{
|
||||
/** @return array<SubmissionAttachment> */
|
||||
public function findBySubmissionId(HomeworkSubmissionId $submissionId): array;
|
||||
|
||||
public function save(HomeworkSubmissionId $submissionId, SubmissionAttachment $attachment): void;
|
||||
|
||||
public function delete(HomeworkSubmissionId $submissionId, SubmissionAttachment $attachment): void;
|
||||
}
|
||||
@@ -176,6 +176,7 @@ final readonly class StudentHomeworkController
|
||||
'dueDate' => $dto->dueDate,
|
||||
'createdAt' => $dto->createdAt,
|
||||
'hasAttachments' => $dto->hasAttachments,
|
||||
'submissionStatus' => $dto->submissionStatus,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Controller;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Command\SaveDraftSubmission\SaveDraftSubmissionCommand;
|
||||
use App\Scolarite\Application\Command\SaveDraftSubmission\SaveDraftSubmissionHandler;
|
||||
use App\Scolarite\Application\Command\SubmitHomework\SubmitHomeworkCommand;
|
||||
use App\Scolarite\Application\Command\SubmitHomework\SubmitHomeworkHandler;
|
||||
use App\Scolarite\Application\Command\UploadSubmissionAttachment\UploadSubmissionAttachmentCommand;
|
||||
use App\Scolarite\Application\Command\UploadSubmissionAttachment\UploadSubmissionAttachmentHandler;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Domain\Exception\EleveNonAffecteAuDevoirException;
|
||||
use App\Scolarite\Domain\Exception\RenduDejaSoumisException;
|
||||
use App\Scolarite\Domain\Exception\RenduNonTrouveException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachment;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Scolarite\Domain\Repository\SubmissionAttachmentRepository;
|
||||
use App\Scolarite\Infrastructure\Security\HomeworkStudentVoter;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function is_string;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted(HomeworkStudentVoter::VIEW)]
|
||||
final readonly class StudentSubmissionController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private SaveDraftSubmissionHandler $saveDraftHandler,
|
||||
private SubmitHomeworkHandler $submitHandler,
|
||||
private UploadSubmissionAttachmentHandler $uploadHandler,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkSubmissionRepository $submissionRepository,
|
||||
private SubmissionAttachmentRepository $attachmentRepository,
|
||||
private StudentClassReader $studentClassReader,
|
||||
private MessageBusInterface $eventBus,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/api/me/homework/{id}/submission', name: 'api_student_submission_save_draft', methods: ['POST'])]
|
||||
public function saveDraft(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$this->assertHomeworkBelongsToStudentClass($user, $id);
|
||||
|
||||
/** @var array{responseHtml?: string|null} $data */
|
||||
$data = $request->toArray();
|
||||
|
||||
try {
|
||||
$submission = ($this->saveDraftHandler)(new SaveDraftSubmissionCommand(
|
||||
tenantId: $user->tenantId(),
|
||||
homeworkId: $id,
|
||||
studentId: $user->userId(),
|
||||
responseHtml: isset($data['responseHtml']) && is_string($data['responseHtml']) ? $data['responseHtml'] : null,
|
||||
));
|
||||
} catch (EleveNonAffecteAuDevoirException $e) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.', $e);
|
||||
} catch (RenduDejaSoumisException $e) {
|
||||
throw new ConflictHttpException($e->getMessage(), $e);
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
['data' => $this->serializeSubmission($submission)],
|
||||
Response::HTTP_CREATED,
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/api/me/homework/{id}/submission/submit', name: 'api_student_submission_submit', methods: ['POST'])]
|
||||
public function submit(string $id): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$this->assertHomeworkBelongsToStudentClass($user, $id);
|
||||
|
||||
try {
|
||||
$submission = ($this->submitHandler)(new SubmitHomeworkCommand(
|
||||
tenantId: $user->tenantId(),
|
||||
homeworkId: $id,
|
||||
studentId: $user->userId(),
|
||||
));
|
||||
} catch (EleveNonAffecteAuDevoirException $e) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.', $e);
|
||||
} catch (RenduNonTrouveException $e) {
|
||||
throw new NotFoundHttpException('Aucun brouillon à soumettre.', $e);
|
||||
} catch (RenduDejaSoumisException $e) {
|
||||
throw new ConflictHttpException($e->getMessage(), $e);
|
||||
}
|
||||
|
||||
foreach ($submission->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return new JsonResponse(['data' => $this->serializeSubmission($submission)]);
|
||||
}
|
||||
|
||||
#[Route('/api/me/homework/{id}/submission/attachments', name: 'api_student_submission_upload_attachment', methods: ['POST'])]
|
||||
public function uploadAttachment(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$this->assertHomeworkBelongsToStudentClass($user, $id);
|
||||
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
$homeworkId = HomeworkId::fromString($id);
|
||||
|
||||
$submission = $this->submissionRepository->findByHomeworkAndStudent(
|
||||
$homeworkId,
|
||||
UserId::fromString($user->userId()),
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
if ($submission === null) {
|
||||
throw new NotFoundHttpException('Aucun brouillon trouvé. Veuillez d\'abord créer un brouillon.');
|
||||
}
|
||||
|
||||
if (!$submission->status->estModifiable()) {
|
||||
throw new ConflictHttpException('Le rendu a déjà été soumis.');
|
||||
}
|
||||
|
||||
$file = $request->files->get('file');
|
||||
|
||||
if ($file === null) {
|
||||
throw new BadRequestHttpException('Aucun fichier fourni.');
|
||||
}
|
||||
|
||||
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file */
|
||||
$attachment = ($this->uploadHandler)(new UploadSubmissionAttachmentCommand(
|
||||
tenantId: $user->tenantId(),
|
||||
submissionId: (string) $submission->id,
|
||||
filename: $file->getClientOriginalName(),
|
||||
mimeType: $file->getMimeType() ?? 'application/octet-stream',
|
||||
fileSize: $file->getSize(),
|
||||
tempFilePath: $file->getPathname(),
|
||||
));
|
||||
|
||||
$this->attachmentRepository->save($submission->id, $attachment);
|
||||
|
||||
return new JsonResponse(
|
||||
['data' => $this->serializeAttachment($attachment)],
|
||||
Response::HTTP_CREATED,
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/api/me/homework/{id}/submission', name: 'api_student_submission_get', methods: ['GET'])]
|
||||
public function getSubmission(string $id): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$this->assertHomeworkBelongsToStudentClass($user, $id);
|
||||
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
$homeworkId = HomeworkId::fromString($id);
|
||||
|
||||
$submission = $this->submissionRepository->findByHomeworkAndStudent(
|
||||
$homeworkId,
|
||||
UserId::fromString($user->userId()),
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
if ($submission === null) {
|
||||
return new JsonResponse(['data' => null]);
|
||||
}
|
||||
|
||||
$attachments = $this->attachmentRepository->findBySubmissionId($submission->id);
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $this->serializeSubmissionWithAttachments($submission, $attachments),
|
||||
]);
|
||||
}
|
||||
|
||||
private function assertHomeworkBelongsToStudentClass(SecurityUser $user, string $homeworkId): void
|
||||
{
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
$homework = $this->homeworkRepository->findById(HomeworkId::fromString($homeworkId), $tenantId);
|
||||
|
||||
if ($homework === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
$classId = $this->studentClassReader->currentClassId($user->userId(), $tenantId);
|
||||
|
||||
if ($classId === null || $classId !== (string) $homework->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 serializeSubmission(HomeworkSubmission $submission): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $submission->id,
|
||||
'homeworkId' => (string) $submission->homeworkId,
|
||||
'studentId' => (string) $submission->studentId,
|
||||
'responseHtml' => $submission->responseHtml,
|
||||
'status' => $submission->status->value,
|
||||
'submittedAt' => $submission->submittedAt?->format(DateTimeImmutable::ATOM),
|
||||
'createdAt' => $submission->createdAt->format(DateTimeImmutable::ATOM),
|
||||
'updatedAt' => $submission->updatedAt->format(DateTimeImmutable::ATOM),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<SubmissionAttachment> $attachments
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function serializeSubmissionWithAttachments(HomeworkSubmission $submission, array $attachments): array
|
||||
{
|
||||
$data = $this->serializeSubmission($submission);
|
||||
$data['attachments'] = array_map($this->serializeAttachment(...), $attachments);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function serializeAttachment(SubmissionAttachment $attachment): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $attachment->id,
|
||||
'filename' => $attachment->filename,
|
||||
'fileSize' => $attachment->fileSize,
|
||||
'mimeType' => $attachment->mimeType,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Controller;
|
||||
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Port\ClassStudentsReader;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachment;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Scolarite\Domain\Repository\SubmissionAttachmentRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_column;
|
||||
use function array_diff;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function in_array;
|
||||
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\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final readonly class TeacherSubmissionController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkSubmissionRepository $submissionRepository,
|
||||
private SubmissionAttachmentRepository $attachmentRepository,
|
||||
private ClassStudentsReader $classStudentsReader,
|
||||
#[Autowire('%kernel.project_dir%/var/storage')]
|
||||
private string $storageDir,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/api/homework/{id}/submissions', name: 'api_teacher_submission_list', methods: ['GET'])]
|
||||
public function list(string $id): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
$homeworkId = HomeworkId::fromString($id);
|
||||
|
||||
$homework = $this->homeworkRepository->findById($homeworkId, $tenantId);
|
||||
|
||||
if ($homework === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
if ((string) $homework->teacherId !== $user->userId()) {
|
||||
throw new AccessDeniedHttpException('Accès non autorisé.');
|
||||
}
|
||||
|
||||
$submissions = $this->submissionRepository->findByHomework($homeworkId, $tenantId);
|
||||
$students = $this->classStudentsReader->studentsInClass((string) $homework->classId, $tenantId);
|
||||
|
||||
$studentNameMap = [];
|
||||
foreach ($students as $student) {
|
||||
/** @var string $studentId */
|
||||
$studentId = $student['id'];
|
||||
/** @var string $studentName */
|
||||
$studentName = $student['name'];
|
||||
$studentNameMap[$studentId] = $studentName;
|
||||
}
|
||||
|
||||
$submittedStudentIds = array_map(
|
||||
static fn (HomeworkSubmission $s): string => (string) $s->studentId,
|
||||
$submissions,
|
||||
);
|
||||
|
||||
$rows = array_map(
|
||||
static fn (HomeworkSubmission $s): array => [
|
||||
'id' => (string) $s->id,
|
||||
'studentId' => (string) $s->studentId,
|
||||
'studentName' => $studentNameMap[(string) $s->studentId] ?? '',
|
||||
'status' => $s->status->value,
|
||||
'submittedAt' => $s->submittedAt?->format(DateTimeImmutable::ATOM),
|
||||
'createdAt' => $s->createdAt->format(DateTimeImmutable::ATOM),
|
||||
],
|
||||
$submissions,
|
||||
);
|
||||
|
||||
foreach ($students as $student) {
|
||||
/** @var string $sId */
|
||||
$sId = $student['id'];
|
||||
|
||||
if (!in_array($sId, $submittedStudentIds, true)) {
|
||||
/** @var string $sName */
|
||||
$sName = $student['name'];
|
||||
$rows[] = [
|
||||
'id' => null,
|
||||
'studentId' => $sId,
|
||||
'studentName' => $sName,
|
||||
'status' => 'not_submitted',
|
||||
'submittedAt' => null,
|
||||
'createdAt' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse(['data' => $rows]);
|
||||
}
|
||||
|
||||
#[Route('/api/homework/{id}/submissions/stats', name: 'api_teacher_submission_stats', methods: ['GET'])]
|
||||
public function stats(string $id): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
$homeworkId = HomeworkId::fromString($id);
|
||||
|
||||
$homework = $this->homeworkRepository->findById($homeworkId, $tenantId);
|
||||
|
||||
if ($homework === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
if ((string) $homework->teacherId !== $user->userId()) {
|
||||
throw new AccessDeniedHttpException('Accès non autorisé.');
|
||||
}
|
||||
|
||||
$submissions = $this->submissionRepository->findByHomework($homeworkId, $tenantId);
|
||||
$students = $this->classStudentsReader->studentsInClass((string) $homework->classId, $tenantId);
|
||||
|
||||
$submittedStudentIds = array_map(
|
||||
static fn (HomeworkSubmission $s): string => (string) $s->studentId,
|
||||
array_filter(
|
||||
$submissions,
|
||||
static fn (HomeworkSubmission $s): bool => !$s->status->estModifiable(),
|
||||
),
|
||||
);
|
||||
|
||||
$allStudentIds = array_column($students, 'id');
|
||||
$missingStudentIds = array_diff($allStudentIds, $submittedStudentIds);
|
||||
|
||||
$studentNameMap = [];
|
||||
foreach ($students as $student) {
|
||||
/** @var string $sId */
|
||||
$sId = $student['id'];
|
||||
/** @var string $sName */
|
||||
$sName = $student['name'];
|
||||
$studentNameMap[$sId] = $sName;
|
||||
}
|
||||
|
||||
$missingStudents = array_values(array_map(
|
||||
static fn (string $studentId): array => [
|
||||
'id' => $studentId,
|
||||
'name' => $studentNameMap[$studentId] ?? '',
|
||||
],
|
||||
array_filter(
|
||||
$missingStudentIds,
|
||||
static fn (string $studentId): bool => !in_array($studentId, $submittedStudentIds, true),
|
||||
),
|
||||
));
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'totalStudents' => count($students),
|
||||
'submittedCount' => count($submittedStudentIds),
|
||||
'missingStudents' => $missingStudents,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/homework/{id}/submissions/{submissionId}', name: 'api_teacher_submission_detail', methods: ['GET'])]
|
||||
public function detail(string $id, string $submissionId): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
$homeworkId = HomeworkId::fromString($id);
|
||||
|
||||
$homework = $this->homeworkRepository->findById($homeworkId, $tenantId);
|
||||
|
||||
if ($homework === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
if ((string) $homework->teacherId !== $user->userId()) {
|
||||
throw new AccessDeniedHttpException('Accès non autorisé.');
|
||||
}
|
||||
|
||||
$submission = $this->submissionRepository->findById(
|
||||
HomeworkSubmissionId::fromString($submissionId),
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
if ($submission === null || !$submission->homeworkId->equals($homeworkId)) {
|
||||
throw new NotFoundHttpException('Rendu non trouvé.');
|
||||
}
|
||||
|
||||
$attachments = $this->attachmentRepository->findBySubmissionId($submission->id);
|
||||
$students = $this->classStudentsReader->studentsInClass((string) $homework->classId, $tenantId);
|
||||
|
||||
$studentName = '';
|
||||
foreach ($students as $student) {
|
||||
if ($student['id'] === (string) $submission->studentId) {
|
||||
/** @var string $name */
|
||||
$name = $student['name'];
|
||||
$studentName = $name;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'id' => (string) $submission->id,
|
||||
'studentId' => (string) $submission->studentId,
|
||||
'studentName' => $studentName,
|
||||
'responseHtml' => $submission->responseHtml,
|
||||
'status' => $submission->status->value,
|
||||
'submittedAt' => $submission->submittedAt?->format(DateTimeImmutable::ATOM),
|
||||
'createdAt' => $submission->createdAt->format(DateTimeImmutable::ATOM),
|
||||
'attachments' => array_map(
|
||||
static fn (SubmissionAttachment $a): array => [
|
||||
'id' => (string) $a->id,
|
||||
'filename' => $a->filename,
|
||||
'fileSize' => $a->fileSize,
|
||||
'mimeType' => $a->mimeType,
|
||||
],
|
||||
$attachments,
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/homework/{homeworkId}/submissions/{submissionId}/attachments/{attachmentId}', name: 'api_teacher_submission_attachment_download', methods: ['GET'])]
|
||||
public function downloadAttachment(string $homeworkId, string $submissionId, 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é.');
|
||||
}
|
||||
|
||||
if ((string) $homework->teacherId !== $user->userId()) {
|
||||
throw new AccessDeniedHttpException('Accès non autorisé.');
|
||||
}
|
||||
|
||||
$submission = $this->submissionRepository->findById(
|
||||
HomeworkSubmissionId::fromString($submissionId),
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
if ($submission === null || !$submission->homeworkId->equals(HomeworkId::fromString($homeworkId))) {
|
||||
throw new NotFoundHttpException('Rendu non trouvé.');
|
||||
}
|
||||
|
||||
$attachments = $this->attachmentRepository->findBySubmissionId($submission->id);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if ((string) $attachment->id === $attachmentId) {
|
||||
$fullPath = $this->storageDir . '/' . $attachment->filePath;
|
||||
$realPath = realpath($fullPath);
|
||||
$realStorageDir = realpath($this->storageDir);
|
||||
|
||||
if ($realPath === false || $realStorageDir === false || !str_starts_with($realPath, $realStorageDir)) {
|
||||
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 getSecurityUser(): SecurityUser
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new AccessDeniedHttpException('Authentification requise.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\EventListener;
|
||||
|
||||
use App\Scolarite\Domain\Event\DevoirRendu;
|
||||
use App\Scolarite\Domain\Event\DevoirRenduEnRetard;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
/**
|
||||
* Placeholder : notifiera l'enseignant lorsqu'un élève rend un devoir.
|
||||
*
|
||||
* L'implémentation réelle dépend de l'Epic 9 (Communication & Notifications).
|
||||
* En attendant, on log l'événement pour la traçabilité.
|
||||
*/
|
||||
#[AsMessageHandler(bus: 'event.bus')]
|
||||
final readonly class NotifierEnseignantDevoirRenduListener
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(DevoirRendu|DevoirRenduEnRetard $event): void
|
||||
{
|
||||
$this->logger->info('Devoir rendu par un élève (notification enseignant à implémenter — Epic 9)', [
|
||||
'submissionId' => (string) $event->submissionId,
|
||||
'homeworkId' => (string) $event->homeworkId,
|
||||
'studentId' => (string) $event->studentId,
|
||||
'tenantId' => (string) $event->tenantId,
|
||||
'late' => $event instanceof DevoirRenduEnRetard,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\RenduNonTrouveException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionStatus;
|
||||
use App\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function array_map;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineHomeworkSubmissionRepository implements HomeworkSubmissionRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(HomeworkSubmission $submission): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO homework_submissions (id, tenant_id, homework_id, student_id, response_html, status, submitted_at, created_at, updated_at)
|
||||
VALUES (:id, :tenant_id, :homework_id, :student_id, :response_html, :status, :submitted_at, :created_at, :updated_at)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
response_html = EXCLUDED.response_html,
|
||||
status = EXCLUDED.status,
|
||||
submitted_at = EXCLUDED.submitted_at,
|
||||
updated_at = EXCLUDED.updated_at',
|
||||
[
|
||||
'id' => (string) $submission->id,
|
||||
'tenant_id' => (string) $submission->tenantId,
|
||||
'homework_id' => (string) $submission->homeworkId,
|
||||
'student_id' => (string) $submission->studentId,
|
||||
'response_html' => $submission->responseHtml,
|
||||
'status' => $submission->status->value,
|
||||
'submitted_at' => $submission->submittedAt?->format(DateTimeImmutable::ATOM),
|
||||
'created_at' => $submission->createdAt->format(DateTimeImmutable::ATOM),
|
||||
'updated_at' => $submission->updatedAt->format(DateTimeImmutable::ATOM),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(HomeworkSubmissionId $id, TenantId $tenantId): HomeworkSubmission
|
||||
{
|
||||
$submission = $this->findById($id, $tenantId);
|
||||
|
||||
if ($submission === null) {
|
||||
throw RenduNonTrouveException::withId($id);
|
||||
}
|
||||
|
||||
return $submission;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findById(HomeworkSubmissionId $id, TenantId $tenantId): ?HomeworkSubmission
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM homework_submissions 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 findByHomeworkAndStudent(
|
||||
HomeworkId $homeworkId,
|
||||
UserId $studentId,
|
||||
TenantId $tenantId,
|
||||
): ?HomeworkSubmission {
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM homework_submissions
|
||||
WHERE homework_id = :homework_id
|
||||
AND student_id = :student_id
|
||||
AND tenant_id = :tenant_id',
|
||||
[
|
||||
'homework_id' => (string) $homeworkId,
|
||||
'student_id' => (string) $studentId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByHomework(HomeworkId $homeworkId, TenantId $tenantId): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT * FROM homework_submissions
|
||||
WHERE homework_id = :homework_id
|
||||
AND tenant_id = :tenant_id
|
||||
ORDER BY submitted_at ASC NULLS LAST, created_at ASC',
|
||||
[
|
||||
'homework_id' => (string) $homeworkId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map($this->hydrate(...), $rows);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findStatusesByStudent(UserId $studentId, TenantId $tenantId, HomeworkId ...$homeworkIds): array
|
||||
{
|
||||
if ($homeworkIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = array_map(static fn (HomeworkId $id): string => (string) $id, $homeworkIds);
|
||||
|
||||
/** @var array<array{homework_id: string, status: string}> $rows */
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT homework_id, status FROM homework_submissions
|
||||
WHERE student_id = :student_id
|
||||
AND tenant_id = :tenant_id
|
||||
AND homework_id IN (:homework_ids)',
|
||||
[
|
||||
'student_id' => (string) $studentId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'homework_ids' => $ids,
|
||||
],
|
||||
['homework_ids' => ArrayParameterType::STRING],
|
||||
);
|
||||
|
||||
$result = array_fill_keys($ids, null);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
/** @var string $hwId */
|
||||
$hwId = $row['homework_id'];
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
$result[$hwId] = $status;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row */
|
||||
private function hydrate(array $row): HomeworkSubmission
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $row['id'];
|
||||
/** @var string $tenantId */
|
||||
$tenantId = $row['tenant_id'];
|
||||
/** @var string $homeworkId */
|
||||
$homeworkId = $row['homework_id'];
|
||||
/** @var string $studentId */
|
||||
$studentId = $row['student_id'];
|
||||
/** @var string|null $responseHtml */
|
||||
$responseHtml = $row['response_html'];
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
/** @var string|null $submittedAt */
|
||||
$submittedAt = $row['submitted_at'];
|
||||
/** @var string $createdAt */
|
||||
$createdAt = $row['created_at'];
|
||||
/** @var string $updatedAt */
|
||||
$updatedAt = $row['updated_at'];
|
||||
|
||||
return HomeworkSubmission::reconstitute(
|
||||
id: HomeworkSubmissionId::fromString($id),
|
||||
tenantId: TenantId::fromString($tenantId),
|
||||
homeworkId: HomeworkId::fromString($homeworkId),
|
||||
studentId: UserId::fromString($studentId),
|
||||
responseHtml: $responseHtml,
|
||||
status: SubmissionStatus::from($status),
|
||||
submittedAt: $submittedAt !== null ? new DateTimeImmutable($submittedAt) : null,
|
||||
createdAt: new DateTimeImmutable($createdAt),
|
||||
updatedAt: new DateTimeImmutable($updatedAt),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachment;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachmentId;
|
||||
use App\Scolarite\Domain\Repository\SubmissionAttachmentRepository;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineSubmissionAttachmentRepository implements SubmissionAttachmentRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findBySubmissionId(HomeworkSubmissionId $submissionId): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT * FROM submission_attachments WHERE submission_id = :submission_id',
|
||||
['submission_id' => (string) $submissionId],
|
||||
);
|
||||
|
||||
return array_map($this->hydrate(...), $rows);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(HomeworkSubmissionId $submissionId, SubmissionAttachment $attachment): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO submission_attachments (id, submission_id, filename, file_path, file_size, mime_type, uploaded_at)
|
||||
VALUES (:id, :submission_id, :filename, :file_path, :file_size, :mime_type, :uploaded_at)',
|
||||
[
|
||||
'id' => (string) $attachment->id,
|
||||
'submission_id' => (string) $submissionId,
|
||||
'filename' => $attachment->filename,
|
||||
'file_path' => $attachment->filePath,
|
||||
'file_size' => $attachment->fileSize,
|
||||
'mime_type' => $attachment->mimeType,
|
||||
'uploaded_at' => $attachment->uploadedAt->format(DateTimeImmutable::ATOM),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function delete(HomeworkSubmissionId $submissionId, SubmissionAttachment $attachment): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
'DELETE FROM submission_attachments WHERE id = :id AND submission_id = :submission_id',
|
||||
[
|
||||
'id' => (string) $attachment->id,
|
||||
'submission_id' => (string) $submissionId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row */
|
||||
private function hydrate(array $row): SubmissionAttachment
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $row['id'];
|
||||
/** @var string $filename */
|
||||
$filename = $row['filename'];
|
||||
/** @var string $filePath */
|
||||
$filePath = $row['file_path'];
|
||||
/** @var string|int $rawFileSize */
|
||||
$rawFileSize = $row['file_size'];
|
||||
$fileSize = (int) $rawFileSize;
|
||||
/** @var string $mimeType */
|
||||
$mimeType = $row['mime_type'];
|
||||
/** @var string $uploadedAt */
|
||||
$uploadedAt = $row['uploaded_at'];
|
||||
|
||||
return new SubmissionAttachment(
|
||||
id: SubmissionAttachmentId::fromString($id),
|
||||
filename: $filename,
|
||||
filePath: $filePath,
|
||||
fileSize: $fileSize,
|
||||
mimeType: $mimeType,
|
||||
uploadedAt: new DateTimeImmutable($uploadedAt),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Persistence\InMemory;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\RenduNonTrouveException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkSubmissionRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function array_filter;
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
|
||||
use Override;
|
||||
|
||||
final class InMemoryHomeworkSubmissionRepository implements HomeworkSubmissionRepository
|
||||
{
|
||||
/** @var array<string, HomeworkSubmission> */
|
||||
private array $byId = [];
|
||||
|
||||
#[Override]
|
||||
public function save(HomeworkSubmission $submission): void
|
||||
{
|
||||
$this->byId[(string) $submission->id] = $submission;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(HomeworkSubmissionId $id, TenantId $tenantId): HomeworkSubmission
|
||||
{
|
||||
$submission = $this->findById($id, $tenantId);
|
||||
|
||||
if ($submission === null) {
|
||||
throw RenduNonTrouveException::withId($id);
|
||||
}
|
||||
|
||||
return $submission;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findById(HomeworkSubmissionId $id, TenantId $tenantId): ?HomeworkSubmission
|
||||
{
|
||||
$submission = $this->byId[(string) $id] ?? null;
|
||||
|
||||
if ($submission === null || !$submission->tenantId->equals($tenantId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $submission;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByHomeworkAndStudent(
|
||||
HomeworkId $homeworkId,
|
||||
UserId $studentId,
|
||||
TenantId $tenantId,
|
||||
): ?HomeworkSubmission {
|
||||
foreach ($this->byId as $submission) {
|
||||
if (
|
||||
$submission->homeworkId->equals($homeworkId)
|
||||
&& $submission->studentId->equals($studentId)
|
||||
&& $submission->tenantId->equals($tenantId)
|
||||
) {
|
||||
return $submission;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByHomework(HomeworkId $homeworkId, TenantId $tenantId): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->byId,
|
||||
static fn (HomeworkSubmission $s): bool => $s->homeworkId->equals($homeworkId)
|
||||
&& $s->tenantId->equals($tenantId),
|
||||
));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findStatusesByStudent(UserId $studentId, TenantId $tenantId, HomeworkId ...$homeworkIds): array
|
||||
{
|
||||
$ids = array_map(static fn (HomeworkId $id): string => (string) $id, $homeworkIds);
|
||||
$result = array_fill_keys($ids, null);
|
||||
|
||||
foreach ($this->byId as $submission) {
|
||||
if (
|
||||
$submission->studentId->equals($studentId)
|
||||
&& $submission->tenantId->equals($tenantId)
|
||||
) {
|
||||
$hwId = (string) $submission->homeworkId;
|
||||
|
||||
if (array_key_exists($hwId, $result)) {
|
||||
$result[$hwId] = $submission->status->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Persistence\InMemory;
|
||||
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachment;
|
||||
use App\Scolarite\Domain\Repository\SubmissionAttachmentRepository;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
|
||||
use Override;
|
||||
|
||||
final class InMemorySubmissionAttachmentRepository implements SubmissionAttachmentRepository
|
||||
{
|
||||
/** @var array<string, array<SubmissionAttachment>> */
|
||||
private array $bySubmissionId = [];
|
||||
|
||||
#[Override]
|
||||
public function findBySubmissionId(HomeworkSubmissionId $submissionId): array
|
||||
{
|
||||
return $this->bySubmissionId[(string) $submissionId] ?? [];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(HomeworkSubmissionId $submissionId, SubmissionAttachment $attachment): void
|
||||
{
|
||||
$this->bySubmissionId[(string) $submissionId][] = $attachment;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function delete(HomeworkSubmissionId $submissionId, SubmissionAttachment $attachment): void
|
||||
{
|
||||
$key = (string) $submissionId;
|
||||
|
||||
if (!isset($this->bySubmissionId[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->bySubmissionId[$key] = array_values(array_filter(
|
||||
$this->bySubmissionId[$key],
|
||||
static fn (SubmissionAttachment $a): bool => (string) $a->id !== (string) $attachment->id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Service;
|
||||
|
||||
use App\Scolarite\Application\Port\ClassStudentsReader;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineClassStudentsReader implements ClassStudentsReader
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function studentsInClass(string $classId, TenantId $tenantId): array
|
||||
{
|
||||
/** @var array<array{id: string, first_name: string, last_name: string}> $rows */
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT u.id, u.first_name, u.last_name
|
||||
FROM class_assignments ca
|
||||
JOIN users u ON u.id = ca.user_id
|
||||
WHERE ca.school_class_id = :class_id
|
||||
AND ca.tenant_id = :tenant_id
|
||||
ORDER BY u.last_name ASC, u.first_name ASC',
|
||||
[
|
||||
'class_id' => $classId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => [
|
||||
'id' => $row['id'],
|
||||
'name' => $row['first_name'] . ' ' . $row['last_name'],
|
||||
],
|
||||
$rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user