Refactor Candidate and Quiz entities, rename Correction to QuizCandidate, and update related workflows

This commit removes nullable Uuid properties for consistency, transitions the Correction entity to QuizCandidate with associated migrations, refactors queries and repositories, adjusts related routes and controllers to use the new entity, updates front-end assets for elimination workflows, and standardizes route requirements and naming conventions.
This commit is contained in:
2025-06-06 23:03:13 +02:00
parent 3e724ff1fb
commit beb8d13dde
27 changed files with 385 additions and 282 deletions

View File

@@ -9,6 +9,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBase
abstract class AbstractController extends AbstractBaseController
{
protected const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
protected const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
#[\Override]
protected function addFlash(FlashType|string $type, mixed $message): void
{

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\Correction;
use App\Entity\QuizCandidate;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
class CorrectionCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Correction::class;
return QuizCandidate::class;
}
}

View File

@@ -6,10 +6,10 @@ namespace App\Controller\Admin;
use App\Entity\Answer;
use App\Entity\Candidate;
use App\Entity\Correction;
use App\Entity\GivenAnswer;
use App\Entity\Question;
use App\Entity\Quiz;
use App\Entity\QuizCandidate;
use App\Entity\Season;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
@@ -58,7 +58,7 @@ class DashboardController extends AbstractDashboardController
yield MenuItem::linkToCrud('Quiz', 'fas fa-list', Quiz::class);
yield MenuItem::linkToCrud('Question', 'fas fa-list', Question::class);
yield MenuItem::linkToCrud('Candidate', 'fas fa-list', Candidate::class);
yield MenuItem::linkToCrud('Correction', 'fas fa-list', Correction::class);
yield MenuItem::linkToCrud('Correction', 'fas fa-list', QuizCandidate::class);
yield MenuItem::linkToCrud('User', 'fas fa-list', User::class);
yield MenuItem::linkToCrud('Given Answer', 'fas fa-list', GivenAnswer::class);
yield MenuItem::linkToCrud('Answer', 'fas fa-list', Answer::class);

View File

