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.
312 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|