mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 12:44:20 +01:00
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:
10
assets/controllers/elimination_controller.js
Normal file
10
assets/controllers/elimination_controller.js
Normal file
@@ -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('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,22 +4,3 @@ import * as bootstrap from 'bootstrap'
|
|||||||
|
|
||||||
import './styles/app.scss'
|
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
90
migrations/Version20250606192337.php
Normal file
90
migrations/Version20250606192337.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250606192337 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Ze Big migration';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
migrations/Version20250606195952.php
Normal file
42
migrations/Version20250606195952.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250606195952 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBase
|
|||||||
|
|
||||||
abstract class AbstractController extends AbstractBaseController
|
abstract class AbstractController extends AbstractBaseController
|
||||||
{
|
{
|
||||||
|
protected const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
|
||||||
|
protected const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
|
||||||
|
|
||||||
#[\Override]
|
#[\Override]
|
||||||
protected function addFlash(FlashType|string $type, mixed $message): void
|
protected function addFlash(FlashType|string $type, mixed $message): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
use App\Entity\Correction;
|
use App\Entity\QuizCandidate;
|
||||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||||
|
|
||||||
class CorrectionCrudController extends AbstractCrudController
|
class CorrectionCrudController extends AbstractCrudController
|
||||||
{
|
{
|
||||||
public static function getEntityFqcn(): string
|
public static function getEntityFqcn(): string
|
||||||
{
|
{
|
||||||
return Correction::class;
|
return QuizCandidate::class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ namespace App\Controller\Admin;
|
|||||||
|
|
||||||
use App\Entity\Answer;
|
use App\Entity\Answer;
|
||||||
use App\Entity\Candidate;
|
use App\Entity\Candidate;
|
||||||
use App\Entity\Correction;
|
|
||||||
use App\Entity\GivenAnswer;
|
use App\Entity\GivenAnswer;
|
||||||
use App\Entity\Question;
|
use App\Entity\Question;
|
||||||
use App\Entity\Quiz;
|
use App\Entity\Quiz;
|
||||||
|
use App\Entity\QuizCandidate;
|
||||||
use App\Entity\Season;
|
use App\Entity\Season;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
|
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('Quiz', 'fas fa-list', Quiz::class);
|
||||||
yield MenuItem::linkToCrud('Question', 'fas fa-list', Question::class);
|
yield MenuItem::linkToCrud('Question', 'fas fa-list', Question::class);
|
||||||
yield MenuItem::linkToCrud('Candidate', 'fas fa-list', Candidate::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('User', 'fas fa-list', User::class);
|
||||||
yield MenuItem::linkToCrud('Given Answer', 'fas fa-list', GivenAnswer::class);
|
yield MenuItem::linkToCrud('Given Answer', 'fas fa-list', GivenAnswer::class);
|
||||||
yield MenuItem::linkToCrud('Answer', 'fas fa-list', Answer::class);
|
yield MenuItem::linkToCrud('Answer', 'fas fa-list', Answer::class);
|
||||||
|
|||||||
@@ -4,19 +4,23 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Backoffice;
|
namespace App\Controller\Backoffice;
|
||||||
|
|
||||||
|
use App\Controller\AbstractController;
|
||||||
use App\Entity\Elimination;
|
use App\Entity\Elimination;
|
||||||
use App\Entity\Quiz;
|
use App\Entity\Quiz;
|
||||||
use App\Entity\Season;
|
use App\Entity\Season;
|
||||||
use App\Factory\EliminationFactory;
|
use App\Factory\EliminationFactory;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
final class PrepareEliminationController extends AbstractController
|
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
|
public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
|
||||||
{
|
{
|
||||||
$elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
|
$elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
|
||||||
@@ -24,7 +28,11 @@ final class PrepareEliminationController extends AbstractController
|
|||||||
return $this->redirectToRoute('app_prepare_elimination_view', ['elimination' => $elimination->getId()]);
|
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
|
public function viewElimination(Elimination $elimination, Request $request, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
if ('POST' === $request->getMethod()) {
|
if ('POST' === $request->getMethod()) {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ class QuizController extends AbstractController
|
|||||||
private readonly CandidateRepository $candidateRepository,
|
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')]
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
public function index(Season $season, Quiz $quiz): Response
|
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')]
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
|
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ class SeasonController extends AbstractController
|
|||||||
public function __construct(private readonly TranslatorInterface $translator, private EntityManagerInterface $em,
|
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')]
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
public function index(Season $season): Response
|
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')]
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
public function addCandidates(Season $season, Request $request): Response
|
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]);
|
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')]
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
|
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Entity\Answer;
|
|||||||
use App\Entity\Candidate;
|
use App\Entity\Candidate;
|
||||||
use App\Entity\GivenAnswer;
|
use App\Entity\GivenAnswer;
|
||||||
use App\Entity\Question;
|
use App\Entity\Question;
|
||||||
|
use App\Entity\Quiz;
|
||||||
use App\Entity\Season;
|
use App\Entity\Season;
|
||||||
use App\Enum\FlashType;
|
use App\Enum\FlashType;
|
||||||
use App\Form\EnterNameType;
|
use App\Form\EnterNameType;
|
||||||
@@ -17,6 +18,7 @@ use App\Repository\AnswerRepository;
|
|||||||
use App\Repository\CandidateRepository;
|
use App\Repository\CandidateRepository;
|
||||||
use App\Repository\GivenAnswerRepository;
|
use App\Repository\GivenAnswerRepository;
|
||||||
use App\Repository\QuestionRepository;
|
use App\Repository\QuestionRepository;
|
||||||
|
use App\Repository\QuizCandidateRepository;
|
||||||
use App\Repository\SeasonRepository;
|
use App\Repository\SeasonRepository;
|
||||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||||
@@ -29,13 +31,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
#[AsController]
|
#[AsController]
|
||||||
final class QuizController extends AbstractController
|
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) {}
|
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
|
public function selectSeason(Request $request, SeasonRepository $seasonRepository): Response
|
||||||
{
|
{
|
||||||
$form = $this->createForm(SelectSeasonType::class);
|
$form = $this->createForm(SelectSeasonType::class);
|
||||||
@@ -47,16 +45,16 @@ final class QuizController extends AbstractController
|
|||||||
if ([] === $seasonRepository->findBy(['seasonCode' => $seasonCode])) {
|
if ([] === $seasonRepository->findBy(['seasonCode' => $seasonCode])) {
|
||||||
$this->addFlash(FlashType::Warning, $this->translator->trans('Invalid season code'));
|
$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]);
|
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(
|
public function enterName(
|
||||||
Request $request,
|
Request $request,
|
||||||
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
|
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
|
||||||
@@ -69,7 +67,7 @@ final class QuizController extends AbstractController
|
|||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$name = $form->get('name')->getData();
|
$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]);
|
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
|
||||||
@@ -77,25 +75,34 @@ final class QuizController extends AbstractController
|
|||||||
|
|
||||||
#[Route(
|
#[Route(
|
||||||
path: '/{seasonCode}/{nameHash}',
|
path: '/{seasonCode}/{nameHash}',
|
||||||
name: 'app_quiz_quizpage',
|
name: 'app_quiz_quiz_page',
|
||||||
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'])]
|
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
|
||||||
Season $season,
|
Season $season,
|
||||||
string $nameHash,
|
string $nameHash,
|
||||||
|
Request $request,
|
||||||
CandidateRepository $candidateRepository,
|
CandidateRepository $candidateRepository,
|
||||||
QuestionRepository $questionRepository,
|
QuestionRepository $questionRepository,
|
||||||
AnswerRepository $answerRepository,
|
AnswerRepository $answerRepository,
|
||||||
GivenAnswerRepository $givenAnswerRepository,
|
GivenAnswerRepository $givenAnswerRepository,
|
||||||
Request $request,
|
QuizCandidateRepository $quizCandidateRepository,
|
||||||
): Response {
|
): Response {
|
||||||
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
|
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
|
||||||
|
|
||||||
if (!$candidate instanceof Candidate) {
|
if (!$candidate instanceof Candidate) {
|
||||||
$this->addFlash(FlashType::Danger, $this->translator->trans('Candidate not found'));
|
$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()) {
|
if ('POST' === $request->getMethod()) {
|
||||||
@@ -105,13 +112,10 @@ final class QuizController extends AbstractController
|
|||||||
throw new BadRequestException('Invalid Answer ID');
|
throw new BadRequestException('Invalid Answer ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
$givenAnswer = (new GivenAnswer())
|
$givenAnswer = new GivenAnswer($candidate, $answer->getQuestion()->getQuiz(), $answer);
|
||||||
->setCandidate($candidate)
|
|
||||||
->setAnswer($answer)
|
|
||||||
->setQuiz($answer->getQuestion()->getQuiz());
|
|
||||||
$givenAnswerRepository->save($givenAnswer);
|
$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);
|
$question = $questionRepository->findNextQuestionForCandidate($candidate);
|
||||||
@@ -119,10 +123,11 @@ final class QuizController extends AbstractController
|
|||||||
if (!$question instanceof Question) {
|
if (!$question instanceof Question) {
|
||||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz completed'));
|
$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]);
|
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,16 +116,6 @@ class Answer
|
|||||||
return $this->givenAnswers;
|
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
|
public function getOrdering(): int
|
||||||
{
|
{
|
||||||
return $this->ordering;
|
return $this->ordering;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Candidate
|
|||||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||||
private ?Uuid $id = null;
|
private Uuid $id;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'candidates')]
|
#[ORM\ManyToOne(inversedBy: 'candidates')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
@@ -35,9 +35,9 @@ class Candidate
|
|||||||
#[ORM\OneToMany(targetEntity: GivenAnswer::class, mappedBy: 'candidate', orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: GivenAnswer::class, mappedBy: 'candidate', orphanRemoval: true)]
|
||||||
private Collection $givenAnswers;
|
private Collection $givenAnswers;
|
||||||
|
|
||||||
/** @var Collection<int, Correction> */
|
/** @var Collection<int, QuizCandidate> */
|
||||||
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'candidate', orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: QuizCandidate::class, mappedBy: 'candidate', orphanRemoval: true)]
|
||||||
private Collection $corrections;
|
private Collection $quizData;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[ORM\Column(length: 16)]
|
#[ORM\Column(length: 16)]
|
||||||
@@ -45,10 +45,10 @@ class Candidate
|
|||||||
) {
|
) {
|
||||||
$this->answersOnCandidate = new ArrayCollection();
|
$this->answersOnCandidate = new ArrayCollection();
|
||||||
$this->givenAnswers = 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;
|
return $this->id;
|
||||||
}
|
}
|
||||||
@@ -108,30 +108,10 @@ class Candidate
|
|||||||
return $this->givenAnswers;
|
return $this->givenAnswers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addGivenAnswer(GivenAnswer $givenAnswer): static
|
/** @return Collection<int, QuizCandidate> */
|
||||||
|
public function getQuizData(): Collection
|
||||||
{
|
{
|
||||||
if (!$this->givenAnswers->contains($givenAnswer)) {
|
return $this->quizData;
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNameHash(): string
|
public function getNameHash(): string
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ class Elimination
|
|||||||
#[ORM\Column(type: Types::JSON)]
|
#[ORM\Column(type: Types::JSON)]
|
||||||
private array $data = [];
|
private array $data = [];
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: false)]
|
||||||
private \DateTimeImmutable $created;
|
private \DateTimeImmutable $created;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -22,22 +22,24 @@ class GivenAnswer
|
|||||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||||
private Uuid $id;
|
private Uuid $id;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: false)]
|
||||||
|
private \DateTimeImmutable $created;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private Candidate $candidate;
|
private Candidate $candidate,
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private Quiz $quiz;
|
private Quiz $quiz,
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
||||||
#[ORM\JoinColumn(nullable: true)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private ?Answer $answer = null;
|
private Answer $answer,
|
||||||
|
) {}
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
public function getId(): Uuid
|
||||||
private \DateTimeImmutable $created;
|
|
||||||
|
|
||||||
public function getId(): ?Uuid
|
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
@@ -47,37 +49,16 @@ class GivenAnswer
|
|||||||
return $this->candidate;
|
return $this->candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCandidate(Candidate $candidate): static
|
public function getQuiz(): Quiz
|
||||||
{
|
|
||||||
$this->candidate = $candidate;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getQuiz(): ?Quiz
|
|
||||||
{
|
{
|
||||||
return $this->quiz;
|
return $this->quiz;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setQuiz(Quiz $quiz): static
|
public function getAnswer(): Answer
|
||||||
{
|
|
||||||
$this->quiz = $quiz;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAnswer(): ?Answer
|
|
||||||
{
|
{
|
||||||
return $this->answer;
|
return $this->answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAnswer(?Answer $answer): static
|
|
||||||
{
|
|
||||||
$this->answer = $answer;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCreated(): \DateTimeImmutable
|
public function getCreated(): \DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->created;
|
return $this->created;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Question
|
|||||||
#[ORM\Column(type: UuidType::NAME)]
|
#[ORM\Column(type: UuidType::NAME)]
|
||||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||||
private ?Uuid $id = null;
|
private Uuid $id;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
|
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
|
||||||
private int $ordering;
|
private int $ordering;
|
||||||
@@ -45,7 +45,7 @@ class Question
|
|||||||
$this->answers = new ArrayCollection();
|
$this->answers = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?Uuid
|
public function getId(): Uuid
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Quiz
|
|||||||
#[ORM\Column(type: UuidType::NAME)]
|
#[ORM\Column(type: UuidType::NAME)]
|
||||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||||
private ?Uuid $id = null;
|
private Uuid $id;
|
||||||
|
|
||||||
#[ORM\Column(length: 64)]
|
#[ORM\Column(length: 64)]
|
||||||
private string $name;
|
private string $name;
|
||||||
@@ -34,9 +34,9 @@ class Quiz
|
|||||||
#[ORM\OrderBy(['ordering' => 'ASC'])]
|
#[ORM\OrderBy(['ordering' => 'ASC'])]
|
||||||
private Collection $questions;
|
private Collection $questions;
|
||||||
|
|
||||||
/** @var Collection<int, Correction> */
|
/** @var Collection<int, QuizCandidate> */
|
||||||
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: QuizCandidate::class, mappedBy: 'quiz', orphanRemoval: true)]
|
||||||
private Collection $corrections;
|
private Collection $candidateData;
|
||||||
|
|
||||||
#[ORM\Column(nullable: false, options: ['default' => 1])]
|
#[ORM\Column(nullable: false, options: ['default' => 1])]
|
||||||
private int $dropouts = 1;
|
private int $dropouts = 1;
|
||||||
@@ -49,11 +49,11 @@ class Quiz
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->questions = new ArrayCollection();
|
$this->questions = new ArrayCollection();
|
||||||
$this->corrections = new ArrayCollection();
|
$this->candidateData = new ArrayCollection();
|
||||||
$this->eliminations = new ArrayCollection();
|
$this->eliminations = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?Uuid
|
public function getId(): Uuid
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
@@ -98,20 +98,10 @@ class Quiz
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Collection<int, Correction> */
|
/** @return Collection<int, QuizCandidate> */
|
||||||
public function getCorrections(): Collection
|
public function getCandidateData(): Collection
|
||||||
{
|
{
|
||||||
return $this->corrections;
|
return $this->candidateData;
|
||||||
}
|
|
||||||
|
|
||||||
public function addCorrection(Correction $correction): static
|
|
||||||
{
|
|
||||||
if (!$this->corrections->contains($correction)) {
|
|
||||||
$this->corrections->add($correction);
|
|
||||||
$correction->setQuiz($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDropouts(): int
|
public function getDropouts(): int
|
||||||
|
|||||||
79
src/Entity/QuizCandidate.php
Normal file
79
src/Entity/QuizCandidate.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ class Season
|
|||||||
#[ORM\Column(type: UuidType::NAME)]
|
#[ORM\Column(type: UuidType::NAME)]
|
||||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||||
private ?Uuid $id = null;
|
private Uuid $id;
|
||||||
|
|
||||||
#[ORM\Column(length: 64)]
|
#[ORM\Column(length: 64)]
|
||||||
private string $name;
|
private string $name;
|
||||||
@@ -52,7 +52,7 @@ class Season
|
|||||||
$this->owners = new ArrayCollection();
|
$this->owners = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?Uuid
|
public function getId(): Uuid
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||||
private ?Uuid $id = null;
|
private Uuid $id;
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
private string $email;
|
private string $email;
|
||||||
@@ -51,7 +51,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
$this->seasons = new ArrayCollection();
|
$this->seasons = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?Uuid
|
public function getId(): Uuid
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ declare(strict_types=1);
|
|||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
|
|
||||||
use App\Entity\Candidate;
|
use App\Entity\Candidate;
|
||||||
use App\Entity\Correction;
|
|
||||||
use App\Entity\Quiz;
|
use App\Entity\Quiz;
|
||||||
use App\Entity\Season;
|
use App\Entity\Season;
|
||||||
use App\Helpers\Base64;
|
use App\Helpers\Base64;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Safe\Exceptions\UrlException;
|
use Safe\Exceptions\UrlException;
|
||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
@@ -56,20 +54,26 @@ class CandidateRepository extends ServiceEntityRepository
|
|||||||
/** @return ResultList */
|
/** @return ResultList */
|
||||||
public function getScores(Quiz $quiz): array
|
public function getScores(Quiz $quiz): array
|
||||||
{
|
{
|
||||||
$scoreTimeQb = $this->createQueryBuilder('c', 'c.id')
|
$scoreQb = $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')
|
->select('c.id', 'c.name', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct')
|
||||||
->join('c.givenAnswers', 'ga')
|
->join('c.givenAnswers', 'ga')
|
||||||
->join('ga.answer', 'a')
|
->join('ga.answer', 'a')
|
||||||
->where('ga.quiz = :quiz')
|
->where('ga.quiz = :quiz')
|
||||||
->groupBy('c.id')
|
->groupBy('c.id')
|
||||||
->setParameter('quiz', $quiz);
|
->setParameter('quiz', $quiz);
|
||||||
|
|
||||||
$correctionsQb = $this->createQueryBuilder('c', 'c.id')
|
$startTimeCorrectionQb = $this->createQueryBuilder('c', 'c.id')
|
||||||
->select('c.id', 'cor.amount as corrections')
|
->select('c.id', 'qc.corrections', 'max(ga.created) - qc.created as time')
|
||||||
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
|
->join('c.quizData', 'qc')
|
||||||
|
->join('c.givenAnswers', 'ga')
|
||||||
|
->where('qc.quiz = :quiz')
|
||||||
|
->groupBy('ga.quiz', 'c.id', 'qc.id')
|
||||||
->setParameter('quiz', $quiz);
|
->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));
|
return $this->sortResults($this->calculateScore($merged));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,29 +17,4 @@ class EliminationRepository extends ServiceEntityRepository
|
|||||||
{
|
{
|
||||||
parent::__construct($registry, Elimination::class);
|
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()
|
|
||||||
// ;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/Repository/QuizCandidateRepository.php
Normal file
36
src/Repository/QuizCandidateRepository.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a {% if season.activeQuiz %}href="{{ path('app_quiz_entername', {seasonCode: season.seasonCode}) }}"
|
<a {% if season.activeQuiz %}href="{{ path('app_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
|
||||||
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
{% extends 'quiz/base.html.twig' %}
|
{% extends 'quiz/base.html.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<img src="{{ asset("img/#{colour}.png") }}" class="elimination-screen" id="{{ colour }}"
|
<img src="{{ asset("img/#{colour}.png") }}"
|
||||||
alt="Screen with colour {{ colour }}">
|
class="elimination-screen" id="{{ colour }}"
|
||||||
|
alt="Screen with colour {{ colour }}"
|
||||||
|
data-controller="elimination"
|
||||||
|
data-action="click->elimination#next"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user