@@ -4,19 +4,23 @@ declare(strict_types=1);
namespace App\Controller\Backoffice;
use App\Controller\AbstractController;
use App\Entity\Elimination;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Factory\EliminationFactory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class PrepareEliminationController extends AbstractController
{
#[Route('/backoffice/elimination/{seasonCode}/{quiz}/prepare', name: 'app_prepare_elimination')]
#[Route(
'/backoffice/elimination/{seasonCode}/{quiz}/prepare',
name: 'app_prepare_elimination',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
)]
public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
{
$elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
@@ -24,7 +28,11 @@ final class PrepareEliminationController extends AbstractController
return $this->redirectToRoute('app_prepare_elimination_view', ['elimination' => $elimination->getId()]);
}
#[Route('/backoffice/elimination/{elimination}', name: 'app_prepare_elimination_view')]
#[Route(
'/backoffice/elimination/{elimination}',
name: 'app_prepare_elimination_view',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
)]
public function viewElimination(Elimination $elimination, Request $request, EntityManagerInterface $em): Response
{
if ('POST' === $request->getMethod()) {

View File

@@ -23,7 +23,9 @@ class QuizController extends AbstractController
private readonly CandidateRepository $candidateRepository,
) {}
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz')]
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season, Quiz $quiz): Response
{
@@ -34,7 +36,9 @@ class QuizController extends AbstractController
]);
}
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable')]
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
{

View File

@@ -29,7 +29,11 @@ class SeasonController extends AbstractController
public function __construct(private readonly TranslatorInterface $translator, private EntityManagerInterface $em,
) {}
#[Route('/backoffice/season/{seasonCode}', name: 'app_backoffice_season')]
#[Route(
'/backoffice/season/{seasonCode}',
name: 'app_backoffice_season',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season): Response
{
@@ -38,7 +42,12 @@ class SeasonController extends AbstractController
]);
}
#[Route('/backoffice/season/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
#[Route(
'/backoffice/season/{seasonCode}/add_candidate',
name: 'app_backoffice_add_candidates',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
priority: 10,
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addCandidates(Season $season, Request $request): Response
{
@@ -59,7 +68,12 @@ class SeasonController extends AbstractController
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
}
#[Route('/backoffice/season/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
#[Route(
'/backoffice/season/{seasonCode}/add',
name: 'app_backoffice_quiz_add',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
priority: 10,
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
{

View File

@@ -8,6 +8,7 @@ use App\Entity\Answer;
use App\Entity\Candidate;
use App\Entity\GivenAnswer;
use App\Entity\Question;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Enum\FlashType;
use App\Form\EnterNameType;
@@ -17,6 +18,7 @@ use App\Repository\AnswerRepository;
use App\Repository\CandidateRepository;
use App\Repository\GivenAnswerRepository;
use App\Repository\QuestionRepository;
use App\Repository\QuizCandidateRepository;
use App\Repository\SeasonRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
@@ -29,13 +31,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController]
final class QuizController extends AbstractController
{
public const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
private const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
public function __construct(private readonly TranslatorInterface $translator) {}
#[Route(path: '/', name: 'app_quiz_selectseason', methods: ['GET', 'POST'])]
#[Route(path: '/', name: 'app_quiz_select_season', methods: ['GET', 'POST'])]
public function selectSeason(Request $request, SeasonRepository $seasonRepository): Response
{
$form = $this->createForm(SelectSeasonType::class);
@@ -47,16 +45,16 @@ final class QuizController extends AbstractController
if ([] === $seasonRepository->findBy(['seasonCode' => $seasonCode])) {
$this->addFlash(FlashType::Warning, $this->translator->trans('Invalid season code'));
return $this->redirectToRoute('app_quiz_selectseason');
return $this->redirectToRoute('app_quiz_select_season');
}
return $this->redirectToRoute('app_quiz_entername', ['seasonCode' => $seasonCode]);
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $seasonCode]);
}
return $this->render('quiz/select_season.html.twig', ['form' => $form]);
}
#[Route(path: '/{seasonCode}', name: 'app_quiz_entername', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
#[Route(path: '/{seasonCode}', name: 'app_quiz_enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
public function enterName(
Request $request,
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
@@ -69,7 +67,7 @@ final class QuizController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
$name = $form->get('name')->getData();
return $this->redirectToRoute('app_quiz_quizpage', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64UrlEncode($name)]);
return $this->redirectToRoute('app_quiz_quiz_page', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64UrlEncode($name)]);
}
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
@@ -77,25 +75,34 @@ final class QuizController extends AbstractController
#[Route(
path: '/{seasonCode}/{nameHash}',
name: 'app_quiz_quizpage',
name: 'app_quiz_quiz_page',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
)]
public function quizPage(
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
Season $season,
string $nameHash,
Request $request,
CandidateRepository $candidateRepository,
QuestionRepository $questionRepository,
AnswerRepository $answerRepository,
GivenAnswerRepository $givenAnswerRepository,
Request $request,
QuizCandidateRepository $quizCandidateRepository,
): Response {
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
if (!$candidate instanceof Candidate) {
$this->addFlash(FlashType::Danger, $this->translator->trans('Candidate not found'));
return $this->redirectToRoute('app_quiz_entername', ['seasonCode' => $season->getSeasonCode()]);
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $season->getSeasonCode()]);
}
$quiz = $season->getActiveQuiz();
if (!$quiz instanceof Quiz) {
$this->addFlash(FlashType::Warning, $this->translator->trans('There is no active quiz'));
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $season->getSeasonCode()]);
}
if ('POST' === $request->getMethod()) {
@@ -105,13 +112,10 @@ final class QuizController extends AbstractController
throw new BadRequestException('Invalid Answer ID');
}
$givenAnswer = (new GivenAnswer())
->setCandidate($candidate)
->setAnswer($answer)
->setQuiz($answer->getQuestion()->getQuiz());
$givenAnswer = new GivenAnswer($candidate, $answer->getQuestion()->getQuiz(), $answer);
$givenAnswerRepository->save($givenAnswer);
return $this->redirectToRoute('app_quiz_quizpage', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => $nameHash]);
return $this->redirectToRoute('app_quiz_quiz_page', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => $nameHash]);
}
$question = $questionRepository->findNextQuestionForCandidate($candidate);
@@ -119,10 +123,11 @@ final class QuizController extends AbstractController
if (!$question instanceof Question) {
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz completed'));
return $this->redirectToRoute('app_quiz_entername', ['seasonCode' => $season->getSeasonCode()]);
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $season->getSeasonCode()]);
}
// TODO One first question record time
$quizCandidateRepository->createIfNotExist($quiz, $candidate);
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]);
}
}

View File

