This commit is contained in:
2025-03-04 08:15:50 +01:00
parent d337ce86fb
commit 451d117d9e
83 changed files with 5528 additions and 262 deletions

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Answer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class AnswerCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Answer::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Candidate;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class CandidateCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Candidate::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Correction;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class CorrectionCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Correction::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
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\Season;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class DashboardController extends AbstractDashboardController
{
#[Route('/admin', name: 'admin')]
public function index(): Response
{
// return parent::index();
// Option 1. You can make your dashboard redirect to some common page of your backend
//
$adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);
return $this->redirect($adminUrlGenerator->setController(SeasonCrudController::class)->generateUrl());
// Option 2. You can make your dashboard redirect to different pages depending on the user
//
// if ('jane' === $this->getUser()->getUsername()) {
// return $this->redirect('...');
// }
// Option 3. You can render some custom template to display a proper dashboard with widgets, etc.
// (tip: it's easier if your template extends from @EasyAdmin/page/content.html.twig)
//
// return $this->render('some/path/my-dashboard.html.twig');
}
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('TijdVoorDeTest');
}
public function configureMenuItems(): iterable
{
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linkToCrud('Season', 'fas fa-list', Season::class);
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('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);
yield MenuItem::linkToLogout('Logout', 'fa fa-exit');
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\GivenAnswer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class GivenAnswerCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return GivenAnswer::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Question;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class QuestionCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Question::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Quiz;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class QuizCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Quiz::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Season;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class SeasonCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Season::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class UserCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return User::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginController extends AbstractController
{
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('login/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Season;
use App\Form\EnterNameType;
use App\Form\SelectSeasonType;
use App\Helpers\Base64;
use Safe\Exceptions\UrlException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
#[AsController]
class QuizController extends AbstractController
{
public const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
private const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
#[Route(path: '/', name: 'select_season', methods: ['GET', 'POST'])]
public function selectSeason(Request $request): Response
{
$form = $this->createForm(SelectSeasonType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
return $this->redirectToRoute('enter_name', ['seasonCode' => $data['season_code']]);
}
return $this->render('quiz/select_season.html.twig', ['form' => $form]);
}
#[Route(path: '/{seasonCode}', name: 'enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
public function enterName(
Request $request,
Season $season,
): Response {
$form = $this->createForm(EnterNameType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$name = $data['name'];
return $this->redirectToRoute('quiz_page', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64_url_encode($name)]);
}
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
}
#[Route(
path: '/{seasonCode}/{nameHash}',
name: 'quiz_page',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
)]
public function quizPage(Season $season, string $nameHash)
{
try {
$name = Base64::base64_url_decode($nameHash);
} catch (UrlException $e) {
}
return $this->render('quiz/question.twig', ['season' => $season, 'name' => $name]);
}
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\Answer;
use App\Entity\Candidate;
use App\Entity\Question;
use App\Entity\Quiz;
use App\Entity\Season;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class KrtekFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
$season = new Season();
$manager->persist($season);
$season->setName('Krtek Weekend')
->setSeasonCode('12345')
->setPreregisterCandidates(true);
$quiz1 = new Quiz();
$manager->persist($quiz1);
$quiz1->setName('Quiz 1')
->setSeason($season);
$season->setActiveQuiz($quiz1)
->addCandidate(new Candidate('Claudia'))
->addCandidate(new Candidate('Eelco'))
->addCandidate(new Candidate('Elise'))
->addCandidate(new Candidate('Gert-Jan'))
->addCandidate(new Candidate('Iris'))
->addCandidate(new Candidate('Jari'))
->addCandidate(new Candidate('Lara'))
->addCandidate(new Candidate('Lotte'))
->addCandidate(new Candidate('Myrthe'))
->addCandidate(new Candidate('Philine'))
->addCandidate(new Candidate('Remy'))
->addCandidate(new Candidate('Robbert'))
->addCandidate(new Candidate('Tom'))
;
$quiz1->addQuestion((new Question())
->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Ja', true))
->addAnswer(new Answer('Nee'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Hoeveel broers heeft de Krtek?')
->addAnswer(new Answer('Geen', true))
->addAnswer(new Answer('1'))
->addAnswer(new Answer('2'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Wat is de lievelingsfeestdag van de Krtek?')
->addAnswer(new Answer('Geen'))
->addAnswer(new Answer('Diens eigen verjaardag'))
->addAnswer(new Answer('Koningsdag'))
->addAnswer(new Answer('Kerst', true))
->addAnswer(new Answer('Oud en Nieuw'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?')
->addAnswer(new Answer('Met het OV', true))
->addAnswer(new Answer('Met de auto'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Met wie keek de Kretek video bij binnenkomst?')
->addAnswer(new Answer('Claudia'))
->addAnswer(new Answer('Eelco'))
->addAnswer(new Answer('Elise'))
->addAnswer(new Answer('Gert-Jan'))
->addAnswer(new Answer('Iris'))
->addAnswer(new Answer('Jari'))
->addAnswer(new Answer('Lara'))
->addAnswer(new Answer('Lotte'))
->addAnswer(new Answer('Myrthe'))
->addAnswer(new Answer('Philine'))
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom', true))
);
$quiz1->addQuestion((new Question())
->setQuestion('Welk advies zou de Krtek zichzelf als kind geven?')
->addAnswer(new Answer('Geef je vader een knuffel.'))
->addAnswer(new Answer('Trek je wat minder aan van anderen.'))
->addAnswer(new Answer('Luister meer naar je eigen gevoel in plaats van naar wat anderen vinden.'))
->addAnswer(new Answer('Stel niet alles tot het laatste moment uit.'))
->addAnswer(new Answer('Altijd doorgaan.'))
->addAnswer(new Answer('Probeer ook eens buiten de lijntjes te kleuren', true))
->addAnswer(new Answer('Ga als je groot bent op groepsreis! '))
->addAnswer(new Answer('Trek minder aan van de mening van anderen, het is oké om anders te zijn.'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Wat voor soort schoenen droeg de Krtek bij het diner?')
->addAnswer(new Answer('Sneakers'))
->addAnswer(new Answer('Wandel-/bergschoenen', true))
->addAnswer(new Answer('Lederen schoenen'))
->addAnswer(new Answer('Pantoffels'))
->addAnswer(new Answer('Hakken'))
->addAnswer(new Answer('Geen schoenen, alleen sokken'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Met welk vervoersmiddel reist de Krtek het liefste?')
->addAnswer(new Answer('Fiets', true))
->addAnswer(new Answer('Auto'))
->addAnswer(new Answer('Trein'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Heeft de Krtek een eigen auto?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
);
$quiz1->addQuestion((new Question())
->setQuestion('Van wie is de quote die de Krtek gepakt heeft')
->addAnswer(new Answer('Karen'))
->addAnswer(new Answer('Gilles de Coster'))
->addAnswer(new Answer('Kees Tol'))
->addAnswer(new Answer('Harry en John'))
->addAnswer(new Answer('Georgina Verbaan'))
->addAnswer(new Answer('Marc-Marie Huijbregts'))
->addAnswer(new Answer('Fresia Cousiño Arias, Rik van de Westelaken'))
->addAnswer(new Answer('Ellie Lust'))
->addAnswer(new Answer('Bouba'))
->addAnswer(new Answer('Jan Versteegh'))
->addAnswer(new Answer('Dick Jol'))
->addAnswer(new Answer('Karin de Groot'))
->addAnswer(new Answer('Pieter'))
->addAnswer(new Answer('Renée Fokker'))
->addAnswer(new Answer('Sam, Davy', true))
);
$quiz1->addQuestion((new Question())
->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topitos uit iemands rugzak stelen om te kunnen winnen?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
);
$quiz1->addQuestion((new Question())
->setQuestion('In wat voor bed slaapt de Krtek dit weekend?')
->addAnswer(new Answer('Éénpersoons, losstaand bed'))
->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true))
->addAnswer(new Answer('Tweepersoons bed'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Hoeveel jaar heeft de Krtek gedaan over de middelbare school?')
->addAnswer(new Answer('5'))
->addAnswer(new Answer('6', true))
->addAnswer(new Answer('7'))
->addAnswer(new Answer('8'))
);
$quiz1->addQuestion((new Question())
->setQuestion('Waar zat de Krtek aan tafel bij het diner?')
->addAnswer(new Answer('Met de rug naar de accommodatie'))
->addAnswer(new Answer('Met de rug naar de buitenmuur', true))
);
$quiz1->addQuestion((new Question())
->setQuestion('Wie is de Krtek?')
->addAnswer(new Answer('Claudia', true))
->addAnswer(new Answer('Eelco'))
->addAnswer(new Answer('Elise'))
->addAnswer(new Answer('Gert-Jan'))
->addAnswer(new Answer('Iris'))
->addAnswer(new Answer('Jari'))
->addAnswer(new Answer('Lara'))
->addAnswer(new Answer('Lotte'))
->addAnswer(new Answer('Myrthe'))
->addAnswer(new Answer('Philine'))
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom'))
);
$manager->flush();
}
}

124
src/Entity/Answer.php Normal file
View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\AnswerRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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: AnswerRepository::class)]
class Answer
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id;
#[ORM\ManyToOne(inversedBy: 'answers')]
#[ORM\JoinColumn(nullable: false)]
private Question $question;
/** @var Collection<int, Candidate> */
#[ORM\ManyToMany(targetEntity: Candidate::class, inversedBy: 'answersOnCandidate')]
private Collection $candidates;
/** @var Collection<int, GivenAnswer> */
#[ORM\OneToMany(targetEntity: GivenAnswer::class, mappedBy: 'answer', orphanRemoval: true)]
private Collection $givenAnswers;
public function __construct(
#[ORM\Column(length: 255)]
private string $text,
#[ORM\Column]
private bool $isRightAnswer = false,
) {
$this->candidates = new ArrayCollection();
$this->givenAnswers = new ArrayCollection();
}
public function getId(): Uuid
{
return $this->id;
}
public function getText(): string
{
return $this->text;
}
public function setText(string $text): static
{
$this->text = $text;
return $this;
}
public function getQuestion(): Question
{
return $this->question;
}
public function setQuestion(Question $question): static
{
$this->question = $question;
return $this;
}
public function isRightAnswer(): ?bool
{
return $this->isRightAnswer;
}
public function setRightAnswer(bool $isRightAnswer): static
{
$this->isRightAnswer = $isRightAnswer;
return $this;
}
/** @return Collection<int, Candidate> */
public function getCandidates(): Collection
{
return $this->candidates;
}
public function addCandidate(Candidate $candidate): static
{
if (!$this->candidates->contains($candidate)) {
$this->candidates->add($candidate);
}
return $this;
}
public function removeCandidate(Candidate $candidate): static
{
$this->candidates->removeElement($candidate);
return $this;
}
/** @return Collection<int, GivenAnswer> */
public function getGivenAnswers(): Collection
{
return $this->givenAnswers;
}
public function addGivenAnswer(GivenAnswer $givenAnswer): static
{
if (!$this->givenAnswers->contains($givenAnswer)) {
$this->givenAnswers->add($givenAnswer);
$givenAnswer->setAnswer($this);
}
return $this;
}
}

140
src/Entity/Candidate.php Normal file
View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Helpers\Base64;
use App\Repository\CandidateRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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: CandidateRepository::class)]
class Candidate
{
#[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: 'candidates')]
#[ORM\JoinColumn(nullable: false)]
private Season $season;
/** @var Collection<int, Answer> */
#[ORM\ManyToMany(targetEntity: Answer::class, mappedBy: 'candidates')]
private Collection $answersOnCandidate;
/** @var Collection<int, GivenAnswer> */
#[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;
public function __construct(
#[ORM\Column(length: 16)]
private string $name,
) {
$this->answersOnCandidate = new ArrayCollection();
$this->givenAnswers = new ArrayCollection();
$this->corrections = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getSeason(): Season
{
return $this->season;
}
public function setSeason(Season $season): static
{
$this->season = $season;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/** @return Collection<int, Answer> */
public function getAnswersOnCandidate(): Collection
{
return $this->answersOnCandidate;
}
public function addAnswersOnCandidate(Answer $answersOnCandidate): static
{
if (!$this->answersOnCandidate->contains($answersOnCandidate)) {
$this->answersOnCandidate->add($answersOnCandidate);
$answersOnCandidate->addCandidate($this);
}
return $this;
}
public function removeAnswersOnCandidate(Answer $answersOnCandidate): static
{
if ($this->answersOnCandidate->removeElement($answersOnCandidate)) {
$answersOnCandidate->removeCandidate($this);
}
return $this;
}
/** @return Collection<int, GivenAnswer> */
public function getGivenAnswers(): Collection
{
return $this->givenAnswers;
}
public function addGivenAnswer(GivenAnswer $givenAnswer): static
{
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;
}
public function getNameHash(): string
{
return Base64::base64_url_encode($this->name);
}
}

74
src/Entity/Correction.php Normal file
View File

@@ -0,0 +1,74 @@
<?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

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\GivenAnswerRepository;
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: GivenAnswerRepository::class)]
#[ORM\HasLifecycleCallbacks]
class GivenAnswer
{
#[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: '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_MUTABLE, nullable: false)]
private \DateTimeInterface $created;
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 getAnswer(): ?Answer
{
return $this->answer;
}
public function setAnswer(?Answer $answer): static
{
$this->answer = $answer;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$this->created = new DateTimeImmutable();
}
}

99
src/Entity/Question.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\QuestionRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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: QuestionRepository::class)]
class Question
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 255, nullable: false)]
private string $question;
#[ORM\ManyToOne(inversedBy: 'questions')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz;
#[ORM\Column]
private bool $enabled = true;
/** @var Collection<int, Answer> */
#[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', cascade: ['persist'], orphanRemoval: true)]
private Collection $answers;
public function __construct()
{
$this->answers = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getQuestion(): string
{
return $this->question;
}
public function setQuestion(string $question): static
{
$this->question = $question;
return $this;
}
public function getQuiz(): Quiz
{
return $this->quiz;
}
public function setQuiz(Quiz $quiz): static
{
$this->quiz = $quiz;
return $this;
}
public function isEnabled(): ?bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): static
{
$this->enabled = $enabled;
return $this;
}
/** @return Collection<int, Answer> */
public function getAnswers(): Collection
{
return $this->answers;
}
public function addAnswer(Answer $answer): static
{
if (!$this->answers->contains($answer)) {
$this->answers->add($answer);
$answer->setQuestion($this);
}
return $this;
}
}

105
src/Entity/Quiz.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\QuizRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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: QuizRepository::class)]
class Quiz
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 64)]
private string $name;
#[ORM\ManyToOne(inversedBy: 'quizzes')]
#[ORM\JoinColumn(nullable: false)]
private Season $season;
/** @var Collection<int, Question> */
#[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
private Collection $questions;
/** @var Collection<int, Correction> */
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
private Collection $corrections;
public function __construct()
{
$this->questions = new ArrayCollection();
$this->corrections = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getSeason(): Season
{
return $this->season;
}
public function setSeason(Season $season): static
{
$this->season = $season;
return $this;
}
/** @return Collection<int, Question> */
public function getQuestions(): Collection
{
return $this->questions;
}
public function addQuestion(Question $question): static
{
if (!$this->questions->contains($question)) {
$this->questions->add($question);
$question->setQuiz($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->setQuiz($this);
}
return $this;
}
}

161
src/Entity/Season.php Normal file
View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\SeasonRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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: SeasonRepository::class)]
class Season
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 64)]
private string $name;
#[ORM\Column(length: 5)]
private string $seasonCode;
#[ORM\Column]
private bool $preregisterCandidates;
/** @var Collection<int, Quiz> */
#[ORM\OneToMany(targetEntity: Quiz::class, mappedBy: 'season', orphanRemoval: true)]
private Collection $quizzes;
/** @var Collection<int, Candidate> */
#[ORM\OneToMany(targetEntity: Candidate::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)]
private Collection $candidates;
/** @var Collection<int, User> */
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'seasons')]
private Collection $owners;
#[ORM\ManyToOne]
private ?Quiz $ActiveQuiz = null;
public function __construct()
{
$this->quizzes = new ArrayCollection();
$this->candidates = new ArrayCollection();
$this->owners = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getSeasonCode(): ?string
{
return $this->seasonCode;
}
public function setSeasonCode(string $seasonCode): static
{
$this->seasonCode = $seasonCode;
return $this;
}
public function isPreregisterCandidates(): bool
{
return $this->preregisterCandidates;
}
public function setPreregisterCandidates(bool $preregisterCandidates): static
{
$this->preregisterCandidates = $preregisterCandidates;
return $this;
}
/** @return Collection<int, Quiz> */
public function getQuizzes(): Collection
{
return $this->quizzes;
}
public function addQuiz(Quiz $quiz): static
{
if (!$this->quizzes->contains($quiz)) {
$this->quizzes->add($quiz);
$quiz->setSeason($this);
}
return $this;
}
/** @return Collection<int, Candidate> */
public function getCandidates(): Collection
{
return $this->candidates;
}
public function addCandidate(Candidate $candidate): static
{
if (!$this->candidates->contains($candidate)) {
$this->candidates->add($candidate);
$candidate->setSeason($this);
}
return $this;
}
/** @return Collection<int, User> */
public function getOwners(): Collection
{
return $this->owners;
}
public function addOwner(User $owner): static
{
if (!$this->owners->contains($owner)) {
$this->owners->add($owner);
}
return $this;
}
public function removeOwner(User $owner): static
{
$this->owners->removeElement($owner);
return $this;
}
public function getActiveQuiz(): ?Quiz
{
return $this->ActiveQuiz;
}
public function setActiveQuiz(?Quiz $ActiveQuiz): static
{
$this->ActiveQuiz = $ActiveQuiz;
return $this;
}
}

