19 Commits

Author SHA1 Message Date
cfb69c8dab Add Deploy step 2025-09-08 21:51:00 +02:00
35ec71302b Update bake 2025-09-08 19:53:23 +02:00
b7a570928a Update dependencies 2025-09-08 19:50:23 +02:00
d80436534f Update Symfony packages 2025-09-08 19:26:45 +02:00
dependabot[bot]
14e2dd490e Bump phpstan/phpstan-symfony from 2.0.6 to 2.0.7
Bumps [phpstan/phpstan-symfony](https://github.com/phpstan/phpstan-symfony) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/phpstan/phpstan-symfony/releases)
- [Commits](https://github.com/phpstan/phpstan-symfony/compare/2.0.6...2.0.7)

---
updated-dependencies:
- dependency-name: phpstan/phpstan-symfony
  dependency-version: 2.0.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 13:30:05 +02:00
dependabot[bot]
68e54b1110 Bump doctrine/doctrine-bundle from 2.14.0 to 2.15.0
Bumps [doctrine/doctrine-bundle](https://github.com/doctrine/DoctrineBundle) from 2.14.0 to 2.15.0.
- [Release notes](https://github.com/doctrine/DoctrineBundle/releases)
- [Commits](https://github.com/doctrine/DoctrineBundle/compare/2.14.0...2.15.0)

---
updated-dependencies:
- dependency-name: doctrine/doctrine-bundle
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 13:29:46 +02:00
dependabot[bot]
e615b2cdea Bump symfony/serializer from 7.3.0 to 7.3.1
Bumps [symfony/serializer](https://github.com/symfony/serializer) from 7.3.0 to 7.3.1.
- [Release notes](https://github.com/symfony/serializer/releases)
- [Changelog](https://github.com/symfony/serializer/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/serializer/compare/v7.3.0...v7.3.1)

---
updated-dependencies:
- dependency-name: symfony/serializer
  dependency-version: 7.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 13:29:34 +02:00
dependabot[bot]
4b45a2c557 Bump symfony/security-bundle from 7.3.0 to 7.3.1
Bumps [symfony/security-bundle](https://github.com/symfony/security-bundle) from 7.3.0 to 7.3.1.
- [Release notes](https://github.com/symfony/security-bundle/releases)
- [Changelog](https://github.com/symfony/security-bundle/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/security-bundle/compare/v7.3.0...v7.3.1)

---
updated-dependencies:
- dependency-name: symfony/security-bundle
  dependency-version: 7.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 13:29:24 +02:00
c6fe553341 Create dependabot.yml 2025-07-25 13:04:20 +02:00
69a2b9c811 Open db port 2025-06-15 02:18:27 +02:00
f31a7d527d Fix scores? 2025-06-14 21:34:52 +02:00
ed3cf7644f Correct sorting for scores 2025-06-14 12:32:18 +02:00
77d21b004f Update backoffice templates to dynamically include titles and improve candidate handling in SeasonController 2025-06-14 12:32:18 +02:00
379fafcd16 Fix cs 2025-06-12 15:03:01 +02:00
7586d2d8ac Merge branch 'main' of github.com:MarijnDoeve/TijdVoorDeTest 2025-06-11 18:27:04 +02:00
9e41376244 Translations! 2025-06-11 18:26:17 +02:00
2bfef94bbe Add settings management for seasons, update templates, migrations, and commands 2025-06-10 23:06:59 +02:00
a8c4cba968 Disable Turbo for now 2025-06-09 15:52:15 +02:00
d5566d4737 Refactor repositories to use DQL queries, simplify logic, and enhance query efficiency 2025-06-09 14:19:10 +02:00
40 changed files with 1419 additions and 868 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "composer" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- main - main
tags:
- '*'
pull_request: ~ pull_request: ~
workflow_dispatch: ~ workflow_dispatch: ~
@@ -18,10 +20,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.1.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build Docker images - name: Build Docker images
uses: docker/bake-action@v4 uses: docker/bake-action@v5
with: with:
pull: true pull: true
load: true load: true
@@ -53,12 +57,17 @@ jobs:
run: docker compose exec -T php vendor/bin/phpunit run: docker compose exec -T php vendor/bin/phpunit
- name: Doctrine Schema Validator - name: Doctrine Schema Validator
run: docker compose exec -T php bin/console -e test doctrine:schema:validate run: docker compose exec -T php bin/console -e test doctrine:schema:validate
lint: deploy:
name: Docker Lint name: Deploy
environment:
name: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }}
url: ${{ vars.URL }}
needs: tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps: steps:
- name: Checkout - shell: bash
uses: actions/checkout@v4 env:
- name: Lint Dockerfile PORTAINER_WEBHOOK: ${{secrets.PORTAINER_WEBHOOK}}
uses: hadolint/hadolint-action@v3.1.0 run: |
curl -v -X POST "$PORTAINER_WEBHOOK"

5
.idea/php.xml generated
View File

@@ -26,6 +26,11 @@
<phpcs_fixer_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" standards="DoctrineAnnotation;PER;PER-CS;PER-CS1.0;PER-CS2.0;PHP54Migration;PHP56Migration;PHP70Migration;PHP71Migration;PHP73Migration;PHP74Migration;PHP80Migration;PHP81Migration;PHP82Migration;PHP83Migration;PHP84Migration;PHPUnit100Migration;PHPUnit30Migration;PHPUnit32Migration;PHPUnit35Migration;PHPUnit43Migration;PHPUnit48Migration;PHPUnit50Migration;PHPUnit52Migration;PHPUnit54Migration;PHPUnit55Migration;PHPUnit56Migration;PHPUnit57Migration;PHPUnit60Migration;PHPUnit75Migration;PHPUnit84Migration;PHPUnit91Migration;PSR1;PSR12;PSR2;PhpCsFixer;Symfony" tool_path="vendor/bin/php-cs-fixer" timeout="30000" /> <phpcs_fixer_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" standards="DoctrineAnnotation;PER;PER-CS;PER-CS1.0;PER-CS2.0;PHP54Migration;PHP56Migration;PHP70Migration;PHP71Migration;PHP73Migration;PHP74Migration;PHP80Migration;PHP81Migration;PHP82Migration;PHP83Migration;PHP84Migration;PHPUnit100Migration;PHPUnit30Migration;PHPUnit32Migration;PHPUnit35Migration;PHPUnit43Migration;PHPUnit48Migration;PHPUnit50Migration;PHPUnit52Migration;PHPUnit54Migration;PHPUnit55Migration;PHPUnit56Migration;PHPUnit57Migration;PHPUnit60Migration;PHPUnit75Migration;PHPUnit84Migration;PHPUnit91Migration;PSR1;PSR12;PSR2;PhpCsFixer;Symfony" tool_path="vendor/bin/php-cs-fixer" timeout="30000" />
</phpcsfixer_settings> </phpcsfixer_settings>
</component> </component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpExternalFormatter"> <component name="PhpExternalFormatter">
<option name="externalFormatter" value="PHP_CS_FIXER" /> <option name="externalFormatter" value="PHP_CS_FIXER" />
</component> </component>

View File

@@ -1,15 +1,15 @@
{ {
"controllers": { "controllers": {
"@symfony/ux-turbo": { "@symfony/ux-turbo": {
"turbo-core": { "turbo-core": {
"enabled": true, "enabled": false,
"fetch": "eager" "fetch": "eager"
}, },
"mercure-turbo-stream": { "mercure-turbo-stream": {
"enabled": false, "enabled": false,
"fetch": "eager" "fetch": "eager"
} }
} }
}, },
"entrypoints": [] "entrypoints": []
} }

View File

@@ -24,6 +24,8 @@ services:
database: database:
networks: networks:
- internal - internal
ports:
- "5430:5432"
networks: networks:
web: web:
external: true external: true

View File

@@ -9,21 +9,21 @@
"php": ">=8.4", "php": ">=8.4",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"doctrine/dbal": "^4.2.3", "doctrine/dbal": "^4.3.3",
"doctrine/doctrine-bundle": "^2.14.0", "doctrine/doctrine-bundle": "^2.16.1",
"doctrine/doctrine-migrations-bundle": "^3.4.2", "doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.3.3", "doctrine/orm": "^3.5.2",
"easycorp/easyadmin-bundle": "^4.24.7", "easycorp/easyadmin-bundle": "^4.25.0",
"phpdocumentor/reflection-docblock": "^5.6.2", "phpdocumentor/reflection-docblock": "^5.6.3",
"phpoffice/phpspreadsheet": "^4.3.1", "phpoffice/phpspreadsheet": "^5.1",
"phpstan/phpdoc-parser": "^2.1", "phpstan/phpdoc-parser": "^2.3",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"sentry/sentry-symfony": "^5.2", "sentry/sentry-symfony": "^5.4",
"symfony/asset": "7.3.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*", "symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*", "symfony/console": "7.3.*",
"symfony/dotenv": "7.3.*", "symfony/dotenv": "7.3.*",
"symfony/flex": "^2.7.1", "symfony/flex": "^2.8.2",
"symfony/form": "7.3.*", "symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*", "symfony/framework-bundle": "7.3.*",
"symfony/mailer": "7.3.*", "symfony/mailer": "7.3.*",
@@ -35,10 +35,10 @@
"symfony/serializer": "7.3.*", "symfony/serializer": "7.3.*",
"symfony/twig-bundle": "7.3.*", "symfony/twig-bundle": "7.3.*",
"symfony/uid": "7.3.*", "symfony/uid": "7.3.*",
"symfony/ux-turbo": "^2.26.1", "symfony/ux-turbo": "^2.30.0",
"symfony/yaml": "7.3.*", "symfony/yaml": "7.3.*",
"symfonycasts/sass-bundle": "^0.8.2", "symfonycasts/sass-bundle": "^0.8.3",
"symfonycasts/verify-email-bundle": "^1.17.3", "symfonycasts/verify-email-bundle": "^1.17.4",
"thecodingmachine/safe": "^3.3.0", "thecodingmachine/safe": "^3.3.0",
"twig/extra-bundle": "^3.21", "twig/extra-bundle": "^3.21",
"twig/intl-extra": "^3.21", "twig/intl-extra": "^3.21",
@@ -46,23 +46,23 @@
}, },
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.1", "doctrine/doctrine-fixtures-bundle": "^4.1",
"friendsofphp/php-cs-fixer": "^3.75.0", "friendsofphp/php-cs-fixer": "^3.87.1",
"phpstan/extension-installer": "^1.4.3", "phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.17", "phpstan/phpstan": "^2.1.22",
"phpstan/phpstan-doctrine": "^2.0.3", "phpstan/phpstan-doctrine": "^2.0.5",
"phpstan/phpstan-phpunit": "^2.0.6", "phpstan/phpstan-phpunit": "^2.0.7",
"phpstan/phpstan-symfony": "^2.0.6", "phpstan/phpstan-symfony": "^2.0.8",
"phpunit/phpunit": "^12.2.1", "phpunit/phpunit": "^12.3.8",
"rector/rector": "^2.0.17", "rector/rector": "^2.1.6",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"symfony/browser-kit": "7.3.*", "symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.3.*", "symfony/css-selector": "7.3.*",
"symfony/maker-bundle": "^1.63.0", "symfony/maker-bundle": "^1.64.0",
"symfony/phpunit-bridge": "7.3.*", "symfony/phpunit-bridge": "7.3.*",
"symfony/stopwatch": "7.3.*", "symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*", "symfony/web-profiler-bundle": "7.3.*",
"thecodingmachine/phpstan-safe-rule": "^1.4.1", "thecodingmachine/phpstan-safe-rule": "^1.4.1",
"vincentlanglet/twig-cs-fixer": "^3.7.1" "vincentlanglet/twig-cs-fixer": "^3.9.0"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {

1678
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,7 @@ security:
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
- { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER } - { path: ^/backoffice, roles: ROLE_USER }
when@test: when@test:
security: security:

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250610210417 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE season_settings (id UUID NOT NULL, show_numbers BOOLEAN DEFAULT false NOT NULL, confirm_answers BOOLEAN DEFAULT false NOT NULL, PRIMARY KEY(id))
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE season ADD settings_id UUID DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE season ADD CONSTRAINT FK_F0E45BA959949888 FOREIGN KEY (settings_id) REFERENCES season_settings (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_F0E45BA959949888 ON season (settings_id)
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
DROP TABLE season_settings
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE season DROP CONSTRAINT FK_F0E45BA959949888
SQL);
$this->addSql(<<<'SQL'
DROP INDEX UNIQ_F0E45BA959949888
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE season DROP settings_id
SQL);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\SeasonSettings;
use App\Repository\SeasonRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:add-settings',
description: 'Add a short description for your command',
)]
readonly class AddSettingsCommand
{
public function __construct(private SeasonRepository $seasonRepository, private EntityManagerInterface $entityManager) {}
public function __invoke(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
foreach ($this->seasonRepository->findAll() as $season) {
if (null !== $season->getSettings()) {
continue;
}
$io->text('Adding settings to season : '.$season->getSeasonCode());
$season->setSettings(new SeasonSettings());
}
$this->entityManager->flush();
return Command::SUCCESS;
}
}

View File

@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Entity\Answer; use App\Entity\Answer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
/** @extends AbstractCrudController<Answer> */
class AnswerCrudController extends AbstractCrudController class AnswerCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string

View File

@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Entity\Candidate; use App\Entity\Candidate;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
/** @extends AbstractCrudController<Candidate> */
class CandidateCrudController extends AbstractCrudController class CandidateCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string

View File

@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Entity\GivenAnswer; use App\Entity\GivenAnswer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
/** @extends AbstractCrudController<GivenAnswer> */
class GivenAnswerCrudController extends AbstractCrudController class GivenAnswerCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string

View File

@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Entity\Question; use App\Entity\Question;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
/** @extends AbstractCrudController<Question> */
class QuestionCrudController extends AbstractCrudController class QuestionCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string

View File

@@ -7,7 +7,8 @@ namespace App\Controller\Admin;
use App\Entity\QuizCandidate; use App\Entity\QuizCandidate;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
class CorrectionCrudController extends AbstractCrudController /** @extends AbstractCrudController<QuizCandidate> */
class QuizCorrectionCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string
{ {

View File

@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Entity\Quiz; use App\Entity\Quiz;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
/** @extends AbstractCrudController<Quiz> */
class QuizCrudController extends AbstractCrudController class QuizCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string

View File

@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Entity\Season; use App\Entity\Season;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
/** @extends AbstractCrudController<Season> */
class SeasonCrudController extends AbstractCrudController class SeasonCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string

View File

@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Entity\User; use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
/** @extends AbstractCrudController<User> */
class UserCrudController extends AbstractCrudController class UserCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string

View File

@@ -51,7 +51,7 @@ class QuizController extends AbstractController
#[Route( #[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/enable', '/backoffice/season/{seasonCode:season}/quiz/{quiz}/enable',
name: 'app_backoffice_enable', name: 'app_backoffice_enable',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID], requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID.'|null'],
)] )]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')] #[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): RedirectResponse public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): RedirectResponse

View File

@@ -10,6 +10,7 @@ use App\Entity\Quiz;
use App\Entity\Season; use App\Entity\Season;
use App\Enum\FlashType; use App\Enum\FlashType;
use App\Form\AddCandidatesFormType; use App\Form\AddCandidatesFormType;
use App\Form\SettingsForm;
use App\Form\UploadQuizFormType; use App\Form\UploadQuizFormType;
use App\Security\Voter\SeasonVoter; use App\Security\Voter\SeasonVoter;
use App\Service\QuizSpreadsheetService; use App\Service\QuizSpreadsheetService;
@@ -26,7 +27,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
#[IsGranted('ROLE_USER')] #[IsGranted('ROLE_USER')]
class SeasonController extends AbstractController class SeasonController extends AbstractController
{ {
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $em, public function __construct(
private readonly TranslatorInterface $translator,
private readonly EntityManagerInterface $em,
) {} ) {}
#[Route( #[Route(
@@ -35,10 +38,19 @@ class SeasonController extends AbstractController
requirements: ['seasonCode' => self::SEASON_CODE_REGEX], requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
)] )]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')] #[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season): Response public function index(Season $season, Request $request): Response
{ {
$form = $this->createForm(SettingsForm::class, $season->getSettings());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->em->flush();
}
return $this->render('backoffice/season.html.twig', [ return $this->render('backoffice/season.html.twig', [
'season' => $season, 'season' => $season,
'form' => $form,
]); ]);
} }
@@ -57,7 +69,7 @@ class SeasonController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$candidates = $form->get('candidates')->getData(); $candidates = $form->get('candidates')->getData();
foreach (explode("\n", (string) $candidates) as $candidate) { foreach (explode("\n", (string) $candidates) as $candidate) {
$season->addCandidate(new Candidate($candidate)); $season->addCandidate(new Candidate(mb_rtrim($candidate)));
} }
$this->em->flush(); $this->em->flush();

