diff --git a/src/Controller/Backoffice/SeasonController.php b/src/Controller/Backoffice/SeasonController.php
index fd1dbbc..22fc715 100644
--- a/src/Controller/Backoffice/SeasonController.php
+++ b/src/Controller/Backoffice/SeasonController.php
@@ -10,10 +10,12 @@ 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\Security\Http\Attribute\IsCsrfTokenValid;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
@@ -97,6 +99,24 @@ class SeasonController extends AbstractController
]);
}
+ #[IsCsrfTokenValid('regenerate_season_code')]
+ #[IsGranted(SeasonVoter::EDIT, subject: 'season')]
+ #[Route(
+ '/backoffice/season/{seasonCode:season}/settings/regenerate-code',
+ name: 'tvdt_backoffice_season_regenerate_code',
+ requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
+ methods: ['POST'],
+ )]
+ public function regenerateSeasonCode(Season $season): RedirectResponse
+ {
+ $season->generateSeasonCode();
+ $this->em->flush();
+
+ $this->addFlash(FlashType::Success, $this->translator->trans('Season code regenerated'));
+
+ return $this->redirectToRoute('tvdt_backoffice_season_settings', ['seasonCode' => $season->seasonCode]);
+ }
+
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/add-candidate',
diff --git a/templates/backoffice/help/nl/season_settings.html.twig b/templates/backoffice/help/nl/season_settings.html.twig
index 9b86a80..856655a 100644
--- a/templates/backoffice/help/nl/season_settings.html.twig
+++ b/templates/backoffice/help/nl/season_settings.html.twig
@@ -1,3 +1,4 @@
Seizoensinstellingen
Pas hier de weergave-instellingen van dit seizoen aan.
Nummers tonen: toont vraagnummers tijdens de test. Antwoord bevestigen: vraagt kandidaten om hun antwoord te bevestigen voordat ze doorgaan.
+
Seizoenscode: de code waarmee kandidaten dit seizoen kunnen vinden. Genereer een nieuwe code als de huidige per ongeluk gedeeld is, de oude code werkt daarna niet meer.
{{ 'The season code is used by candidates to join this season. Regenerating it invalidates the current code, so make sure to share the new one.'|trans }}
diff --git a/tests/Controller/Backoffice/SeasonControllerTest.php b/tests/Controller/Backoffice/SeasonControllerTest.php
new file mode 100644
index 0000000..12922c3
--- /dev/null
+++ b/tests/Controller/Backoffice/SeasonControllerTest.php
@@ -0,0 +1,70 @@
+client = self::createClient();
+ $this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
+
+ $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'krtek-admin@example.org']);
+ $this->assertInstanceOf(User::class, $user);
+ $this->client->loginUser($user);
+ }
+
+ public function testRegenerateSeasonCodeChangesTheCode(): void
+ {
+ $oldCode = 'krtek';
+ $crawler = $this->client->request(Request::METHOD_GET, \sprintf('/backoffice/season/%s/settings', $oldCode));
+ self::assertResponseIsSuccessful();
+
+ $token = (string) $crawler->filter('form[action*="/regenerate-code"] input[name="_token"]')->first()->attr('value');
+
+ $this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/%s/settings/regenerate-code', $oldCode), [
+ '_token' => $token,
+ ]);
+
+ self::assertResponseRedirects();
+ $this->entityManager->clear();
+
+ $this->assertNotInstanceOf(Season::class, $this->entityManager->getRepository(Season::class)->findOneBy(['seasonCode' => $oldCode]));
+
+ $location = (string) $this->client->getResponse()->headers->get('Location');
+ $this->assertMatchesRegularExpression('#^/backoffice/season/[a-z]{5}/settings$#', $location);
+ }
+
+ public function testRegenerateSeasonCodeIsDeniedForNonOwner(): void
+ {
+ $crawler = $this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/settings');
+ self::assertResponseIsSuccessful();
+ $token = (string) $crawler->filter('form[action*="/regenerate-code"] input[name="_token"]')->first()->attr('value');
+
+ $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'test@example.org']);
+ $this->assertInstanceOf(User::class, $user);
+ $this->client->loginUser($user);
+
+ $this->client->request(Request::METHOD_POST, '/backoffice/season/krtek/settings/regenerate-code', [
+ '_token' => $token,
+ ]);
+
+ self::assertResponseStatusCodeSame(403);
+ }
+}
diff --git a/translations/messages+intl-icu.nl.xliff b/translations/messages+intl-icu.nl.xliff
index b077c92..0628e58 100644
--- a/translations/messages+intl-icu.nl.xliff
+++ b/translations/messages+intl-icu.nl.xliff
@@ -221,6 +221,10 @@
Create an empty quiz and add questions from the question bank.Maak een lege quiz aan en voeg vragen toe vanuit de vragenbank.
+
+ Current code:
+ Huidige code:
+ Current email address:Huidig e-mailadres:
@@ -669,6 +673,14 @@
Refresh the page to try again.Ververs de pagina om het opnieuw te proberen.
+
+ Regenerate season code
+ Seizoenscode opnieuw genereren
+
+
+ Regenerate season code...
+ Seizoenscode opnieuw genereren...
+ RegisterRegistreren
@@ -729,6 +741,14 @@
Season NameSeizoennaam
+
+ Season code
+ Seizoenscode
+
+
+ Season code regenerated
+ Seizoenscode opnieuw gegenereerd
+ SeasonsSeizoenen
@@ -769,6 +789,14 @@
Sync latest changes to this quizLaatste wijzigingen synchroniseren naar deze quiz
+
+ The confirmation email could not be sent. Please try again later.
+ De bevestigingsmail kon niet worden verzonden. Probeer het later opnieuw.
+
+
+ The confirmation email could not be sent. Please use the resend button to try again.
+ De bevestigingsmail kon niet worden verzonden. Gebruik de knop om het opnieuw te proberen.
+ The password fields must match.De wachtwoorden moeten overeen komen.
@@ -793,6 +821,10 @@
The quiz must be finalized before it can be activatedDe test moet afgerond zijn voordat deze geactiveerd kan worden
+
+ The season code is used by candidates to join this season. Regenerating it invalidates the current code, so make sure to share the new one.
+ De seizoenscode wordt door kandidaten gebruikt om dit seizoen te vinden. Als je een nieuwe code genereert, werkt de oude niet meer, dus deel de nieuwe code opnieuw.
+ There are no answers for this questionEr zijn geen antwoorden voor deze vraag
@@ -809,6 +841,10 @@
This deletes your account and every season you are the only owner of. Enter your password to confirm.Dit verwijdert je account en alle seizoenen waarvan jij de enige eigenaar bent. Vul je wachtwoord in om te bevestigen.
+
+ This invalidates the current season code. Anyone using the old code will no longer be able to join this season.
+ Hiermee wordt de huidige seizoenscode ongeldig. Iedereen die de oude code gebruikt, kan dit seizoen niet meer vinden.
+ This link will expire in %count%.Deze link verloopt over %count%.
@@ -901,18 +937,14 @@
Your dataJe gegevens
+
+ Your email address has been changed.
+ Je e-mailadres is gewijzigd.
+ Your email address has been changed. Please check your inbox to confirm it.Je e-mailadres is gewijzigd. Check je inbox om het te bevestigen.
-
- The confirmation email could not be sent. Please use the resend button to try again.
- De bevestigingsmail kon niet worden verzonden. Gebruik de knop om het opnieuw te proberen.
-
-
- The confirmation email could not be sent. Please try again later.
- De bevestigingsmail kon niet worden verzonden. Probeer het later opnieuw.
- Your email address has been verified.Je e-mailadres is geverifieerd.