Add AbstractController, implement flash message handling, and refactor repositories

This commit is contained in:
2025-03-05 21:01:57 +01:00
parent 29bc74fe4f
commit 0ccce51af8
18 changed files with 111 additions and 21 deletions

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Enum\FlashType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBaseController;
abstract class AbstractController extends AbstractBaseController
{
protected function addFlash(FlashType|string $type, mixed $message): void
{
if ($type instanceof FlashType) {
$type = $type->value;
}
parent::addFlash($type, $message);
}
}

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Answer; use App\Entity\Answer;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Candidate; use App\Entity\Candidate;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Correction; use App\Entity\Correction;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\GivenAnswer; use App\Entity\GivenAnswer;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Question; use App\Entity\Question;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Quiz; use App\Entity\Quiz;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Season; use App\Entity\Season;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\User; use App\Entity\User;

View File

@@ -4,15 +4,21 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\Answer;
use App\Entity\Candidate; use App\Entity\Candidate;
use App\Entity\GivenAnswer;
use App\Entity\Question;
use App\Entity\Season; use App\Entity\Season;
use App\Enum\FlashType; use App\Enum\FlashType;
use App\Form\EnterNameType; use App\Form\EnterNameType;
use App\Form\SelectSeasonType; use App\Form\SelectSeasonType;
use App\Helpers\Base64; use App\Helpers\Base64;
use App\Repository\AnswerRepository;
use App\Repository\CandidateRepository; use App\Repository\CandidateRepository;
use App\Repository\GivenAnswerRepository;
use App\Repository\QuestionRepository; use App\Repository\QuestionRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
@@ -42,6 +48,7 @@ class QuizController extends AbstractController
#[Route(path: '/{seasonCode}', name: 'enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])] #[Route(path: '/{seasonCode}', name: 'enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
public function enterName( public function enterName(
Request $request, Request $request,
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
Season $season, Season $season,
): Response { ): Response {
$form = $this->createForm(EnterNameType::class); $form = $this->createForm(EnterNameType::class);
@@ -64,22 +71,50 @@ class QuizController extends AbstractController
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX], requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
)] )]
public function quizPage( public function quizPage(
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
Season $season, Season $season,
string $nameHash, string $nameHash,
CandidateRepository $candidateRepository, CandidateRepository $candidateRepository,
QuestionRepository $questionRepository, QuestionRepository $questionRepository,
AnswerRepository $answerRepository,
GivenAnswerRepository $givenAnswerRepository,
Request $request,
): Response { ): Response {
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash); $candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
if (!$candidate instanceof Candidate) { if (!$candidate instanceof Candidate) {
// Add option to add new candidate when preregister is disabled if (false === $season->isPreregisterCandidates()) {
$this->addFlash(FlashType::Danger->value, 'Candidate not found'); $candidate = new Candidate(Base64::base64_url_decode($nameHash));
$candidateRepository->save($candidate);
} else {
$this->addFlash(FlashType::Danger, 'Candidate not found');
return $this->redirectToRoute('enter_name', ['seasonCode' => $season->getSeasonCode()]); return $this->redirectToRoute('enter_name', ['seasonCode' => $season->getSeasonCode()]);
} }
}
if ('POST' === $request->getMethod()) {
$answer = $answerRepository->findOneBy(['id' => $request->request->get('answer')]);
if (!$answer instanceof Answer) {
throw new BadRequestException('Invalid Answer ID');
}
$givenAnswer = new GivenAnswer();
$givenAnswer->setCandidate($candidate)
->setAnswer($answer)
->setQuiz($answer->getQuestion()->getQuiz());
$givenAnswerRepository->save($givenAnswer);
}
$question = $questionRepository->findNextQuestionForCandidate($candidate); $question = $questionRepository->findNextQuestionForCandidate($candidate);
if (!$question instanceof Question) {
$this->addFlash(FlashType::Success, 'Quiz completed');
return $this->redirectToRoute('enter_name', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]); return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]);
} }
} }

View File