View File

@@ -125,6 +125,6 @@ final class QuizController extends AbstractController
$quizCandidateRepository->createIfNotExist($quiz, $candidate); $quizCandidateRepository->createIfNotExist($quiz, $candidate);
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]); return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question, 'season' => $season]);
} }
} }

View File

@@ -57,6 +57,7 @@ final class RegistrationController extends AbstractController
} catch (TransportExceptionInterface $e) { } catch (TransportExceptionInterface $e) {
$logger->error($e->getMessage()); $logger->error($e->getMessage());
} }
$response = $security->login($user, 'form_login', 'main'); $response = $security->login($user, 'form_login', 'main');
\assert($response instanceof Response); \assert($response instanceof Response);

View File

@@ -46,8 +46,13 @@ class Season
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Quiz $ActiveQuiz = null; private ?Quiz $ActiveQuiz = null;
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: true)]
private ?SeasonSettings $settings = null;
public function __construct() public function __construct()
{ {
$this->settings = new SeasonSettings();
$this->quizzes = new ArrayCollection(); $this->quizzes = new ArrayCollection();
$this->candidates = new ArrayCollection(); $this->candidates = new ArrayCollection();
$this->owners = new ArrayCollection(); $this->owners = new ArrayCollection();
@@ -166,4 +171,16 @@ class Season
return $this; return $this;
} }
public function getSettings(): ?SeasonSettings
{
return $this->settings;
}
public function setSettings(SeasonSettings $settings): static
{
$this->settings = $settings;
return $this;
}
} }

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\SeasonSettingsRepository;
use Doctrine\DBAL\Types\Types;
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: SeasonSettingsRepository::class)]
class SeasonSettings
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id;
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
private bool $showNumbers = false;
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
private bool $confirmAnswers = false;
public function getId(): Uuid
{
return $this->id;
}
public function isShowNumbers(): bool
{
return $this->showNumbers;
}
public function setShowNumbers(bool $showNumbers): self
{
$this->showNumbers = $showNumbers;
return $this;
}
public function isConfirmAnswers(): bool
{
return $this->confirmAnswers;
}
public function setConfirmAnswers(bool $confirmAnswers): self
{
$this->confirmAnswers = $confirmAnswers;
return $this;
}
}

