From a5b0b23559be3e1edd25501ba5fbbf476b6c22f6 Mon Sep 17 00:00:00 2001 From: Marijn Doeve Date: Wed, 8 Jul 2026 15:50:30 +0200 Subject: [PATCH] feat: add candidate rename and delete Adds per-candidate rename and delete actions to the candidates tab, guarded by confirmation modals since deletion also discards the candidate's given answers. Closes #18 --- .../Backoffice/SeasonController.php | 55 ++++++++ src/Repository/CandidateRepository.php | 6 + .../help/nl/season_candidates.html.twig | 1 + .../season/tab_candidates.html.twig | 62 ++++++++- .../Backoffice/SeasonControllerTest.php | 120 ++++++++++++++++++ translations/messages+intl-icu.nl.xliff | 48 +++++-- 6 files changed, 282 insertions(+), 10 deletions(-) create mode 100644 tests/Controller/Backoffice/SeasonControllerTest.php diff --git a/src/Controller/Backoffice/SeasonController.php b/src/Controller/Backoffice/SeasonController.php index fd1dbbc..32f2e8d 100644 --- a/src/Controller/Backoffice/SeasonController.php +++ b/src/Controller/Backoffice/SeasonController.php @@ -10,10 +10,13 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\RedirectResponse; 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\Routing\Requirement\Requirement; +use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; @@ -26,6 +29,7 @@ use Tvdt\Enum\FlashType; use Tvdt\Form\AddCandidatesFormType; use Tvdt\Form\SettingsForm; use Tvdt\Form\UploadQuizFormType; +use Tvdt\Repository\CandidateRepository; use Tvdt\Security\Voter\SeasonVoter; use Tvdt\Service\QuizSpreadsheetService; @@ -37,6 +41,7 @@ class SeasonController extends AbstractController private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $em, private readonly QuizSpreadsheetService $quizSpreadsheet, + private readonly CandidateRepository $candidateRepository, ) {} #[IsGranted(SeasonVoter::EDIT, subject: 'season')] @@ -123,6 +128,56 @@ class SeasonController extends AbstractController return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form, 'season' => $season]); } + #[IsCsrfTokenValid('rename_candidate')] + #[IsGranted(SeasonVoter::EDIT, subject: 'candidate')] + #[Route( + '/backoffice/season/{seasonCode:season}/candidate/{candidate}/rename', + name: 'tvdt_backoffice_candidate_rename', + requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'candidate' => Requirement::UUID], + methods: ['POST'], + )] + public function renameCandidate(Season $season, Candidate $candidate, Request $request): RedirectResponse + { + $name = mb_trim($request->request->getString('name')); + + if ('' === $name || mb_strlen($name) > 16) { + $this->addFlash(FlashType::Danger, $this->translator->trans('The candidate name must be between 1 and 16 characters')); + + return $this->redirectToRoute('tvdt_backoffice_season_candidates', ['seasonCode' => $season->seasonCode]); + } + + $candidate->name = $name; + + try { + $this->em->flush(); + } catch (UniqueConstraintViolationException) { + $this->addFlash(FlashType::Danger, $this->translator->trans('A candidate with this name already exists in this season')); + + return $this->redirectToRoute('tvdt_backoffice_season_candidates', ['seasonCode' => $season->seasonCode]); + } + + $this->addFlash(FlashType::Success, $this->translator->trans('Candidate renamed')); + + return $this->redirectToRoute('tvdt_backoffice_season_candidates', ['seasonCode' => $season->seasonCode]); + } + + #[IsCsrfTokenValid('delete_candidate')] + #[IsGranted(SeasonVoter::DELETE, subject: 'candidate')] + #[Route( + '/backoffice/season/{seasonCode:season}/candidate/{candidate}/delete', + name: 'tvdt_backoffice_candidate_delete', + requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'candidate' => Requirement::UUID], + methods: ['POST'], + )] + public function deleteCandidate(Season $season, Candidate $candidate): RedirectResponse + { + $this->candidateRepository->deleteCandidate($candidate); + + $this->addFlash(FlashType::Success, $this->translator->trans('Candidate deleted')); + + return $this->redirectToRoute('tvdt_backoffice_season_candidates', ['seasonCode' => $season->seasonCode]); + } + #[IsGranted(SeasonVoter::EDIT, subject: 'season')] #[Route( '/backoffice/season/{seasonCode:season}/add-quiz', diff --git a/src/Repository/CandidateRepository.php b/src/Repository/CandidateRepository.php index 8c8bf20..ccf6fe2 100644 --- a/src/Repository/CandidateRepository.php +++ b/src/Repository/CandidateRepository.php @@ -19,6 +19,12 @@ class CandidateRepository extends ServiceEntityRepository parent::__construct($registry, Candidate::class); } + public function deleteCandidate(Candidate $candidate): void + { + $this->getEntityManager()->remove($candidate); + $this->getEntityManager()->flush(); + } + public function getCandidateByHash(Season $season, string $hash): ?Candidate { try { diff --git a/templates/backoffice/help/nl/season_candidates.html.twig b/templates/backoffice/help/nl/season_candidates.html.twig index bfabbb6..1ba7720 100644 --- a/templates/backoffice/help/nl/season_candidates.html.twig +++ b/templates/backoffice/help/nl/season_candidates.html.twig @@ -1,3 +1,4 @@
Kandidaten

Dit zijn de spelers van dit seizoen. Voeg alle deelnemers toe voordat je de eerste test start, kandidaten worden automatisch aan nieuwe testen gekoppeld.

Namen zijn vrij in te voeren, gebruik dezelfde schrijfwijze die je in het spel gebruikt.

+

Gebruik het potlood-icoon om een kandidaat te hernoemen en het prullenbak-icoon om er een te verwijderen. Verwijderen gooit ook alle gegeven antwoorden van die kandidaat weg.

diff --git a/templates/backoffice/season/tab_candidates.html.twig b/templates/backoffice/season/tab_candidates.html.twig index 70f9a95..33fab63 100644 --- a/templates/backoffice/season/tab_candidates.html.twig +++ b/templates/backoffice/season/tab_candidates.html.twig @@ -4,9 +4,67 @@ {{ 'Add Candidate'|trans }} -