@@ -116,16 +116,6 @@ class Answer
return $this->givenAnswers;
}
public function addGivenAnswer(GivenAnswer $givenAnswer): static
{
if (!$this->givenAnswers->contains($givenAnswer)) {
$this->givenAnswers->add($givenAnswer);
$givenAnswer->setAnswer($this);
}
return $this;
}
public function getOrdering(): int
{
return $this->ordering;

View File

@@ -21,7 +21,7 @@ class Candidate
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
private Uuid $id;
#[ORM\ManyToOne(inversedBy: 'candidates')]
#[ORM\JoinColumn(nullable: false)]
@@ -35,9 +35,9 @@ class Candidate
#[ORM\OneToMany(targetEntity: GivenAnswer::class, mappedBy: 'candidate', orphanRemoval: true)]
private Collection $givenAnswers;
/** @var Collection<int, Correction> */
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'candidate', orphanRemoval: true)]
private Collection $corrections;
/** @var Collection<int, QuizCandidate> */
#[ORM\OneToMany(targetEntity: QuizCandidate::class, mappedBy: 'candidate', orphanRemoval: true)]
private Collection $quizData;
public function __construct(
#[ORM\Column(length: 16)]
@@ -45,10 +45,10 @@ class Candidate
) {
$this->answersOnCandidate = new ArrayCollection();
$this->givenAnswers = new ArrayCollection();
$this->corrections = new ArrayCollection();
$this->quizData = new ArrayCollection();
}
public function getId(): ?Uuid
public function getId(): Uuid
{
return $this->id;
}
@@ -108,30 +108,10 @@ class Candidate
return $this->givenAnswers;
}
public function addGivenAnswer(GivenAnswer $givenAnswer): static
/** @return Collection<int, QuizCandidate> */
public function getQuizData(): Collection
{
if (!$this->givenAnswers->contains($givenAnswer)) {
$this->givenAnswers->add($givenAnswer);
$givenAnswer->setCandidate($this);
}
return $this;
}
/** @return Collection<int, Correction> */
public function getCorrections(): Collection
{
return $this->corrections;
}
public function addCorrection(Correction $correction): static
{
if (!$this->corrections->contains($correction)) {
$this->corrections->add($correction);
$correction->setCandidate($this);
}
return $this;
return $this->quizData;
}
public function getNameHash(): string

View File

@@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\CorrectionRepository;
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: CorrectionRepository::class)]
#[ORM\UniqueConstraint(columns: ['candidate_id', 'quiz_id'])]
class Correction
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\ManyToOne(inversedBy: 'corrections')]
#[ORM\JoinColumn(nullable: false)]
private Candidate $candidate;
#[ORM\ManyToOne(inversedBy: 'corrections')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz;
#[ORM\Column]
private float $amount = 0;
public function getId(): ?Uuid
{
return $this->id;
}
public function getCandidate(): Candidate
{
return $this->candidate;
}
public function setCandidate(Candidate $candidate): static
{
$this->candidate = $candidate;
return $this;
}
public function getQuiz(): Quiz
{
return $this->quiz;
}
public function setQuiz(Quiz $quiz): static
{
$this->quiz = $quiz;
return $this;
}
public function getAmount(): ?float
{
return $this->amount;
}
public function setAmount(float $amount): static
{
$this->amount = $amount;
return $this;
}
}

View File

@@ -30,7 +30,7 @@ class Elimination
#[ORM\Column(type: Types::JSON)]
private array $data = [];
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $created;
public function __construct(

View File

@@ -22,22 +22,24 @@ class GivenAnswer
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id;
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: false)]
private Candidate $candidate;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz;
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: true)]
private ?Answer $answer = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $created;
public function getId(): ?Uuid
public function __construct(
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: false)]
private Candidate $candidate,
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz,
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: false)]
private Answer $answer,
) {}
public function getId(): Uuid
{
return $this->id;
}
@@ -47,37 +49,16 @@ class GivenAnswer
return $this->candidate;
}
public function setCandidate(Candidate $candidate): static
{
$this->candidate = $candidate;
return $this;
}
public function getQuiz(): ?Quiz
public function getQuiz(): Quiz
{
return $this->quiz;
}
public function setQuiz(Quiz $quiz): static
{
$this->quiz = $quiz;
return $this;
}
public function getAnswer(): ?Answer
public function getAnswer(): Answer
{
return $this->answer;
}
public function setAnswer(?Answer $answer): static
{
$this->answer = $answer;
return $this;
}
public function getCreated(): \DateTimeImmutable
{
return $this->created;

View File

@@ -20,7 +20,7 @@ class Question
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
private Uuid $id;
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
private int $ordering;
@@ -45,7 +45,7 @@ class Question
$this->answers = new ArrayCollection();
}
public function getId(): ?Uuid
public function getId(): Uuid
{
return $this->id;
}

View File