View File

@@ -39,15 +39,8 @@ class RegistrationFormType extends AbstractType
'second_options' => ['label' => $this->translator->trans('Repeat Password')], 'second_options' => ['label' => $this->translator->trans('Repeat Password')],
'mapped' => false, 'mapped' => false,
'constraints' => [ 'constraints' => [
new NotBlank([ new NotBlank(message: 'Please enter a password'),
'message' => 'Please enter a password', new Length(min: 8, max: 4096, minMessage: 'Your password should be at least {{ limit }} characters'),
]),
new Length([
'min' => 8,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
], ],
'translation_domain' => false, 'translation_domain' => false,
]) ])

35
src/Form/SettingsForm.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\SeasonSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/** @extends AbstractType<SeasonSettings> */
class SettingsForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('showNumbers', options: [
'label_attr' => ['class' => 'checkbox-switch'],
'attr' => ['role' => 'switch', 'switch' => null]])
->add('confirmAnswers', options: [
'label_attr' => ['class' => 'checkbox-switch'],
'attr' => ['role' => 'switch', 'switch' => null]])
->add('save', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => SeasonSettings::class,
]);
}
}

View File

@@ -31,13 +31,9 @@ class UploadQuizFormType extends AbstractType
'required' => true, 'required' => true,
'translation_domain' => false, 'translation_domain' => false,
'constraints' => [ 'constraints' => [
new File([ new File(maxSize: '1024k', mimeTypes: [
'maxSize' => '1024k', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'mimeTypes' => [ ], mimeTypesMessage: $this->translator->trans('Please upload a valid XLSX file')),
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
'mimeTypesMessage' => $this->translator->trans('Please upload a valid XLSX file'),
]),
], ],
]) ])
; ;

