mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 04:44:19 +01:00
Refactor elimination feature and improve backoffice usability
This commit introduces a refactored EliminationFactory for better modularity, updates the elimination preparation process, and adds functionality to view eliminations. Backoffice templates and forms have been reorganized, minor translations were corrected, and additional assets like styles and flashes were included for enhanced user experience.
This commit is contained in:
79
src/Controller/Backoffice/BackofficeController.php
Normal file
79
src/Controller/Backoffice/BackofficeController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Backoffice;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Season;
|
||||
use App\Entity\User;
|
||||
use App\Form\CreateSeasonFormType;
|
||||
use App\Repository\SeasonRepository;
|
||||
use App\Service\QuizSpreadsheetService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[AsController]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
final class BackofficeController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SeasonRepository $seasonRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
#[Route('/backoffice/', name: 'app_backoffice_index')]
|
||||
public function index(): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
\assert($user instanceof User);
|
||||
|
||||
$seasons = $this->security->isGranted('ROLE_ADMIN')
|
||||
? $this->seasonRepository->findAll()
|
||||
: $this->seasonRepository->getSeasonsForUser($user);
|
||||
|
||||
return $this->render('backoffice/index.html.twig', [
|
||||
'seasons' => $seasons,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
|
||||
public function addSeason(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$season = new Season();
|
||||
$form = $this->createForm(CreateSeasonFormType::class, $season);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user = $this->getUser();
|
||||
\assert($user instanceof User);
|
||||
|
||||
$season->addOwner($user);
|
||||
$season->generateSeasonCode();
|
||||
|
||||
$em->persist($season);
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
|
||||
public function getTemplate(QuizSpreadsheetService $excel): Response
|
||||
{
|
||||
$response = new StreamedResponse($excel->generateTemplate());
|
||||
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
50
src/Controller/Backoffice/QuizController.php
Normal file
50
src/Controller/Backoffice/QuizController.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Backoffice;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Repository\CandidateRepository;
|
||||
use App\Security\Voter\SeasonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[AsController]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
class QuizController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CandidateRepository $candidateRepository,
|
||||
) {}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function index(Season $season, Quiz $quiz): Response
|
||||
{
|
||||
return $this->render('backoffice/quiz.html.twig', [
|
||||
'season' => $season,
|
||||
'quiz' => $quiz,
|
||||
'result' => $this->candidateRepository->getScores($quiz),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
|
||||
{
|
||||
$season->setActiveQuiz($quiz);
|
||||
$em->flush();
|
||||
|
||||
if ($quiz instanceof Quiz) {
|
||||
return $this->redirectToRoute('app_backoffice_quiz', ['seasonCode' => $season->getSeasonCode(), 'quiz' => $quiz->getId()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
}
|
||||
88
src/Controller/Backoffice/SeasonController.php
Normal file
88
src/Controller/Backoffice/SeasonController.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Backoffice;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Enum\FlashType;
|
||||
use App\Form\AddCandidatesFormType;
|
||||
use App\Form\UploadQuizFormType;
|
||||
use App\Security\Voter\SeasonVoter;
|
||||
use App\Service\QuizSpreadsheetService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsController]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
class SeasonController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator, private EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}', name: 'app_backoffice_season')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function index(Season $season): Response
|
||||
{
|
||||
return $this->render('backoffice/season.html.twig', [
|
||||
'season' => $season,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function addCandidates(Season $season, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(AddCandidatesFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$candidates = $form->get('candidates')->getData();
|
||||
foreach (explode("\r\n", (string) $candidates) as $candidate) {
|
||||
$season->addCandidate(new Candidate($candidate));
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
|
||||
{
|
||||
$quiz = new Quiz();
|
||||
$form = $this->createForm(UploadQuizFormType::class, $quiz);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/* @var UploadedFile $sheet */
|
||||
$sheet = $form->get('sheet')->getData();
|
||||
|
||||
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
|
||||
|
||||
$quiz->setSeason($season);
|
||||
$this->em->persist($quiz);
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Entity\User;
|
||||
use App\Enum\FlashType;
|
||||
use App\Form\AddCandidatesFormType;
|
||||
use App\Form\CreateSeasonFormType;
|
||||
use App\Form\UploadQuizFormType;
|
||||
use App\Repository\CandidateRepository;
|
||||
use App\Repository\SeasonRepository;
|
||||
use App\Security\Voter\SeasonVoter;
|
||||
use App\Service\QuizSpreadsheetService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsController]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
final class BackofficeController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SeasonRepository $seasonRepository,
|
||||
private readonly CandidateRepository $candidateRepository,
|
||||
private readonly Security $security,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
#[Route('/backoffice/', name: 'app_backoffice_index')]
|
||||
public function index(): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
\assert($user instanceof User);
|
||||
|
||||
$seasons = $this->security->isGranted('ROLE_ADMIN')
|
||||
? $this->seasonRepository->findAll()
|
||||
: $this->seasonRepository->getSeasonsForUser($user);
|
||||
|
||||
return $this->render('backoffice/index.html.twig', [
|
||||
'seasons' => $seasons,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
|
||||
public function seasonAdd(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$season = new Season();
|
||||
$form = $this->createForm(CreateSeasonFormType::class, $season);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user = $this->getUser();
|
||||
\assert($user instanceof User);
|
||||
|
||||
$season->addOwner($user);
|
||||
$season->generateSeasonCode();
|
||||
|
||||
$em->persist($season);
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}', name: 'app_backoffice_season')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function season(Season $season): Response
|
||||
{
|
||||
return $this->render('backoffice/season.html.twig', [
|
||||
'season' => $season,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}/{quiz}', name: 'app_backoffice_quiz')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function quiz(Season $season, Quiz $quiz): Response
|
||||
{
|
||||
return $this->render('backoffice/quiz.html.twig', [
|
||||
'season' => $season,
|
||||
'quiz' => $quiz,
|
||||
'result' => $this->candidateRepository->getScores($quiz),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}/{quiz}/enable', name: 'app_backoffice_enable')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
|
||||
{
|
||||
$season->setActiveQuiz($quiz);
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function addCandidates(Season $season, Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$form = $this->createForm(AddCandidatesFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$candidates = $form->get('candidates')->getData();
|
||||
foreach (explode("\r\n", (string) $candidates) as $candidate) {
|
||||
$season->addCandidate(new Candidate($candidate));
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet, EntityManagerInterface $em): Response
|
||||
{
|
||||
$quiz = new Quiz();
|
||||
$form = $this->createForm(UploadQuizFormType::class, $quiz);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/* @var UploadedFile $sheet */
|
||||
$sheet = $form->get('sheet')->getData();
|
||||
|
||||
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
|
||||
|
||||
$quiz->setSeason($season);
|
||||
$em->persist($quiz);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
|
||||
public function getTemplate(QuizSpreadsheetService $excel): Response
|
||||
{
|
||||
$response = new StreamedResponse($excel->generateTemplate());
|
||||
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use App\Enum\FlashType;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsController]
|
||||
final class LoginController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/login', name: 'app_login_login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
public function login(AuthenticationUtils $authenticationUtils, TranslatorInterface $translator): Response
|
||||
{
|
||||
// get the login error if there is one
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
@@ -22,7 +24,11 @@ final class LoginController extends AbstractController
|
||||
// last username entered by the user
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('login/login.html.twig', [
|
||||
if ($error instanceof AuthenticationException) {
|
||||
$this->addFlash(FlashType::Danger, $translator->trans($error->getMessageKey(), $error->getMessageData(), 'security'));
|
||||
}
|
||||
|
||||
return $this->render('backoffice/login/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
|
||||
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Elimination;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Factory\EliminationFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -13,12 +15,19 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
final class PrepareEliminationController extends AbstractController
|
||||
{
|
||||
#[Route('/backoffice/elimination/{seasonCode}/{quiz}/prepare', name: 'app_prepare_elimination')]
|
||||
public function index(Season $season, Quiz $quiz): Response
|
||||
public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
|
||||
{
|
||||
return $this->render('prepare_elimination/index.html.twig', [
|
||||
$elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
|
||||
|
||||
return $this->redirectToRoute('app_prepare_elimination_view', ['elimination' => $elimination->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/elimination/{elimination}', name: 'app_prepare_elimination_view')]
|
||||
public function viewElimination(Elimination $elimination): Response
|
||||
{
|
||||
return $this->render('backoffice/prepare_elimination/index.html.twig', [
|
||||
'controller_name' => 'PrepareEliminationController',
|
||||
'season' => $season,
|
||||
'quiz' => $quiz,
|
||||
'elimination' => $elimination,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ final class RegistrationController extends AbstractController
|
||||
(new TemplatedEmail())
|
||||
->to((string) $user->getEmail())
|
||||
->subject($this->translator->trans('Please Confirm your Email'))
|
||||
->htmlTemplate('registration/confirmation_email.html.twig')
|
||||
->htmlTemplate('backoffice/registration/confirmation_email.html.twig')
|
||||
);
|
||||
|
||||
$response = $security->login($user, 'form_login', 'main');
|
||||
@@ -57,7 +57,7 @@ final class RegistrationController extends AbstractController
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->render('registration/register.html.twig', [
|
||||
return $this->render('backoffice/registration/register.html.twig', [
|
||||
'registrationForm' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -7,27 +7,37 @@ namespace App\Entity;
|
||||
use App\Repository\EliminationRepository;
|
||||
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: EliminationRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class Elimination
|
||||
{
|
||||
public const string SCREEN_GREEN = 'green';
|
||||
public const string SCREEN_RED = 'red';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'eliminations')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Quiz $quiz;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $data = [];
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||
private \DateTimeImmutable $created;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(inversedBy: 'eliminations')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Quiz $quiz,
|
||||
) {}
|
||||
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
@@ -40,22 +50,26 @@ class Elimination
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
public function setData(array $data): static
|
||||
public function setData(array $data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setQuiz(Quiz $quiz): self
|
||||
{
|
||||
$this->quiz = $quiz;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuiz(): Quiz
|
||||
{
|
||||
return $this->quiz;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$this->created = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getCreated(): \DateTimeInterface
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,12 @@ class Quiz
|
||||
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
|
||||
private Collection $corrections;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $dropouts = null;
|
||||
#[ORM\Column(nullable: false, options: ['default' => 1])]
|
||||
private int $dropouts = 1;
|
||||
|
||||
/** @var Collection<int, Elimination> */
|
||||
#[ORM\OneToMany(targetEntity: Elimination::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['created' => 'DESC'])]
|
||||
private Collection $eliminations;
|
||||
|
||||
public function __construct()
|
||||
@@ -113,12 +114,12 @@ class Quiz
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDropouts(): ?int
|
||||
public function getDropouts(): int
|
||||
{
|
||||
return $this->dropouts;
|
||||
}
|
||||
|
||||
public function setDropouts(?int $dropouts): static
|
||||
public function setDropouts(int $dropouts): static
|
||||
{
|
||||
$this->dropouts = $dropouts;
|
||||
|
||||
|
||||
38
src/Factory/EliminationFactory.php
Normal file
38
src/Factory/EliminationFactory.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Entity\Elimination;
|
||||
use App\Entity\Quiz;
|
||||
use App\Repository\CandidateRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final readonly class EliminationFactory
|
||||
{
|
||||
public function __construct(
|
||||
private CandidateRepository $candidateRepository,
|
||||
private EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function createEliminationFromQuiz(Quiz $quiz): Elimination
|
||||
{
|
||||
$elimination = new Elimination($quiz);
|
||||
$this->em->persist($elimination);
|
||||
|
||||
$scores = $this->candidateRepository->getScores($quiz);
|
||||
|
||||
$simpleScores = [];
|
||||
|
||||
foreach (array_reverse($scores) as $i => $score) {
|
||||
$simpleScores[$score['name']] = $i < $quiz->getDropouts() ? Elimination::SCREEN_RED : Elimination::SCREEN_GREEN;
|
||||
}
|
||||
|
||||
$elimination->setData($simpleScores);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $elimination;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class AddCandidatesFormType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('candidates', TextareaType::class, [
|
||||
'label' => $this->translator->trans('Candidates'),
|
||||
'label' => $this->translator->trans('Candidates'), 'translation_domain' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class CreateSeasonFormType extends AbstractType
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => $this->translator->trans('Season Name'),
|
||||
'translation_domain' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ class EnterNameType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class,
|
||||
['required' => true, 'label' => $this->translator->trans('Enter your name')],
|
||||
[
|
||||
'required' => true,
|
||||
'label' => $this->translator->trans('Enter your name'),
|
||||
'translation_domain' => false,
|
||||
],
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Entity\User;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
@@ -27,11 +28,16 @@ class RegistrationFormType extends AbstractType
|
||||
->add('email', EmailType::class, [
|
||||
'label' => $this->translator->trans('Email'),
|
||||
'attr' => ['autocomplete' => 'email'],
|
||||
'translation_domain' => false,
|
||||
])
|
||||
->add('plainPassword', PasswordType::class, [
|
||||
'label' => $this->translator->trans('Password'),
|
||||
->add('plainPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'invalid_message' => $this->translator->trans('The password fields must match.'),
|
||||
'options' => ['attr' => ['class' => 'password-field']],
|
||||
'required' => true,
|
||||
'first_options' => ['label' => $this->translator->trans('Password')],
|
||||
'second_options' => ['label' => $this->translator->trans('Repeat Password')],
|
||||
'mapped' => false,
|
||||
'attr' => ['autocomplete' => 'new-password'],
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'Please enter a password',
|
||||
@@ -43,6 +49,7 @@ class RegistrationFormType extends AbstractType
|
||||
'max' => 4096,
|
||||
]),
|
||||
],
|
||||
'translation_domain' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class SelectSeasonType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('season_code', TextType::class,
|
||||
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), 'label' => $this->translator->trans('Season Code')]
|
||||
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), 'label' => $this->translator->trans('Season Code'), 'translation_domain' => false]
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ class UploadQuizFormType extends AbstractType
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => $this->translator->trans('Quiz name'),
|
||||
'translation_domain' => false,
|
||||
])
|
||||
->add('sheet', FileType::class, [
|
||||
'label' => $this->translator->trans('Quiz (xlsx)'),
|
||||
'mapped' => false,
|
||||
'required' => true,
|
||||
'translation_domain' => false,
|
||||
'constraints' => [
|
||||
new File([
|
||||
'maxSize' => '1024k',
|
||||
|
||||
@@ -13,11 +13,12 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Safe\Exceptions\UrlException;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Candidate>
|
||||
*
|
||||
* @phpstan-type Result array{0: Candidate, correct: int, time: \DateInterval, corrections?: float, score: float}
|
||||
* @phpstan-type Result array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections?: float, score: float}
|
||||
* @phpstan-type ResultList list<Result>
|
||||
*/
|
||||
class CandidateRepository extends ServiceEntityRepository
|
||||
@@ -56,7 +57,7 @@ class CandidateRepository extends ServiceEntityRepository
|
||||
public function getScores(Quiz $quiz): array
|
||||
{
|
||||
$scoreTimeQb = $this->createQueryBuilder('c', 'c.id')
|
||||
->select('c', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'max(ga.created) - min(ga.created) as time')
|
||||
->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')
|
||||
->join('c.givenAnswers', 'ga')
|
||||
->join('ga.answer', 'a')
|
||||
->where('ga.quiz = :quiz')
|
||||
@@ -64,7 +65,7 @@ class CandidateRepository extends ServiceEntityRepository
|
||||
->setParameter('quiz', $quiz);
|
||||
|
||||
$correctionsQb = $this->createQueryBuilder('c', 'c.id')
|
||||
->select('c', 'cor.amount as corrections')
|
||||
->select('c.id', 'cor.amount as corrections')
|
||||
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
|
||||
->setParameter('quiz', $quiz);
|
||||
|
||||
@@ -74,7 +75,7 @@ class CandidateRepository extends ServiceEntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{0: Candidate, correct: int, time: \DateInterval, corrections?: float}> $in
|
||||
* @param array<string, array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections?: float}> $in
|
||||
*
|
||||
* @return array<string, Result>
|
||||
* */
|
||||
|
||||
@@ -52,14 +52,14 @@ class QuizSpreadsheetService
|
||||
}
|
||||
|
||||
/** @throws SpreadsheetDataException */
|
||||
public function xlsxToQuiz(Quiz $quiz, File $file): Quiz
|
||||
public function xlsxToQuiz(Quiz $quiz, File $file): void
|
||||
{
|
||||
$spreadsheet = $this->readSheet($file);
|
||||
$sheet = $spreadsheet->getSheet($spreadsheet->getFirstSheetIndex());
|
||||
|
||||
$answerLines = \array_slice($sheet->toArray(formatData: false), 1);
|
||||
|
||||
return $this->fillQuizFromArray($quiz, $answerLines);
|
||||
$this->fillQuizFromArray($quiz, $answerLines);
|
||||
}
|
||||
|
||||
private function readSheet(File $file): Spreadsheet
|
||||
|
||||
Reference in New Issue
Block a user