Refactor code for improved readability and consistency; add flash message handling and enhance quiz functionality
Some checks failed
CI / Tests (push) Failing after 9s
CI / Docker Lint (push) Successful in 4s

This commit is contained in:
2025-03-12 23:18:13 +01:00
parent 448daed6ea
commit acf5c06fcc
21 changed files with 309 additions and 80 deletions

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\CandidateRepository;
use App\Repository\QuizRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:test-command',
description: 'Add a short description for your command',
)]
class TestCommand extends Command
{
public function __construct(private readonly CandidateRepository $candidateRepository, private readonly QuizRepository $quizRepository)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
new SymfonyStyle($input, $output);
dd($this->candidateRepository->getScores($this->quizRepository->find('1effa06a-8aca-6c52-b52b-3974eda7eed7')));
return Command::SUCCESS;
}
}

View File

@@ -9,11 +9,13 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBase
abstract class AbstractController extends AbstractBaseController
{
#[\Override]
protected function addFlash(FlashType|string $type, mixed $message): void
{
if ($type instanceof FlashType) {
$type = $type->value;
}
parent::addFlash($type, $message);
}
}

View File

@@ -22,6 +22,7 @@ use Symfony\Component\Routing\Attribute\Route;
class DashboardController extends AbstractDashboardController
{
#[Route('/admin', name: 'admin')]
#[\Override]
public function index(): Response
{
// return parent::index();
@@ -44,12 +45,14 @@ class DashboardController extends AbstractDashboardController
// return $this->render('some/path/my-dashboard.html.twig');
}
#[\Override]
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('TijdVoorDeTest');
}
#[\Override]
public function configureMenuItems(): iterable
{
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');

View File

@@ -6,6 +6,7 @@ namespace App\Controller;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Repository\CandidateRepository;
use App\Repository\SeasonRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
@@ -13,14 +14,14 @@ use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
#[AsController]
#[Route('/backoffice', name: 'backoffice_')]
final class BackofficeController extends AbstractController
{
public function __construct(private readonly SeasonRepository $seasonRepository)
{
}
public function __construct(
private readonly SeasonRepository $seasonRepository,
private readonly CandidateRepository $candidateRepository,
) {}
#[Route('/', name: 'index')]
#[Route('/backoffice/', name: 'index')]
public function index(): Response
{
$seasons = $this->seasonRepository->findAll();
@@ -30,7 +31,7 @@ final class BackofficeController extends AbstractController
]);
}
#[Route('/{seasonCode}', name: 'season')]
#[Route('/backoffice/{seasonCode}', name: 'season')]
public function season(Season $season): Response
{
return $this->render('backoffice/season.html.twig', [
@@ -38,12 +39,13 @@ final class BackofficeController extends AbstractController
]);
}
#[Route('/{seasonCode}/{quiz}', name: 'quiz')]
#[Route('/backoffice/{seasonCode}/{quiz}', name: 'quiz')]
public function quiz(Season $season, Quiz $quiz): Response
{
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $quiz,
'result' => $this->candidateRepository->getScores($quiz),
]);
}
}

View File

@@ -28,6 +28,7 @@ use Symfony\Component\Routing\Attribute\Route;
final class QuizController extends AbstractController
{
public const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
private const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
#[Route(path: '/', name: 'select_season', methods: ['GET', 'POST'])]
@@ -83,7 +84,7 @@ final class QuizController extends AbstractController
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
if (!$candidate instanceof Candidate) {
if (true === $season->isPreregisterCandidates()) {
if ($season->isPreregisterCandidates()) {
$this->addFlash(FlashType::Danger, 'Candidate not found');
return $this->redirectToRoute('enter_name', ['seasonCode' => $season->getSeasonCode()]);

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\EliminationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: EliminationRepository::class)]
class Elimination
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(type: Types::JSON)]
private array $data = [];
public function getId(): ?int
{
return $this->id;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
}

View File

@@ -96,4 +96,23 @@ class Question
return $this;
}
public function getErrors(): ?string
{
if (0 === \count($this->answers)) {
return 'This question has no answers';
}
$correctAnswers = $this->answers->filter(static fn (Answer $answer): ?bool => $answer->isRightAnswer())->count();
if (0 === $correctAnswers) {
return 'This question has no correct answers';
}
if ($correctAnswers > 1) {
return 'This question has multiple correct answers';
}
return null;
}
}

View File

@@ -11,9 +11,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class EnterNameType extends AbstractType
{
public function __construct(private TranslatorInterface $translator)
{
}
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{

View File

@@ -8,9 +8,7 @@ use Safe\Exceptions\UrlException;
class Base64
{
private function __construct()
{
}
private function __construct() {}
public static function base64_url_encode(string $input): string
{

View File

@@ -5,14 +5,20 @@ declare(strict_types=1);
namespace App\Repository;
use App\Entity\Candidate;
use App\Entity\Correction;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Helpers\Base64;
use DateInterval;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry;
use Safe\Exceptions\UrlException;
/**
* @extends ServiceEntityRepository<Candidate>
*
* @phpstan-type ResultArray array<string, array{0: Candidate, correct: int, time: DateInterval, corrections?: float, score: float}>
*/
class CandidateRepository extends ServiceEntityRepository
{
@@ -41,8 +47,54 @@ class CandidateRepository extends ServiceEntityRepository
{
$this->getEntityManager()->persist($candidate);
if (true === $flush) {
if ($flush) {
$this->getEntityManager()->flush();
}
}
/** @return ResultArray */
public function getScores(Quiz $quiz): array
{
$scoreTimeQb = $this->createQueryBuilder('c', 'c.id')
->select('c', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'max(ga.created) - min(ga.created) as time')
->join('c.givenAnswers', 'ga')
->join('ga.answer', 'a')
->where('ga.quiz = :quiz')
->groupBy('c.id')
->setParameter('quiz', $quiz);
$correctionsQb = $this->createQueryBuilder('c', 'c.id')
->select('c', 'cor.amount as corrections')
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
->setParameter('quiz', $quiz);
$merged = array_merge_recursive($scoreTimeQb->getQuery()->getArrayResult(), $correctionsQb->getQuery()->getArrayResult());
return $this->sortResults($this->calculateScore($merged));
}
/**
* @param array<string, array{0: Candidate, correct: int, time: \DateInterval, corrections?: float}> $in
*
* @return ResultArray
*/
private function calculateScore(array $in): array
{
return array_map(static fn ($candidate): array => [
...$candidate,
'score' => $candidate['correct'] + ($candidate['corrections'] ?? 0.0),
], $in);
}
/**
* @param ResultArray $results
*
* @return ResultArray
*/
private function sortResults(array $results): array
{
usort($results, static fn ($a, $b): int => $b['score'] <=> $a['score']);
return $results;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Elimination;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Elimination>
*/
class EliminationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Elimination::class);
}
// /**
// * @return Elimination[] Returns an array of Elimination objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('e.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Elimination
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -22,7 +22,7 @@ class GivenAnswerRepository extends ServiceEntityRepository
{
$this->getEntityManager()->persist($givenAnswer);
if (true === $flush) {
if ($flush) {
$this->getEntityManager()->flush();
}
}

View File

@@ -18,7 +18,5 @@ class QuizRepository extends ServiceEntityRepository
parent::__construct($registry, Quiz::class);
}
public function quizReault(Quiz $quiz): array
{
}
public function quizReault(Quiz $quiz): array {}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\CandidateRepository;
/**
* @phpstan-import-type ResultArray from CandidateRepository
*/
class EliminationService
{
/** @phpstan-param ResultArray $result */
public function createEliminationFromResult(array $result): void {}
}