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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user