mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-05 15:10:16 +02:00
281462fab8
* Added Gedmo stuff, fix translations * Add CSRF token validation across backoffice forms - Added CSRF validations to candidate correction, penalty, answer saving, and elimination forms. - Updated corresponding Twig templates to include CSRF token inputs. - Adjusted column count in `tab_result` template to maintain layout consistency. * Add unique index constraint for `quiz_candidate` with soft delete support - Updated migration to include a unique index on `quiz_candidate` table that excludes soft-deleted records. - Adjusted `QuizCandidate` entity to reflect the new unique constraint with `deleted_at` condition. * Add CSRF token validation for quiz-related actions - Added CSRF validation to `enableQuiz`, `clearQuiz`, `deleteQuiz`, `toggleCandidate`, and `prepareElimination` actions. - Updated Twig templates to replace links with POST forms to include CSRF tokens. - Set HTTP method restrictions for related endpoints to `POST`. * Fix unique index condition for `quiz_candidate` with soft deletes - Updated condition in unique index definition of `quiz_candidate` to add parentheses for clarity. - Adjusted related migration to reflect the revised condition. * Remove if for post an use methods in Route instead * Refactor CSRF token validation in backoffice controllers - Applied `#[IsCsrfTokenValid]` attribute for CSRF checks to simplify and standardize validation. - Removed manual `isCsrfTokenValid` calls and associated exception throwing. - Updated method signatures across affected endpoints to remove unnecessary `Request` dependency. - Ensured consistency in route HTTP method restrictions where applicable. * Add rector and phpstan * Add validation for answering incorrect quiz question - Added logic to prevent candidates from answering questions out of sequence in `QuizController`. - Updated Dutch translations to include the new error message. * Things
157 lines
6.7 KiB
PHP
157 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tvdt\Controller;
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
|
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
use Tvdt\Entity\Answer;
|
|
use Tvdt\Entity\Candidate;
|
|
use Tvdt\Entity\GivenAnswer;
|
|
use Tvdt\Entity\Question;
|
|
use Tvdt\Entity\Quiz;
|
|
use Tvdt\Entity\Season;
|
|
use Tvdt\Enum\FlashType;
|
|
use Tvdt\Form\EnterNameType;
|
|
use Tvdt\Form\SelectSeasonType;
|
|
use Tvdt\Helpers\Base64;
|
|
use Tvdt\Repository\AnswerRepository;
|
|
use Tvdt\Repository\CandidateRepository;
|
|
use Tvdt\Repository\QuestionRepository;
|
|
use Tvdt\Repository\QuizCandidateRepository;
|
|
use Tvdt\Repository\SeasonRepository;
|
|
|
|
#[AsController]
|
|
final class QuizController extends AbstractController
|
|
{
|
|
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $entityManager, private readonly SeasonRepository $seasonRepository, private readonly CandidateRepository $candidateRepository, private readonly QuestionRepository $questionRepository, private readonly AnswerRepository $answerRepository, private readonly QuizCandidateRepository $quizCandidateRepository) {}
|
|
|
|
#[Route(path: '/', name: 'tvdt_quiz_select_season', methods: ['GET', 'POST'])]
|
|
public function selectSeason(Request $request): Response
|
|
{
|
|
$form = $this->createForm(SelectSeasonType::class);
|
|
$form->handleRequest($request);
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$seasonCode = $form->get('season_code')->getData();
|
|
|
|
if ([] === $this->seasonRepository->findBy(['seasonCode' => $seasonCode])) {
|
|
$this->addFlash(FlashType::Warning, $this->translator->trans('Invalid season code'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_select_season');
|
|
}
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_enter_name', ['seasonCode' => $seasonCode]);
|
|
}
|
|
|
|
return $this->render('quiz/select_season.html.twig', ['form' => $form]);
|
|
}
|
|
|
|
#[Route(path: '/{seasonCode:season}', name: 'tvdt_quiz_enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
|
|
public function enterName(
|
|
Request $request,
|
|
Season $season,
|
|
): Response {
|
|
$form = $this->createForm(EnterNameType::class);
|
|
|
|
$form->handleRequest($request);
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$name = $form->get('name')->getData();
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_quiz_page', ['seasonCode' => $season->seasonCode, 'nameHash' => Base64::base64UrlEncode($name)]);
|
|
}
|
|
|
|
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
|
|
}
|
|
|
|
#[IsCsrfTokenValid('question', tokenKey: 'token', methods: ['POST'])]
|
|
#[Route(
|
|
path: '/{seasonCode:season}/{nameHash}',
|
|
name: 'tvdt_quiz_quiz_page',
|
|
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
|
|
methods: ['GET', 'POST'],
|
|
)]
|
|
public function quizPage(
|
|
Season $season,
|
|
string $nameHash,
|
|
Request $request,
|
|
): Response {
|
|
$candidate = $this->candidateRepository->getCandidateByHash($season, $nameHash);
|
|
|
|
if (!$candidate instanceof Candidate) {
|
|
$this->addFlash(FlashType::Danger, $this->translator->trans('Candidate not found'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_enter_name', ['seasonCode' => $season->seasonCode]);
|
|
}
|
|
|
|
$quiz = $season->activeQuiz;
|
|
|
|
if (!$quiz instanceof Quiz) {
|
|
$this->addFlash(FlashType::Warning, $this->translator->trans('There is no active quiz'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_enter_name', ['seasonCode' => $season->seasonCode]);
|
|
}
|
|
|
|
if ($request->isMethod('POST')) {
|
|
// TODO: Extract saving answer logic to a service
|
|
// Check if candidate is inactive for this quiz
|
|
$quizCandidate = $this->quizCandidateRepository->findOneBy(['quiz' => $quiz, 'candidate' => $candidate]);
|
|
if (null !== $quizCandidate && !$quizCandidate->active) {
|
|
$this->addFlash(FlashType::Danger, $this->translator->trans('You are not allowed to answer this quiz'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_enter_name', ['seasonCode' => $season->seasonCode]);
|
|
}
|
|
|
|
$answer = $this->answerRepository->findOneBy(['id' => $request->request->get('answer')]);
|
|
|
|
if (!$answer instanceof Answer) {
|
|
$this->addFlash(FlashType::Danger, $this->translator->trans('Please select an answer'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_quiz_page', ['seasonCode' => $season->seasonCode, 'nameHash' => $nameHash]);
|
|
}
|
|
|
|
if ($answer->question !== $this->questionRepository->findNextQuestionForCandidate($candidate)) {
|
|
$this->addFlash(FlashType::Danger, $this->translator->trans('You cannot answer this question'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_quiz_page', ['seasonCode' => $season->seasonCode, 'nameHash' => $nameHash]);
|
|
}
|
|
|
|
$givenAnswer = new GivenAnswer($candidate, $answer->question->quiz, $answer);
|
|
$this->entityManager->persist($givenAnswer);
|
|
$this->entityManager->flush();
|
|
|
|
// end of extracting saving answer logic
|
|
return $this->redirectToRoute('tvdt_quiz_quiz_page', ['seasonCode' => $season->seasonCode, 'nameHash' => $nameHash]);
|
|
}
|
|
|
|
// TODO: Extract getting next question logic to a service
|
|
$question = $this->questionRepository->findNextQuestionForCandidate($candidate);
|
|
|
|
// Keep creating flash here based on the return type of service call
|
|
if (!$question instanceof Question) {
|
|
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz completed'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_enter_name', ['seasonCode' => $season->seasonCode]);
|
|
}
|
|
|
|
$result = $this->quizCandidateRepository->createIfNotExist($quiz, $candidate);
|
|
|
|
// Check if candidate is inactive
|
|
if (null === $result) {
|
|
$this->addFlash(FlashType::Danger, $this->translator->trans('You are not allowed to answer this quiz'));
|
|
|
|
return $this->redirectToRoute('tvdt_quiz_enter_name', ['seasonCode' => $season->seasonCode]);
|
|
}
|
|
|
|
// end of extracting getting next question logic
|
|
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question, 'season' => $season]);
|
|
}
|
|
}
|