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,198 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\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\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionStatus;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class HomeworkSubmissionTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string HOMEWORK_ID = '550e8400-e29b-41d4-a716-446655440050';
|
||||
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440060';
|
||||
|
||||
#[Test]
|
||||
public function creerBrouillonCreatesDraftSubmission(): void
|
||||
{
|
||||
$submission = $this->createDraft();
|
||||
|
||||
self::assertSame(SubmissionStatus::DRAFT, $submission->status);
|
||||
self::assertNull($submission->submittedAt);
|
||||
self::assertEmpty($submission->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creerBrouillonSetsAllProperties(): void
|
||||
{
|
||||
$tenantId = TenantId::fromString(self::TENANT_ID);
|
||||
$homeworkId = HomeworkId::fromString(self::HOMEWORK_ID);
|
||||
$studentId = UserId::fromString(self::STUDENT_ID);
|
||||
$now = new DateTimeImmutable('2026-03-24 10:00:00');
|
||||
|
||||
$submission = HomeworkSubmission::creerBrouillon(
|
||||
tenantId: $tenantId,
|
||||
homeworkId: $homeworkId,
|
||||
studentId: $studentId,
|
||||
responseHtml: '<p>Ma réponse</p>',
|
||||
now: $now,
|
||||
);
|
||||
|
||||
self::assertTrue($submission->tenantId->equals($tenantId));
|
||||
self::assertTrue($submission->homeworkId->equals($homeworkId));
|
||||
self::assertTrue($submission->studentId->equals($studentId));
|
||||
self::assertSame('<p>Ma réponse</p>', $submission->responseHtml);
|
||||
self::assertSame(SubmissionStatus::DRAFT, $submission->status);
|
||||
self::assertEquals($now, $submission->createdAt);
|
||||
self::assertEquals($now, $submission->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creerBrouillonAllowsNullResponse(): void
|
||||
{
|
||||
$submission = HomeworkSubmission::creerBrouillon(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
homeworkId: HomeworkId::fromString(self::HOMEWORK_ID),
|
||||
studentId: UserId::fromString(self::STUDENT_ID),
|
||||
responseHtml: null,
|
||||
now: new DateTimeImmutable('2026-03-24 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertNull($submission->responseHtml);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierBrouillonUpdatesResponse(): void
|
||||
{
|
||||
$submission = $this->createDraft();
|
||||
$updatedAt = new DateTimeImmutable('2026-03-24 11:00:00');
|
||||
|
||||
$submission->modifierBrouillon('<p>Réponse modifiée</p>', $updatedAt);
|
||||
|
||||
self::assertSame('<p>Réponse modifiée</p>', $submission->responseHtml);
|
||||
self::assertEquals($updatedAt, $submission->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierBrouillonThrowsWhenAlreadySubmitted(): void
|
||||
{
|
||||
$submission = $this->createDraft();
|
||||
$submission->soumettre(
|
||||
dueDate: new DateTimeImmutable('2026-04-15'),
|
||||
now: new DateTimeImmutable('2026-03-24 12:00:00'),
|
||||
);
|
||||
|
||||
$this->expectException(RenduDejaSoumisException::class);
|
||||
|
||||
$submission->modifierBrouillon('<p>Modification</p>', new DateTimeImmutable('2026-03-24 13:00:00'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function soumettreOnTimeRecordsDevoirRenduEvent(): void
|
||||
{
|
||||
$submission = $this->createDraft();
|
||||
$dueDate = new DateTimeImmutable('2026-04-15 23:59:59');
|
||||
$now = new DateTimeImmutable('2026-04-10 14:00:00');
|
||||
|
||||
$submission->soumettre(dueDate: $dueDate, now: $now);
|
||||
|
||||
self::assertSame(SubmissionStatus::SUBMITTED, $submission->status);
|
||||
self::assertEquals($now, $submission->submittedAt);
|
||||
|
||||
$events = $submission->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(DevoirRendu::class, $events[0]);
|
||||
self::assertSame($submission->id, $events[0]->submissionId);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function soumettreLateSetsLateStatusAndRecordsEvent(): void
|
||||
{
|
||||
$submission = $this->createDraft();
|
||||
$dueDate = new DateTimeImmutable('2026-03-20 23:59:59');
|
||||
$now = new DateTimeImmutable('2026-03-24 14:00:00');
|
||||
|
||||
$submission->soumettre(dueDate: $dueDate, now: $now);
|
||||
|
||||
self::assertSame(SubmissionStatus::LATE, $submission->status);
|
||||
self::assertEquals($now, $submission->submittedAt);
|
||||
|
||||
$events = $submission->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(DevoirRenduEnRetard::class, $events[0]);
|
||||
self::assertSame($submission->id, $events[0]->submissionId);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function soumettreThrowsWhenAlreadySubmitted(): void
|
||||
{
|
||||
$submission = $this->createDraft();
|
||||
$submission->soumettre(
|
||||
dueDate: new DateTimeImmutable('2026-04-15'),
|
||||
now: new DateTimeImmutable('2026-03-24 12:00:00'),
|
||||
);
|
||||
|
||||
$this->expectException(RenduDejaSoumisException::class);
|
||||
|
||||
$submission->soumettre(
|
||||
dueDate: new DateTimeImmutable('2026-04-15'),
|
||||
now: new DateTimeImmutable('2026-03-24 13:00:00'),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reconstituteRestoresAllPropertiesWithoutEvents(): void
|
||||
{
|
||||
$id = \App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId::generate();
|
||||
$tenantId = TenantId::fromString(self::TENANT_ID);
|
||||
$homeworkId = HomeworkId::fromString(self::HOMEWORK_ID);
|
||||
$studentId = UserId::fromString(self::STUDENT_ID);
|
||||
$createdAt = new DateTimeImmutable('2026-03-24 10:00:00');
|
||||
$updatedAt = new DateTimeImmutable('2026-03-24 12:00:00');
|
||||
$submittedAt = new DateTimeImmutable('2026-03-24 11:30:00');
|
||||
|
||||
$submission = HomeworkSubmission::reconstitute(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
homeworkId: $homeworkId,
|
||||
studentId: $studentId,
|
||||
responseHtml: '<p>Réponse</p>',
|
||||
status: SubmissionStatus::SUBMITTED,
|
||||
submittedAt: $submittedAt,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
|
||||
self::assertTrue($submission->id->equals($id));
|
||||
self::assertTrue($submission->tenantId->equals($tenantId));
|
||||
self::assertTrue($submission->homeworkId->equals($homeworkId));
|
||||
self::assertTrue($submission->studentId->equals($studentId));
|
||||
self::assertSame('<p>Réponse</p>', $submission->responseHtml);
|
||||
self::assertSame(SubmissionStatus::SUBMITTED, $submission->status);
|
||||
self::assertEquals($submittedAt, $submission->submittedAt);
|
||||
self::assertEquals($createdAt, $submission->createdAt);
|
||||
self::assertEquals($updatedAt, $submission->updatedAt);
|
||||
self::assertEmpty($submission->pullDomainEvents());
|
||||
}
|
||||
|
||||
private function createDraft(): HomeworkSubmission
|
||||
{
|
||||
return HomeworkSubmission::creerBrouillon(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
homeworkId: HomeworkId::fromString(self::HOMEWORK_ID),
|
||||
studentId: UserId::fromString(self::STUDENT_ID),
|
||||
responseHtml: '<p>Ma réponse</p>',
|
||||
now: new DateTimeImmutable('2026-03-24 10:00:00'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Domain\Model\HomeworkSubmission;
|
||||
|
||||
use App\Scolarite\Domain\Exception\PieceJointeInvalideException;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachment;
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionAttachmentId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SubmissionAttachmentTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
#[DataProvider('validMimeTypes')]
|
||||
public function acceptsAllowedMimeTypes(string $mimeType): void
|
||||
{
|
||||
$attachment = new SubmissionAttachment(
|
||||
id: SubmissionAttachmentId::generate(),
|
||||
filename: 'test.pdf',
|
||||
filePath: 'submissions/tenant/hw/sub/test.pdf',
|
||||
fileSize: 1024,
|
||||
mimeType: $mimeType,
|
||||
uploadedAt: new DateTimeImmutable(),
|
||||
);
|
||||
|
||||
self::assertSame($mimeType, $attachment->mimeType);
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{string}> */
|
||||
public static function validMimeTypes(): iterable
|
||||
{
|
||||
yield 'pdf' => ['application/pdf'];
|
||||
yield 'jpeg' => ['image/jpeg'];
|
||||
yield 'png' => ['image/png'];
|
||||
yield 'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejectsInvalidMimeType(): void
|
||||
{
|
||||
$this->expectException(PieceJointeInvalideException::class);
|
||||
|
||||
new SubmissionAttachment(
|
||||
id: SubmissionAttachmentId::generate(),
|
||||
filename: 'test.exe',
|
||||
filePath: 'submissions/tenant/hw/sub/test.exe',
|
||||
fileSize: 1024,
|
||||
mimeType: 'application/x-msdownload',
|
||||
uploadedAt: new DateTimeImmutable(),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejectsFileTooLarge(): void
|
||||
{
|
||||
$this->expectException(PieceJointeInvalideException::class);
|
||||
|
||||
new SubmissionAttachment(
|
||||
id: SubmissionAttachmentId::generate(),
|
||||
filename: 'large.pdf',
|
||||
filePath: 'submissions/tenant/hw/sub/large.pdf',
|
||||
fileSize: 11 * 1024 * 1024, // 11 Mo
|
||||
mimeType: 'application/pdf',
|
||||
uploadedAt: new DateTimeImmutable(),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function acceptsFileAtMaxSize(): void
|
||||
{
|
||||
$attachment = new SubmissionAttachment(
|
||||
id: SubmissionAttachmentId::generate(),
|
||||
filename: 'max.pdf',
|
||||
filePath: 'submissions/tenant/hw/sub/max.pdf',
|
||||
fileSize: 10 * 1024 * 1024, // exactly 10 Mo
|
||||
mimeType: 'application/pdf',
|
||||
uploadedAt: new DateTimeImmutable(),
|
||||
);
|
||||
|
||||
self::assertSame(10 * 1024 * 1024, $attachment->fileSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Domain\Model\HomeworkSubmission;
|
||||
|
||||
use App\Scolarite\Domain\Model\HomeworkSubmission\SubmissionStatus;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SubmissionStatusTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function draftEstModifiable(): void
|
||||
{
|
||||
self::assertTrue(SubmissionStatus::DRAFT->estModifiable());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function submittedNestPasModifiable(): void
|
||||
{
|
||||
self::assertFalse(SubmissionStatus::SUBMITTED->estModifiable());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function lateNestPasModifiable(): void
|
||||
{
|
||||
self::assertFalse(SubmissionStatus::LATE->estModifiable());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function backingValues(): void
|
||||
{
|
||||
self::assertSame('draft', SubmissionStatus::DRAFT->value);
|
||||
self::assertSame('submitted', SubmissionStatus::SUBMITTED->value);
|
||||
self::assertSame('late', SubmissionStatus::LATE->value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user