mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 04:44:19 +01:00
Refactor code for improved readability and consistency; add flash message handling and enhance quiz functionality
This commit is contained in:
34
src/Command/TestCommand.php
Normal file
34
src/Command/TestCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
42
src/Entity/Elimination.php
Normal file
42
src/Entity/Elimination.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
45
src/Repository/EliminationRepository.php
Normal file
45
src/Repository/EliminationRepository.php
Normal 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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class GivenAnswerRepository extends ServiceEntityRepository
|
||||
{
|
||||
$this->getEntityManager()->persist($givenAnswer);
|
||||
|
||||
if (true === $flush) {
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
16
src/Service/EliminationService.php
Normal file
16
src/Service/EliminationService.php
Normal 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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user