diff --git a/assets/controllers/elimination_controller.js b/assets/controllers/elimination_controller.js new file mode 100644 index 0000000..6550bee --- /dev/null +++ b/assets/controllers/elimination_controller.js @@ -0,0 +1,10 @@ +import {Controller} from '@hotwired/stimulus'; + +export default class extends Controller { + next() { + const currentUrl = window.location.href; + const urlParts = currentUrl.split('/'); + urlParts.pop(); + window.location.href = urlParts.join('/'); + } +} diff --git a/assets/quiz.js b/assets/quiz.js index f7c2c54..b9160a7 100644 --- a/assets/quiz.js +++ b/assets/quiz.js @@ -4,22 +4,3 @@ import * as bootstrap from 'bootstrap' import './styles/app.scss' -document.addEventListener('DOMContentLoaded', function () { - // Check if we're on the elimination candidate screen - const eliminationScreen = document.querySelector('.elimination-screen'); - if (eliminationScreen) { - // Add event listener for any keypress - document.addEventListener('keydown', function (event) { - // Get the current URL - const currentUrl = window.location.href; - // Extract the elimination ID from the URL - const urlParts = currentUrl.split('/'); - // Remove the candidate hash (last part of the URL) - urlParts.pop(); - // Construct the URL to the main elimination page - const redirectUrl = urlParts.join('/'); - // Redirect to the main elimination page - window.location.href = redirectUrl; - }); - } -}); diff --git a/migrations/Version20250606192337.php b/migrations/Version20250606192337.php new file mode 100644 index 0000000..0ce244b --- /dev/null +++ b/migrations/Version20250606192337.php @@ -0,0 +1,90 @@ +addSql(<<<'SQL' + CREATE TABLE quiz_candidate (id UUID NOT NULL, corrections DOUBLE PRECISION NOT NULL, created TIMESTAMP(0) WITH TIME ZONE NOT NULL, quiz_id UUID NOT NULL, candidate_id UUID NOT NULL, PRIMARY KEY(id)) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_CED2FFA2853CD175 ON quiz_candidate (quiz_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_CED2FFA291BD8781 ON quiz_candidate (candidate_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_CED2FFA291BD8781853CD175 ON quiz_candidate (candidate_id, quiz_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE quiz_candidate ADD CONSTRAINT FK_CED2FFA2853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE quiz_candidate ADD CONSTRAINT FK_CED2FFA291BD8781 FOREIGN KEY (candidate_id) REFERENCES candidate (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE correction DROP CONSTRAINT fk_a29da1b891bd8781 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE correction DROP CONSTRAINT fk_a29da1b8853cd175 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE correction + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE elimination ALTER created TYPE TIMESTAMP(0) WITH TIME ZONE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE given_answer ALTER created TYPE TIMESTAMP(0) WITH TIME ZONE + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE correction (id UUID NOT NULL, candidate_id UUID NOT NULL, quiz_id UUID NOT NULL, amount DOUBLE PRECISION NOT NULL, PRIMARY KEY(id)) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX uniq_a29da1b891bd8781853cd175 ON correction (candidate_id, quiz_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX idx_a29da1b8853cd175 ON correction (quiz_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX idx_a29da1b891bd8781 ON correction (candidate_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE correction ADD CONSTRAINT fk_a29da1b891bd8781 FOREIGN KEY (candidate_id) REFERENCES candidate (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE correction ADD CONSTRAINT fk_a29da1b8853cd175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE quiz_candidate DROP CONSTRAINT FK_CED2FFA2853CD175 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE quiz_candidate DROP CONSTRAINT FK_CED2FFA291BD8781 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE quiz_candidate + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE given_answer ALTER created TYPE TIMESTAMP(0) WITHOUT TIME ZONE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE elimination ALTER created TYPE TIMESTAMP(0) WITHOUT TIME ZONE + SQL); + } +} diff --git a/migrations/Version20250606195952.php b/migrations/Version20250606195952.php new file mode 100644 index 0000000..f0aa594 --- /dev/null +++ b/migrations/Version20250606195952.php @@ -0,0 +1,42 @@ +addSql(<<<'SQL' + delete from given_answer where answer_id is null + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE given_answer ALTER answer_id TYPE UUID + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE given_answer ALTER answer_id SET NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE given_answer ALTER answer_id TYPE UUID + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE given_answer ALTER answer_id DROP NOT NULL + SQL); + } +} diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php index 347eccf..4663bef 100644 --- a/src/Controller/AbstractController.php +++ b/src/Controller/AbstractController.php @@ -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 { diff --git a/src/Controller/Admin/CorrectionCrudController.php b/src/Controller/Admin/CorrectionCrudController.php index 89b649f..6cc2028 100644 --- a/src/Controller/Admin/CorrectionCrudController.php +++ b/src/Controller/Admin/CorrectionCrudController.php @@ -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; } } diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index 0933738..97bb080 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -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); diff --git a/src/Controller/Backoffice/PrepareEliminationController.php b/src/Controller/Backoffice/PrepareEliminationController.php index 96ee405..0ed5fe3 100644 --- a/src/Controller/Backoffice/PrepareEliminationController.php +++ b/src/Controller/Backoffice/PrepareEliminationController.php @@ -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()) { diff --git a/src/Controller/Backoffice/QuizController.php b/src/Controller/Backoffice/QuizController.php index 047ac6c..a08ce25 100644 --- a/src/Controller/Backoffice/QuizController.php +++ b/src/Controller/Backoffice/QuizController.php @@ -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 { diff --git a/src/Controller/Backoffice/SeasonController.php b/src/Controller/Backoffice/SeasonController.php index 055f811..c280225 100644 --- a/src/Controller/Backoffice/SeasonController.php +++ b/src/Controller/Backoffice/SeasonController.php @@ -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 { diff --git a/src/Controller/QuizController.php b/src/Controller/QuizController.php index 12dd24f..cd7460b 100644 --- a/src/Controller/QuizController.php +++ b/src/Controller/QuizController.php @@ -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]); } } diff --git a/src/Entity/Answer.php b/src/Entity/Answer.php index ffb217b..2d96e15 100644 --- a/src/Entity/Answer.php +++ b/src/Entity/Answer.php @@ -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; diff --git a/src/Entity/Candidate.php b/src/Entity/Candidate.php index ee6254f..ddd35fb 100644 --- a/src/Entity/Candidate.php +++ b/src/Entity/Candidate.php @@ -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 */ - #[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'candidate', orphanRemoval: true)] - private Collection $corrections; + /** @var Collection */ + #[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 */ + public function getQuizData(): Collection { - if (!$this->givenAnswers->contains($givenAnswer)) { - $this->givenAnswers->add($givenAnswer); - $givenAnswer->setCandidate($this); - } - - return $this; - } - - /** @return Collection */ - 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 diff --git a/src/Entity/Correction.php b/src/Entity/Correction.php deleted file mode 100644 index 5e9cd26..0000000 --- a/src/Entity/Correction.php +++ /dev/null @@ -1,74 +0,0 @@ -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; - } -} diff --git a/src/Entity/Elimination.php b/src/Entity/Elimination.php index beaae93..6efeab8 100644 --- a/src/Entity/Elimination.php +++ b/src/Entity/Elimination.php @@ -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( diff --git a/src/Entity/GivenAnswer.php b/src/Entity/GivenAnswer.php index 851d311..6d83857 100644 --- a/src/Entity/GivenAnswer.php +++ b/src/Entity/GivenAnswer.php @@ -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; diff --git a/src/Entity/Question.php b/src/Entity/Question.php index 2c618f4..faeb83f 100644 --- a/src/Entity/Question.php +++ b/src/Entity/Question.php @@ -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; } diff --git a/src/Entity/Quiz.php b/src/Entity/Quiz.php index 1f3981f..958876d 100644 --- a/src/Entity/Quiz.php +++ b/src/Entity/Quiz.php @@ -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 */ - #[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)] - private Collection $corrections; + /** @var Collection */ + #[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 */ - public function getCorrections(): Collection + /** @return Collection */ + 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 diff --git a/src/Entity/QuizCandidate.php b/src/Entity/QuizCandidate.php new file mode 100644 index 0000000..ff1c41a --- /dev/null +++ b/src/Entity/QuizCandidate.php @@ -0,0 +1,79 @@ +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(); + } +} diff --git a/src/Entity/Season.php b/src/Entity/Season.php index 97d65b9..42ad344 100644 --- a/src/Entity/Season.php +++ b/src/Entity/Season.php @@ -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; } diff --git a/src/Entity/User.php b/src/Entity/User.php index 5ca9d4b..2c20f09 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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; } diff --git a/src/Repository/CandidateRepository.php b/src/Repository/CandidateRepository.php index ce61c02..3cdbaa4 100644 --- a/src/Repository/CandidateRepository.php +++ b/src/Repository/CandidateRepository.php @@ -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)); } diff --git a/src/Repository/CorrectionRepository.php b/src/Repository/CorrectionRepository.php deleted file mode 100644 index c9ae6e5..0000000 --- a/src/Repository/CorrectionRepository.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ -class CorrectionRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, Correction::class); - } -} diff --git a/src/Repository/EliminationRepository.php b/src/Repository/EliminationRepository.php index 60782c2..8e28752 100644 --- a/src/Repository/EliminationRepository.php +++ b/src/Repository/EliminationRepository.php @@ -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() - // ; - // } } diff --git a/src/Repository/QuizCandidateRepository.php b/src/Repository/QuizCandidateRepository.php new file mode 100644 index 0000000..42e217b --- /dev/null +++ b/src/Repository/QuizCandidateRepository.php @@ -0,0 +1,36 @@ + + */ +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; + } +} diff --git a/templates/backoffice/index.html.twig b/templates/backoffice/index.html.twig index fa1c9be..2857cd0 100644 --- a/templates/backoffice/index.html.twig +++ b/templates/backoffice/index.html.twig @@ -38,7 +38,7 @@ {% endif %} - {{ season.seasonCode }} diff --git a/templates/quiz/elimination/candidate.html.twig b/templates/quiz/elimination/candidate.html.twig index 48453f0..bb09028 100644 --- a/templates/quiz/elimination/candidate.html.twig +++ b/templates/quiz/elimination/candidate.html.twig @@ -1,7 +1,12 @@ {% extends 'quiz/base.html.twig' %} {% block body %} - Screen with colour {{ colour }} + Screen with colour {{ colour }} {% endblock %}