@@ -20,7 +20,7 @@ class Quiz
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
private Uuid $id;
#[ORM\Column(length: 64)]
private string $name;
@@ -34,9 +34,9 @@ class Quiz
#[ORM\OrderBy(['ordering' => 'ASC'])]
private Collection $questions;
/** @var Collection<int, Correction> */
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
private Collection $corrections;
/** @var Collection<int, QuizCandidate> */
#[ORM\OneToMany(targetEntity: QuizCandidate::class, mappedBy: 'quiz', orphanRemoval: true)]
private Collection $candidateData;
#[ORM\Column(nullable: false, options: ['default' => 1])]
private int $dropouts = 1;
@@ -49,11 +49,11 @@ class Quiz
public function __construct()
{
$this->questions = new ArrayCollection();
$this->corrections = new ArrayCollection();
$this->candidateData = new ArrayCollection();
$this->eliminations = new ArrayCollection();
}
public function getId(): ?Uuid
public function getId(): Uuid
{
return $this->id;
}
@@ -98,20 +98,10 @@ class Quiz
return $this;
}
/** @return Collection<int, Correction> */
public function getCorrections(): Collection
/** @return Collection<int, QuizCandidate> */
public function getCandidateData(): Collection
{
return $this->corrections;
}
public function addCorrection(Correction $correction): static
{
if (!$this->corrections->contains($correction)) {
$this->corrections->add($correction);
$correction->setQuiz($this);
}
return $this;
return $this->candidateData;
}
public function getDropouts(): int

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\QuizCandidateRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Safe\DateTimeImmutable;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: QuizCandidateRepository::class)]
#[ORM\UniqueConstraint(columns: ['candidate_id', 'quiz_id'])]
#[ORM\HasLifecycleCallbacks]
class QuizCandidate
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id;
#[ORM\Column]
private float $corrections = 0;
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)]
private \DateTimeImmutable $created;
public function __construct(
#[ORM\ManyToOne(inversedBy: 'candidateData')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz,
#[ORM\ManyToOne(inversedBy: 'quizData')]
#[ORM\JoinColumn(nullable: false)]
private Candidate $candidate,
) {}
public function getId(): Uuid
{
return $this->id;
}
public function getCandidate(): Candidate
{
return $this->candidate;
}
public function getQuiz(): Quiz
{
return $this->quiz;
}
public function getCorrections(): ?float
{
return $this->corrections;
}
public function setCorrections(float $corrections): static
{
$this->corrections = $corrections;
return $this;
}
public function getCreated(): \DateTimeImmutable
{
return $this->created;
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$this->created = new DateTimeImmutable();
}
}

View File

@@ -21,7 +21,7 @@ class Season
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
private Uuid $id;
#[ORM\Column(length: 64)]
private string $name;
@@ -52,7 +52,7 @@ class Season
$this->owners = new ArrayCollection();
}
public function getId(): ?Uuid
public function getId(): Uuid
{
return $this->id;
}

View File

@@ -26,7 +26,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
private Uuid $id;
#[ORM\Column(length: 180)]
private string $email;
@@ -51,7 +51,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
$this->seasons = new ArrayCollection();
}
public function getId(): ?Uuid
public function getId(): Uuid
{
return $this->id;
}

View File

@@ -5,12 +5,10 @@ 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 Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry;
use Safe\Exceptions\UrlException;
use Symfony\Component\Uid\Uuid;
@@ -56,20 +54,26 @@ class CandidateRepository extends ServiceEntityRepository
/** @return ResultList */
public function getScores(Quiz $quiz): array
{
$scoreTimeQb = $this->createQueryBuilder('c', 'c.id')
->select('c.id', 'c.name', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'max(ga.created) - min(ga.created) as time')
$scoreQb = $this->createQueryBuilder('c', 'c.id')
->select('c.id', 'c.name', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct')
->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.id', 'cor.amount as corrections')
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
$startTimeCorrectionQb = $this->createQueryBuilder('c', 'c.id')
->select('c.id', 'qc.corrections', 'max(ga.created) - qc.created as time')
->join('c.quizData', 'qc')
->join('c.givenAnswers', 'ga')
->where('qc.quiz = :quiz')
->groupBy('ga.quiz', 'c.id', 'qc.id')
->setParameter('quiz', $quiz);
$merged = array_merge_recursive($scoreTimeQb->getQuery()->getArrayResult(), $correctionsQb->getQuery()->getArrayResult());
$merged = array_merge_recursive(
$scoreQb->getQuery()->getArrayResult(),
$startTimeCorrectionQb->getQuery()->getArrayResult(),
);
return $this->sortResults($this->calculateScore($merged));
}

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Correction;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Correction>
*/
class CorrectionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Correction::class);
}
}

View File

@@ -17,29 +17,4 @@ class EliminationRepository extends ServiceEntityRepository
{
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

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Candidate;
use App\Entity\Quiz;
use App\Entity\QuizCandidate;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<QuizCandidate>
*/
class QuizCandidateRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, QuizCandidate::class);
}
/** @return bool true if a new entry was created */
public function createIfNotExist(Quiz $quiz, Candidate $candidate): bool
{
if (0 !== $this->count(['candidate' => $candidate, 'quiz' => $quiz])) {
return false;
}
$quizCandidate = new QuizCandidate($quiz, $candidate);
$this->getEntityManager()->persist($quizCandidate);
$this->getEntityManager()->flush();
return true;
}
}