View File

@@ -34,12 +34,14 @@ class CandidateRepository extends ServiceEntityRepository
return null; return null;
} }
return $this->createQueryBuilder('c') return $this->getEntityManager()->createQuery(<<<DQL
->where('c.season = :season') select c from App\Entity\Candidate c
->andWhere('lower(c.name) = lower(:name)') where c.season = :season
->setParameter('season', $season) and lower(c.name) = lower(:name)
DQL
)->setParameter('season', $season)
->setParameter('name', $name) ->setParameter('name', $name)
->getQuery()->getOneOrNullResult(); ->getOneOrNullResult();
} }
public function save(Candidate $candidate, bool $flush = true): void public function save(Candidate $candidate, bool $flush = true): void
@@ -54,44 +56,22 @@ class CandidateRepository extends ServiceEntityRepository
/** @return ResultList */ /** @return ResultList */
public function getScores(Quiz $quiz): array public function getScores(Quiz $quiz): array
{ {
$qb = $this->createQueryBuilder('c', 'c.id') return $this->getEntityManager()->createQuery(<<<DQL
->select('c.id', 'c.name', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'qc.corrections', 'max(ga.created) - qc.created as time') select
->join('c.givenAnswers', 'ga') c.id,
->join('ga.answer', 'a') c.name,
->join('c.quizData', 'qc') sum(case when a.isRightAnswer = true then 1 else 0 end) as correct,
->where('qc.quiz = :quiz') qc.corrections,
->groupBy('ga.quiz', 'c.id', 'qc.id') max(ga.created) - qc.created as time,
->setParameter('quiz', $quiz); (sum(case when a.isRightAnswer = true then 1 else 0 end) + qc.corrections) as score
from App\Entity\Candidate c
return $this->sortResults( join c.givenAnswers ga
$this->calculateScore( join ga.answer a
$qb->getQuery()->getResult(), join c.quizData qc
), where qc.quiz = :quiz and ga.quiz = :quiz
); group by ga.quiz, c.id, qc.id
} order by score desc, time asc
DQL
/** )->setParameter('quiz', $quiz)->getResult();
* @param array<string, array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections: float}> $in
*
* @return array<string, Result>
*/
private function calculateScore(array $in): array
{
return array_map(static fn ($candidate): array => [
...$candidate,
'score' => $candidate['correct'] + $candidate['corrections'],
], $in);
}
/**
* @param array<string, Result> $results
*
* @return ResultList
* */
private function sortResults(array $results): array
{
usort($results, static fn ($a, $b): int => $b['score'] <=> $a['score']);
return $results;
} }
} }

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Entity\Candidate; use App\Entity\Candidate;
use App\Entity\GivenAnswer;
use App\Entity\Question; use App\Entity\Question;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@@ -22,22 +21,21 @@ class QuestionRepository extends ServiceEntityRepository
public function findNextQuestionForCandidate(Candidate $candidate): ?Question public function findNextQuestionForCandidate(Candidate $candidate): ?Question
{ {
$qb = $this->createQueryBuilder('q'); return $this->getEntityManager()->createQuery(<<<DQL
select q from App\Entity\Question q
return $qb->join('q.quiz', 'qz') join q.quiz qz
->andWhere($qb->expr()->notIn('q.id', $this->getEntityManager()->createQueryBuilder() where q.id not in (
->select('q1') select q1.id from App\Entity\GivenAnswer ga
->from(GivenAnswer::class, 'ga') join ga.answer a
->join('ga.answer', 'a') join a.question q1
->join('a.question', 'q1') where ga.candidate = :candidate
->andWhere($qb->expr()->isNotNull('ga.answer')) and q1.quiz = :quiz
->andWhere('ga.candidate = :candidate') )
->andWhere('q1.quiz = :quiz') and qz = :quiz
->getDQL())) DQL)
->andWhere('qz = :quiz')
->setMaxResults(1) ->setMaxResults(1)
->setParameter('candidate', $candidate) ->setParameter('candidate', $candidate)
->setParameter('quiz', $candidate->getSeason()->getActiveQuiz()) ->setParameter('quiz', $candidate->getSeason()->getActiveQuiz())
->getQuery()->getOneOrNullResult(); ->getOneOrNullResult();
} }
} }

