Files
Classeo/backend/src/Scolarite/Infrastructure/Api/Controller/TeacherSubmissionController.php
Mathias STRASSER e72867932d
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Provisionner automatiquement un nouvel établissement
Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
2026-04-13 15:44:38 +02:00

312 lines
11 KiB
PHP

<?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\Application\Port\FileStorage;
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 fclose;
use function fpassthru;
use function in_array;
use RuntimeException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
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,
private FileStorage $fileStorage,
) {
}
#[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): StreamedResponse
{
$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) {
try {
$stream = $this->fileStorage->readStream($attachment->filePath);
} catch (RuntimeException) {
throw new NotFoundHttpException('Pièce jointe non trouvée.');
}
$response = new StreamedResponse(static function () use ($stream): void {
try {
fpassthru($stream);
} finally {
fclose($stream);
}
});
$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_INLINE,
$attachment->filename,
);
$response->headers->set('Content-Type', $attachment->mimeType);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Length', (string) $attachment->fileSize);
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;
}
}