143
src/Entity/User.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 180)]
private string $email;
/** @var list<string> The user roles */
#[ORM\Column]
private array $roles = [];
/** @var string The hashed password */
#[ORM\Column]
private string $password;
/** @var Collection<int, Season> */
#[ORM\ManyToMany(targetEntity: Season::class, mappedBy: 'owners')]
private Collection $seasons;
public function __construct()
{
$this->seasons = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*
* @return non-empty-string
*/
public function getUserIdentifier(): string
{
return $this->email;
}
/**
* @see UserInterface
*
* @return non-empty-list<string>
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/** @param list<string> $roles */
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/** @see PasswordAuthenticatedUserInterface */
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
/** @see UserInterface */
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
/** @return Collection<int, Season> */
public function getSeasons(): Collection
{
return $this->seasons;
}
public function addSeason(Season $season): static
{
if (!$this->seasons->contains($season)) {
$this->seasons->add($season);
$season->addOwner($this);
}
return $this;
}
public function removeSeason(Season $season): static
{
if ($this->seasons->removeElement($season)) {
$season->removeOwner($this);
}
return $this;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class EnterNameType extends AbstractType
{
public function __construct(private TranslatorInterface $translator)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class,
['required' => true, 'label' => $this->translator->trans('Enter your name')],
)
// ->add('submit', SubmitType::class, ['label' => 'Start quiz'])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Regex;
class SelectSeasonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('season_code', TextType::class,
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/")],
)
// ->add('submit', SubmitType::class, ['label' => 'Start quiz'])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}

25
src/Helpers/Base64.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Helpers;
use Safe\Exceptions\UrlException;
class Base64
{
private function __construct()
{
}
public static function base64_url_encode(string $input): string
{
return strtr(base64_encode($input), '+/=', '-_.');
}
/** @throws UrlException */
public static function base64_url_decode(string $input): string
{
return \Safe\base64_decode(strtr($input, '-_.', '+/='), true);
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;

View File

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

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Candidate;
use App\Entity\Season;
use App\Helpers\Base64;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Safe\Exceptions\UrlException;
/**
* @extends ServiceEntityRepository<Candidate>
*/
class CandidateRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Candidate::class);
}
public function getCandidateByHash(Season $season, string $hash): ?Candidate
{
try {
$name = Base64::base64_url_decode($hash);
} catch (UrlException) {
return null;
}
return $this->findOneBy(['season' => $season, 'name' => $name]);
}
}

View File

@@ -0,0 +1,20 @@
<?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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<User>
*
* @implements PasswordUpgraderInterface<User>
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
/** Used to upgrade (rehash) the user's password automatically over time. */
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
$user->setPassword($newHashedPassword);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
}