mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-06 23:50:16 +02:00
64a09453e6
- Replace Bootstrap accordion with flat card list for questions - Add HTML5 drag-and-drop reordering with placeholder-between-cards UX and amber/green/red save status indicator next to the heading - Add edit button per question opening a Bootstrap modal (bo--modal-form Stimulus controller with X-Modal-Request header pattern) - Show read-only view button instead of edit for locked/finalized quizzes - Add BankQuestion edit modal in question bank tab using same infrastructure - Move modal action buttons into modal-footer via <template data-modal-footer> - Fix IS_AUTHENTICATED_FULLY 403: replace ROLE_USER with IS_AUTHENTICATED on all backoffice controllers and in security.yaml access_control
149 lines
5.0 KiB
PHP
149 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tvdt\Controller\Backoffice;
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Routing\Requirement\Requirement;
|
|
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
use Tvdt\Controller\AbstractController;
|
|
use Tvdt\Entity\Question;
|
|
use Tvdt\Entity\Quiz;
|
|
use Tvdt\Entity\Season;
|
|
use Tvdt\Enum\FlashType;
|
|
use Tvdt\Form\QuestionFormType;
|
|
use Tvdt\Security\Voter\SeasonVoter;
|
|
|
|
#[AsController]
|
|
#[IsGranted('IS_AUTHENTICATED')]
|
|
class QuizQuestionController extends AbstractController
|
|
{
|
|
public function __construct(
|
|
private readonly EntityManagerInterface $em,
|
|
private readonly TranslatorInterface $translator,
|
|
) {}
|
|
|
|
#[IsGranted(SeasonVoter::MODIFY_QUIZ_CONTENT, subject: 'question')]
|
|
#[Route(
|
|
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/question/{question}/edit',
|
|
name: 'tvdt_backoffice_quiz_question_edit',
|
|
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID, 'question' => Requirement::UUID],
|
|
)]
|
|
public function edit(Season $season, Quiz $quiz, Question $question, Request $request): Response
|
|
{
|
|
if ($question->quiz !== $quiz || $quiz->season !== $season) {
|
|
throw new NotFoundHttpException();
|
|
}
|
|
|
|
$isModalRequest = $request->headers->has('X-Modal-Request');
|
|
|
|
$form = $this->createForm(QuestionFormType::class, $question);
|
|
$form->handleRequest($request);
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$this->applyAnswerOrdering($question);
|
|
$this->em->flush();
|
|
|
|
$this->addFlash(FlashType::Success, $this->translator->trans('Question updated'));
|
|
|
|
if ($isModalRequest) {
|
|
return new Response('', Response::HTTP_NO_CONTENT);
|
|
}
|
|
|
|
return $this->redirectToRoute('tvdt_backoffice_quiz_overview', [
|
|
'seasonCode' => $season->seasonCode,
|
|
'quiz' => $quiz->id,
|
|
]);
|
|
}
|
|
|
|
$template = $isModalRequest
|
|
? 'backoffice/quiz/_question_form_body.html.twig'
|
|
: 'backoffice/quiz/question_form.html.twig';
|
|
|
|
$response = $this->render($template, [
|
|
'season' => $season,
|
|
'quiz' => $quiz,
|
|
'question' => $question,
|
|
'form' => $form,
|
|
'isModal' => $isModalRequest,
|
|
]);
|
|
|
|
if ($form->isSubmitted()) {
|
|
$response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
#[Route(
|
|
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/question/{question}/view',
|
|
name: 'tvdt_backoffice_quiz_question_view',
|
|
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID, 'question' => Requirement::UUID],
|
|
)]
|
|
public function view(Season $season, Quiz $quiz, Question $question): Response
|
|
{
|
|
if ($question->quiz !== $quiz || $quiz->season !== $season) {
|
|
throw new NotFoundHttpException();
|
|
}
|
|
|
|
return $this->render('backoffice/quiz/_question_detail_body.html.twig', [
|
|
'question' => $question,
|
|
]);
|
|
}
|
|
|
|
#[IsCsrfTokenValid('question_reorder')]
|
|
#[IsGranted(SeasonVoter::MODIFY_QUIZ_CONTENT, subject: 'quiz')]
|
|
#[Route(
|
|
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/questions/reorder',
|
|
name: 'tvdt_backoffice_quiz_questions_reorder',
|
|
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
|
|
methods: ['POST'],
|
|
)]
|
|
public function reorder(Season $season, Quiz $quiz, Request $request): Response
|
|
{
|
|
if ($quiz->season !== $season) {
|
|
throw new NotFoundHttpException();
|
|
}
|
|
|
|
/** @var list<string> $ordering */
|
|
$ordering = $request->request->all('ordering');
|
|
|
|
$questionsById = [];
|
|
foreach ($quiz->questions as $question) {
|
|
$questionsById[$question->id->toString()] = $question;
|
|
}
|
|
|
|
foreach ($ordering as $questionId) {
|
|
if (!isset($questionsById[$questionId])) {
|
|
throw new BadRequestHttpException(\sprintf('Unknown question id: %s', $questionId));
|
|
}
|
|
}
|
|
|
|
$position = 1;
|
|
foreach ($ordering as $questionId) {
|
|
$questionsById[$questionId]->ordering = $position++;
|
|
}
|
|
|
|
$this->em->flush();
|
|
|
|
return new Response('', Response::HTTP_NO_CONTENT);
|
|
}
|
|
|
|
private function applyAnswerOrdering(Question $question): void
|
|
{
|
|
$ordering = 1;
|
|
foreach ($question->answers as $answer) {
|
|
$answer->ordering = $ordering++;
|
|
}
|
|
}
|
|
}
|