@@ -12,6 +12,6 @@ enum FlashType: string
case Danger = 'danger'; case Danger = 'danger';
case Warning = 'warning'; case Warning = 'warning';
case Info = 'info'; case Info = 'info';
case Ligt = 'light'; case Light = 'light';
case Dark = 'dark'; case Dark = 'dark';
} }

View File

@@ -7,7 +7,6 @@ namespace App\Form;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class EnterNameType extends AbstractType class EnterNameType extends AbstractType
@@ -22,14 +21,6 @@ class EnterNameType extends AbstractType
->add('name', TextType::class, ->add('name', TextType::class,
['required' => true, 'label' => $this->translator->trans('Enter your name')], ['required' => true, 'label' => $this->translator->trans('Enter your name')],
) )
// ->add('submit', SubmitType::class, ['label' => 'Start quiz'])
; ;
} }
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
} }

View File

@@ -14,7 +14,7 @@ class Base64
public static function base64_url_encode(string $input): string public static function base64_url_encode(string $input): string
{ {
return strtr(base64_encode($input), '+/', '-_'); return rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
} }
/** @throws UrlException */ /** @throws UrlException */

View File

@@ -36,4 +36,13 @@ class CandidateRepository extends ServiceEntityRepository
->setParameter('name', $name) ->setParameter('name', $name)
->getQuery()->getOneOrNullResult(); ->getQuery()->getOneOrNullResult();
} }
public function save(Candidate $candidate, bool $flush = true): void
{
$this->getEntityManager()->persist($candidate);
if (true === $flush) {
$this->getEntityManager()->flush();
}
}
} }

View File

@@ -17,4 +17,13 @@ class GivenAnswerRepository extends ServiceEntityRepository
{ {
parent::__construct($registry, GivenAnswer::class); parent::__construct($registry, GivenAnswer::class);
} }
public function save(GivenAnswer $givenAnswer, bool $flush = true): void
{
$this->getEntityManager()->persist($givenAnswer);
if (true === $flush) {
$this->getEntityManager()->flush();
}
}
} }

View File

@@ -20,13 +20,13 @@ class QuestionRepository extends ServiceEntityRepository
parent::__construct($registry, Question::class); parent::__construct($registry, Question::class);
} }
public function findNextQuestionForCandidate(Candidate $candidate): Question public function findNextQuestionForCandidate(Candidate $candidate): ?Question
{ {
$qb = $this->createQueryBuilder('q'); $qb = $this->createQueryBuilder('q');
return $qb->join('q.quiz', 'qz') return $qb->join('q.quiz', 'qz')
->andWhere($qb->expr()->notIn('q.id', $this->getEntityManager()->createQueryBuilder() ->andWhere($qb->expr()->notIn('q.id', $this->getEntityManager()->createQueryBuilder()
->select('ga.id') ->select('q1')
->from(GivenAnswer::class, 'ga') ->from(GivenAnswer::class, 'ga')
->join('ga.answer', 'a') ->join('ga.answer', 'a')
->join('a.question', 'q1') ->join('a.question', 'q1')
@@ -38,6 +38,6 @@ class QuestionRepository extends ServiceEntityRepository
->setMaxResults(1) ->setMaxResults(1)
->setParameter('candidate', $candidate) ->setParameter('candidate', $candidate)
->setParameter('quiz', $candidate->getSeason()->getActiveQuiz()) ->setParameter('quiz', $candidate->getSeason()->getActiveQuiz())
->getQuery()->getSingleResult(); ->getQuery()->getOneOrNullResult();
} }
} }

View File

@@ -3,7 +3,16 @@
Candiadte: {{ candidate.name }}<br/> Candiadte: {{ candidate.name }}<br/>
{{ question.question }}<br/> {{ question.question }}<br/>
<form method="post">
{% for answer in question.answers %} {% for answer in question.answers %}
<input type="radio" name="answer" value="{{ answer.id }}"> {{ answer.text }} <div>
<button class="btn btn-outline-success"
type="submit"
name="answer"
value="{{ answer.id }}">{{ answer.text }}</button>
</div>
{% else %}
Weirdly enough this question has no answers...
{% endfor %} {% endfor %}
</form>
{% endblock body %} {% endblock body %}