View File

@@ -22,11 +22,9 @@ class SeasonRepository extends ServiceEntityRepository
/** @return list<Season> Returns an array of Season objects */ /** @return list<Season> Returns an array of Season objects */
public function getSeasonsForUser(User $user): array public function getSeasonsForUser(User $user): array
{ {
$qb = $this->createQueryBuilder('s') return $this->getEntityManager()->createQuery(<<<DQL
->where(':user MEMBER OF s.owners') select s from App\Entity\Season s where :user member of s.owners order by s.name
->orderBy('s.name') DQL
->setParameter('user', $user); )->setParameter('user', $user)->getResult();
return $qb->getQuery()->getResult();
} }
} }

View File

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

View File

@@ -12,6 +12,7 @@ use App\Entity\Quiz;
use App\Entity\Season; use App\Entity\Season;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** @extends Voter<string, Season> */ /** @extends Voter<string, Season> */
@@ -37,7 +38,7 @@ final class SeasonVoter extends Voter
} }
/** @param Season|Elimination|Quiz|Candidate|Answer|Question $subject */ /** @param Season|Elimination|Quiz|Candidate|Answer|Question $subject */
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$user = $token->getUser(); $user = $token->getUser();
if (!$user instanceof User) { if (!$user instanceof User) {

View File

@@ -73,7 +73,7 @@ class QuizSpreadsheetService
* *
* @throws SpreadsheetDataException * @throws SpreadsheetDataException
*/ */
private function fillQuizFromArray(Quiz $quiz, array $sheet): Quiz private function fillQuizFromArray(Quiz $quiz, array $sheet): void
{ {
$errors = []; $errors = [];
@@ -110,8 +110,6 @@ class QuizSpreadsheetService
if ([] !== $errors) { if ([] !== $errors) {
throw new SpreadsheetDataException($errors); throw new SpreadsheetDataException($errors);
} }
return $quiz;
} }
public function quizToXlsx(Quiz $quiz): void {} public function quizToXlsx(Quiz $quiz): void {}

View File

@@ -1,3 +1,4 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block importmap %}{{ importmap('backoffice') }}{% endblock %} {% block importmap %}{{ importmap('backoffice') }}{% endblock %}
{% block title %}Tijd voor de test | {% endblock %}
{% block nav %}{{ include('backoffice/nav.html.twig') }}{% endblock %} {% block nav %}{{ include('backoffice/nav.html.twig') }}{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends 'backoffice/base.html.twig' %} {% extends 'backoffice/base.html.twig' %}
{% block title %}Hello BackofficeController!{% endblock %} {% block title %}{{ parent() }}Backoffice{% endblock %}
{% block body %} {% block body %}
<div class="d-flex flex-row align-items-center"> <div class="d-flex flex-row align-items-center">

View File

@@ -1,5 +1,7 @@
{% extends 'backoffice/base.html.twig' %} {% extends 'backoffice/base.html.twig' %}
{% block title %}{{ parent() }}{{ quiz.season.name }}{% endblock %}
{% block body %} {% block body %}
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2> <h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
<div class="py-2 btn-group" data-controller="bo--quiz"> <div class="py-2 btn-group" data-controller="bo--quiz">
@@ -51,7 +53,7 @@
</div> </div>
</div> </div>
{% else %} {% else %}
EMPTY {{ 'EMPTY'|trans }}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -125,15 +127,16 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">Please Confirm</h1> <h1 class="modal-title fs-5" id="staticBackdropLabel">{{ 'Please Confirm'|trans }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
Are you sure you want to clear all the results? This will also delete al the eliminations. {{ 'Are you sure you want to clear all the results? This will also delete al the eliminations.'|trans }}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'No'|trans }}</button>
<a href="{{ path('app_backoffice_quiz_clear', {quiz: quiz.id}) }}" class="btn btn-danger">Yes</a> <a href="{{ path('app_backoffice_quiz_clear', {quiz: quiz.id}) }}"
class="btn btn-danger">{{ 'Yes'|trans }}</a>
</div> </div>
</div> </div>
</div> </div>
@@ -146,19 +149,18 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">Please Confirm</h1> <h1 class="modal-title fs-5" id="staticBackdropLabel">{{ 'Please Confirm'|trans }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
Are you sure you want to delete this quiz? {{ 'Are you sure you want to delete this quiz?'|trans }}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'No'|trans }}</button>
<a href="{{ path('app_backoffice_quiz_delete', {quiz: quiz.id}) }}" class="btn btn-danger">Yes</a> <a href="{{ path('app_backoffice_quiz_delete', {quiz: quiz.id}) }}"
class="btn btn-danger">{{ 'Yes'|trans }}</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block title %}
{% endblock %}

