Compare commits

..

4 Commits

Author SHA1 Message Date
Marijn b8b50e6d50 fix: right-align quiz status badges
Puts the badge at the end of the row via ms-auto instead of leading
the quiz name.
2026-07-08 15:33:59 +02:00
Marijn e8a8fb3f54 fix: align quiz status badges to the left of the row
Previously the badge trailed the quiz name, so its position shifted
with the name's length. Placing it first keeps it at a consistent
left edge across all rows.
2026-07-08 15:30:34 +02:00
Marijn 15e3ecb2e4 fix: hide Ready badge on the active quiz, translate to Voorbereid
An active quiz is finalized by definition, so showing both badges was
redundant.
2026-07-08 15:26:24 +02:00
Marijn 748f51e1a5 feat: show active/ready badges on quiz list
Adds an "Active" badge for the season's currently selected quiz and
renames the "Finalized" badge to "Ready" for finalized quizzes, so
both states are visible at a glance.
2026-07-08 15:24:18 +02:00
6 changed files with 17 additions and 166 deletions
@@ -10,12 +10,10 @@ 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;
@@ -99,24 +97,6 @@ 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',
@@ -1,4 +1,3 @@
<h6>Seizoensinstellingen</h6>
<p>Pas hier de weergave-instellingen van dit seizoen aan.</p>
<p><strong>Nummers tonen:</strong> toont vraagnummers tijdens de test. <strong>Antwoord bevestigen:</strong> vraagt kandidaten om hun antwoord te bevestigen voordat ze doorgaan.</p>
<p><strong>Seizoenscode:</strong> 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.</p>
@@ -1,40 +1,8 @@
<div class="row">
<div class="col-md-6 col-12">
{{ form(form) }}
<hr>
<h4 class="text-danger">{{ 'Season code'|trans }}</h4>
<p>{{ '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 }}</p>
<p><strong>{{ 'Current code:'|trans }}</strong> {{ season.seasonCode }}</p>
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#regenerateSeasonCodeModal">
{{ 'Regenerate season code...'|trans }}
</button>
</div>
<div class="col-md-6 col-12">
{{ include('backoffice/help/season_settings.html.twig') }}
</div>
</div>
<div class="modal fade" id="regenerateSeasonCodeModal" data-bs-backdrop="static"
tabindex="-1"
aria-labelledby="regenerateSeasonCodeModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ path('tvdt_backoffice_season_regenerate_code', {seasonCode: season.seasonCode}) }}" method="POST">
<div class="modal-header">
<h1 class="modal-title fs-5" id="regenerateSeasonCodeModalLabel">{{ 'Regenerate season code'|trans }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>{{ 'This invalidates the current season code. Anyone using the old code will no longer be able to join this season.'|trans }}</p>
<input type="hidden" name="_token" value="{{ csrf_token('regenerate_season_code') }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Cancel'|trans }}</button>
<button type="submit" class="btn btn-danger">{{ 'Regenerate season code'|trans }}</button>
</div>
</form>
</div>
</div>
</div>
@@ -8,11 +8,13 @@
</div>
<div class="list-group mb-3">
{% for quiz in season.quizzes %}
<a class="list-group-item list-group-item-action{% if season.activeQuiz == quiz %} active{% endif %}"
<a class="list-group-item list-group-item-action d-flex align-items-center gap-2{% if season.activeQuiz == quiz %} active{% endif %}"
href="{{ path('tvdt_backoffice_quiz', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">
{{ quiz.name }}
{% if quiz.isFinalized %}
<span class="badge text-bg-success">{{ 'Finalized'|trans }}</span>
{% if season.activeQuiz == quiz %}
<span class="badge text-bg-light ms-auto">{{ 'Active'|trans }}</span>
{% elseif quiz.isFinalized %}
<span class="badge text-bg-success ms-auto">{{ 'Ready'|trans }}</span>
{% endif %}
</a>
{% else %}
@@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
namespace Tvdt\Tests\Controller\Backoffice;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
use Tvdt\Controller\Backoffice\SeasonController;
use Tvdt\Entity\Season;
use Tvdt\Entity\User;
#[CoversClass(SeasonController::class)]
final class SeasonControllerTest extends WebTestCase
{
private KernelBrowser $client;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
$this->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);
}
}
+12 -40
View File
@@ -221,10 +221,6 @@
<source>Create an empty quiz and add questions from the question bank.</source>
<target>Maak een lege quiz aan en voeg vragen toe vanuit de vragenbank.</target>
</trans-unit>
<trans-unit id="jrXhJOR" resname="Current code:">
<source>Current code:</source>
<target>Huidige code:</target>
</trans-unit>
<trans-unit id="3leUyoA" resname="Current email address:">
<source>Current email address:</source>
<target>Huidig e-mailadres:</target>
@@ -665,6 +661,10 @@
<source>Re-opens the quiz for editing. Candidates will no longer be able to take the quiz until it is finalized again.</source>
<target>Heropent de test voor bewerking. Deelnemers kunnen de test niet meer afnemen totdat deze opnieuw is afgerond.</target>
</trans-unit>
<trans-unit id="MuwD4xO" resname="Ready">
<source>Ready</source>
<target>Voorbereid</target>
</trans-unit>
<trans-unit id="P1HcfAu" resname="Red">
<source>Red</source>
<target>Rood</target>
@@ -673,14 +673,6 @@
<source>Refresh the page to try again.</source>
<target>Ververs de pagina om het opnieuw te proberen.</target>
</trans-unit>
<trans-unit id="EGW_4W_" resname="Regenerate season code">
<source>Regenerate season code</source>
<target>Seizoenscode opnieuw genereren</target>
</trans-unit>
<trans-unit id="EWuNNNu" resname="Regenerate season code...">
<source>Regenerate season code...</source>
<target>Seizoenscode opnieuw genereren...</target>
</trans-unit>
<trans-unit id="fGfBzt6" resname="Register">
<source>Register</source>
<target>Registreren</target>
@@ -741,14 +733,6 @@
<source>Season Name</source>
<target>Seizoennaam</target>
</trans-unit>
<trans-unit id="IxKDF4a" resname="Season code">
<source>Season code</source>
<target>Seizoenscode</target>
</trans-unit>
<trans-unit id="iigT2eM" resname="Season code regenerated">
<source>Season code regenerated</source>
<target>Seizoenscode opnieuw gegenereerd</target>
</trans-unit>
<trans-unit id="kc_J96C" resname="Seasons">
<source>Seasons</source>
<target>Seizoenen</target>
@@ -789,14 +773,6 @@
<source>Sync latest changes to this quiz</source>
<target>Laatste wijzigingen synchroniseren naar deze quiz</target>
</trans-unit>
<trans-unit id="XwQ1Fav" resname="The confirmation email could not be sent. Please try again later.">
<source>The confirmation email could not be sent. Please try again later.</source>
<target>De bevestigingsmail kon niet worden verzonden. Probeer het later opnieuw.</target>
</trans-unit>
<trans-unit id="mMDwcxj" resname="The confirmation email could not be sent. Please use the resend button to try again.">
<source>The confirmation email could not be sent. Please use the resend button to try again.</source>
<target>De bevestigingsmail kon niet worden verzonden. Gebruik de knop om het opnieuw te proberen.</target>
</trans-unit>
<trans-unit id="_z4el3Z" resname="The password fields must match.">
<source>The password fields must match.</source>
<target>De wachtwoorden moeten overeen komen.</target>
@@ -821,10 +797,6 @@
<source>The quiz must be finalized before it can be activated</source>
<target>De test moet afgerond zijn voordat deze geactiveerd kan worden</target>
</trans-unit>
<trans-unit id="IKPt_Pf" resname="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.">
<source>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.</source>
<target>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.</target>
</trans-unit>
<trans-unit id="HuzRgeN" resname="There are no answers for this question">
<source>There are no answers for this question</source>
<target>Er zijn geen antwoorden voor deze vraag</target>
@@ -841,10 +813,6 @@
<source>This deletes your account and every season you are the only owner of. Enter your password to confirm.</source>
<target>Dit verwijdert je account en alle seizoenen waarvan jij de enige eigenaar bent. Vul je wachtwoord in om te bevestigen.</target>
</trans-unit>
<trans-unit id="EBWUWJu" resname="This invalidates the current season code. Anyone using the old code will no longer be able to join this season.">
<source>This invalidates the current season code. Anyone using the old code will no longer be able to join this season.</source>
<target>Hiermee wordt de huidige seizoenscode ongeldig. Iedereen die de oude code gebruikt, kan dit seizoen niet meer vinden.</target>
</trans-unit>
<trans-unit id="YueaA2f" resname="This link will expire in %count%.">
<source>This link will expire in %count%.</source>
<target>Deze link verloopt over %count%.</target>
@@ -937,14 +905,18 @@
<source>Your data</source>
<target>Je gegevens</target>
</trans-unit>
<trans-unit id="sAb9l8I" resname="Your email address has been changed.">
<source>Your email address has been changed.</source>
<target>Je e-mailadres is gewijzigd.</target>
</trans-unit>
<trans-unit id="OqDtnJw" resname="Your email address has been changed. Please check your inbox to confirm it.">
<source>Your email address has been changed. Please check your inbox to confirm it.</source>
<target>Je e-mailadres is gewijzigd. Check je inbox om het te bevestigen.</target>
</trans-unit>
<trans-unit id="kWpL7Rn" resname="The confirmation email could not be sent. Please use the resend button to try again.">
<source>The confirmation email could not be sent. Please use the resend button to try again.</source>
<target>De bevestigingsmail kon niet worden verzonden. Gebruik de knop om het opnieuw te proberen.</target>
</trans-unit>
<trans-unit id="mQxV2Jf" resname="The confirmation email could not be sent. Please try again later.">
<source>The confirmation email could not be sent. Please try again later.</source>
<target>De bevestigingsmail kon niet worden verzonden. Probeer het later opnieuw.</target>
</trans-unit>
<trans-unit id="m80cBv0" resname="Your email address has been verified.">
<source>Your email address has been verified.</source>
<target>Je e-mailadres is geverifieerd.</target>