View File

@@ -21,5 +21,5 @@
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{{ parent() }}Backoffice
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends 'backoffice/base.html.twig' %} {% extends 'backoffice/base.html.twig' %}
{% block title %}{{ parent() }}{{ season.name }}{% endblock %}
{% block body %} {% block body %}
<h2 class="py-2">{{ 'Season'|trans }}: {{ season.name }}</h2> <h2 class="py-2">{{ 'Season'|trans }}: {{ season.name }}</h2>
<div class="row"> <div class="row">
@@ -21,13 +22,18 @@
<div class="d-flex flex-row align-items-center"> <div class="d-flex flex-row align-items-center">
<h4 class="py-2 pe-2">{{ 'Candidates'|trans }}</h4> <h4 class="py-2 pe-2">{{ 'Candidates'|trans }}</h4>
<a class="link" <a class="link"
href="{{ path('app_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}</a> href="{{ path('app_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}
</a>
</div> </div>
<ul> <ul>
{% for candidate in season.candidates %} {% for candidate in season.candidates %}
<li>{{ candidate.name }}</li>{% endfor %} <li>{{ candidate.name }}</li>{% endfor %}
</ul> </ul>
<div class="d-flex flex-row align-items-center">
<h4 class="py-2 pe-2">{{ 'Settings'|trans }}</h4>
</div>
{{ form(form) }}
</div> </div>
<div class="col-12 col-md-3"></div> <div class="col-12 col-md-3"></div>
</div> </div>

View File

@@ -1,17 +1,35 @@
{% extends 'quiz/base.html.twig' %} {% extends 'quiz/base.html.twig' %}
{% block body %} {% block body %}
{{ question.question }}<br/> <h4>
{% if season.settings.showNumbers %}
({{ question.ordering }}/{{ question.quiz.questions.count }})
{% endif %}{{ question.question }}<br/>
</h4>
<form method="post"> <form method="post">
<input type="hidden" name="token" value="{{ csrf_token('question') }}"> <input type="hidden" name="token" value="{{ csrf_token('question') }}">
{% for answer in question.answers %} {% if season.settings.confirmAnswers == false %}
<div> {% for answer in question.answers %}
<button class="btn btn-outline-success" <div class="py-2">
type="submit" <button class="btn btn-outline-success"
name="answer" type="submit"
value="{{ answer.id }}">{{ answer.text }}</button> name="answer"
</div> value="{{ answer.id }}">{{ answer.text }}</button>
</div>
{% endfor %}
{% else %} {% else %}
Weirdly enough this question has no answers... {% for answer in question.answers %}
{% endfor %} <div class="py-1">
<input type="radio" class="btn-check" name="answer" id="answer-{{ loop.index0 }}" autocomplete="off"
value="{{ answer.id }}">
<label class="btn btn-outline-secondary" for="answer-{{ loop.index0 }}">{{ answer.text }}</label>
</div>
{% endfor %}
<div class="py-2">
<button class="btn btn-success"
type="submit"
>{{ 'Next'|trans }}</button>
</div>
{% endif %}
</form> </form>
{% endblock body %} {% endblock body %}

View File

@@ -33,6 +33,14 @@
<source>Already have an account? Log in</source> <source>Already have an account? Log in</source>
<target>Heb je al een account? Log in</target> <target>Heb je al een account? Log in</target>
</trans-unit> </trans-unit>
<trans-unit id="Qu1euq_" resname="Are you sure you want to clear all the results? This will also delete al the eliminations.">
<source>Are you sure you want to clear all the results? This will also delete al the eliminations.</source>
<target>Weet je zeker datatype je de resultaten will leegmaken? Dit gooit ook alle eliminaties weg.</target>
</trans-unit>
<trans-unit id="Ec4twG8" resname="Are you sure you want to delete this quiz?">
<source>Are you sure you want to delete this quiz?</source>
<target>Weet je zeker datatype je deze test will verwijderen?</target>
</trans-unit>
<trans-unit id=".QFPbFe" resname="Back"> <trans-unit id=".QFPbFe" resname="Back">
<source>Back</source> <source>Back</source>
<target>Terug</target> <target>Terug</target>
@@ -89,6 +97,10 @@
<source>Download Template</source> <source>Download Template</source>
<target>Download sjabloon</target> <target>Download sjabloon</target>
</trans-unit> </trans-unit>
<trans-unit id="FfYlwX8" resname="EMPTY">
<source>EMPTY</source>
<target>LEEG</target>
</trans-unit>
<trans-unit id="JZi_tm0" resname="Email"> <trans-unit id="JZi_tm0" resname="Email">
<source>Email</source> <source>Email</source>
<target>E-mail</target> <target>E-mail</target>
@@ -137,6 +149,14 @@
<source>Name</source> <source>Name</source>
<target>Naam</target> <target>Naam</target>
</trans-unit> </trans-unit>
<trans-unit id="wd1MvZW" resname="No">
<source>No</source>
<target>Nee</target>
</trans-unit>
<trans-unit id="gefhnBC" resname="Next">
<source>Next</source>
<target>Volgende</target>
</trans-unit>
<trans-unit id="nOHriCl" resname="No active quiz"> <trans-unit id="nOHriCl" resname="No active quiz">
<source>No active quiz</source> <source>No active quiz</source>
<target>Geen actieve test</target> <target>Geen actieve test</target>
@@ -157,9 +177,13 @@
<source>Password</source> <source>Password</source>
<target>Wachtwoord</target> <target>Wachtwoord</target>
</trans-unit> </trans-unit>
<trans-unit id="VbgD9L8" resname="Please Confirm">
<source>Please Confirm</source>
<target>Bevestig Alsjeblieft</target>
</trans-unit>
<trans-unit id="6EclFME" resname="Please Confirm your Email"> <trans-unit id="6EclFME" resname="Please Confirm your Email">
<source>Please Confirm your Email</source> <source>Please Confirm your Email</source>
<target>messages</target> <target>Bevestig je e-mailadres alsjeblieft</target>
</trans-unit> </trans-unit>
<trans-unit id="lSX_PHJ" resname="Please sign in"> <trans-unit id="lSX_PHJ" resname="Please sign in">
<source>Please sign in</source> <source>Please sign in</source>
@@ -253,6 +277,10 @@
<source>Seasons</source> <source>Seasons</source>
<target>Seizoenen</target> <target>Seizoenen</target>
</trans-unit> </trans-unit>
<trans-unit id="VXFwlwn" resname="Settings">
<source>Settings</source>
<target>Instellingen</target>
</trans-unit>
<trans-unit id="pNIxNSX" resname="Sign in"> <trans-unit id="pNIxNSX" resname="Sign in">
<source>Sign in</source> <source>Sign in</source>
<target>Log in</target> <target>Log in</target>
@@ -277,6 +305,10 @@
<source>Time</source> <source>Time</source>
<target>Tijd</target> <target>Tijd</target>
</trans-unit> </trans-unit>
<trans-unit id="pRCwpOT" resname="Yes">
<source>Yes</source>
<target>Ja</target>
</trans-unit>
<trans-unit id="0afY1NF" resname="You have no seasons yet."> <trans-unit id="0afY1NF" resname="You have no seasons yet.">
<source>You have no seasons yet.</source> <source>You have no seasons yet.</source>
<target>Je hebt nog geen seizoenen.</target> <target>Je hebt nog geen seizoenen.</target>