29 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
366bc36520 Fix typo 2025-06-07 23:47:58 +02:00
79b24b0d44 Refine error handling 2025-06-07 23:45:40 +02:00
7f93680987 Allow mail send fail 2025-06-07 23:43:52 +02:00
bdbff32256 Add MAILER_DSN 2025-06-07 23:34:50 +02:00
ebadc24b59 PHP 8.4 2025-06-07 22:24:38 +02:00
e0075fdcdc Rector upgrades 2025-06-07 22:24:38 +02:00
06aafefffc Upgrade to Symfony 7.3 2025-06-07 22:24:38 +02:00
ff6534fa81 Improve links 2025-06-07 22:24:38 +02:00
6a77df402d Add quiz clearing and deletion functionality with UI enhancements
This commit introduces the ability to clear quiz results and delete quizzes directly from the backoffice. It includes new routes, controllers, modals for user confirmation, and updates to translations. The `QuizRepository` now supports dedicated methods for clearing results and deleting quizzes along with error handling. Related database migrations and front-end adjustments are also included.
2025-06-07 20:59:01 +02:00
79236d84e9 Add correction management to backoffice, refactor security voter logic, and enhance candidate scoring
This commit introduces functionality to manage candidate corrections in the backoffice, with updated templates and a new route handler. The SeasonVoter is refactored to support additional entities, and scoring logic is updated to incorporate corrections consistently. Includes test coverage for voter logic and UI improvements for score tables.
2025-06-07 16:09:13 +02:00
71 changed files with 2196 additions and 1198 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
@@ -50,16 +54,20 @@ jobs:
- name: Run migrations - name: Run migrations
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
- name: Run PHPUnit - name: Run PHPUnit
if: false # Remove this line when the tests are ready
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"

View File

@@ -134,7 +134,6 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/browser-kit" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/browser-kit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mailer" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php84" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/verify-email-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/verify-email-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" /> <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/jean85/pretty-package-versions" /> <excludeFolder url="file://$MODULE_DIR$/vendor/jean85/pretty-package-versions" />
@@ -166,6 +165,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/intl-extra" /> <excludeFolder url="file://$MODULE_DIR$/vendor/twig/intl-extra" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stimulus-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stimulus-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/ux-turbo" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/ux-turbo" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

23
.idea/php.xml generated
View File

@@ -7,11 +7,6 @@
</laravel_pint_by_interpreter> </laravel_pint_by_interpreter>
</laravel_pint_settings> </laravel_pint_settings>
</component> </component>
<component name="MessDetector">
<phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="30000" />
</phpmd_settings>
</component>
<component name="MessDetectorOptionsConfiguration"> <component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
@@ -108,7 +103,6 @@
<path value="$PROJECT_DIR$/vendor/symfony/http-client" /> <path value="$PROJECT_DIR$/vendor/symfony/http-client" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" /> <path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" /> <path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" /> <path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" /> <path value="$PROJECT_DIR$/vendor/symfony/mime" />
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" /> <path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
@@ -201,11 +195,13 @@
<path value="$PROJECT_DIR$/vendor/twig/twig" /> <path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" /> <path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" /> <path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
</include_path> </include_path>
</component> </component>
<component name="PhpInterpreters"> <component name="PhpInterpreters">
<interpreters> <interpreters>
<interpreter id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" name="Compose PHP 8.3" home="docker-compose://DATA" auto="false" debugger_id="php.debugger.XDebug"> <interpreter id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" name="Compose PHP 8.4" home="docker-compose://DATA" auto="false" debugger_id="php.debugger.XDebug">
<remote_data INTERPRETER_PATH="php" HELPERS_PATH="/opt/.phpstorm_helpers" VALID="true" RUN_AS_ROOT_VIA_SUDO="false" DOCKER_ACCOUNT_NAME="Colima" DOCKER_COMPOSE_SERVICE_NAME="php" DOCKER_REMOTE_PROJECT_PATH="/opt/project"> <remote_data INTERPRETER_PATH="php" HELPERS_PATH="/opt/.phpstorm_helpers" VALID="true" RUN_AS_ROOT_VIA_SUDO="false" DOCKER_ACCOUNT_NAME="Colima" DOCKER_COMPOSE_SERVICE_NAME="php" DOCKER_REMOTE_PROJECT_PATH="/opt/project">
<type_data command="EXEC" /> <type_data command="EXEC" />
<dockerComposeConfigurationPaths> <dockerComposeConfigurationPaths>
@@ -219,15 +215,15 @@
</component> </component>
<component name="PhpInterpretersPhpInfoCache"> <component name="PhpInterpretersPhpInfoCache">
<phpInfoCache> <phpInfoCache>
<interpreter name="Compose PHP 8.3"> <interpreter name="Compose PHP 8.4">
<phpinfo binary_type="PHP" php_cgi="/usr/local/bin/php-cgi" php_cli="/usr/local/bin/php" path_separator=":" version="8.3.19"> <phpinfo binary_type="PHP" php_cgi="/usr/local/bin/php-cgi" php_cli="/usr/local/bin/php" path_separator=":" version="8.4.8">
<additional_php_ini>/usr/local/etc/php/conf.d/docker-php-ext-apcu.ini, /usr/local/etc/php/conf.d/docker-php-ext-intl.ini, /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini, /usr/local/etc/php/conf.d/docker-php-ext-pdo_pgsql.ini, /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini, /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini, /usr/local/etc/php/conf.d/docker-php-ext-zip.ini, /usr/local/etc/php/app.conf.d/10-app.ini, /usr/local/etc/php/app.conf.d/20-app.dev.ini</additional_php_ini> <additional_php_ini>/usr/local/etc/php/conf.d/docker-php-ext-apcu.ini, /usr/local/etc/php/conf.d/docker-php-ext-excimer.ini, /usr/local/etc/php/conf.d/docker-php-ext-gd.ini, /usr/local/etc/php/conf.d/docker-php-ext-intl.ini, /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini, /usr/local/etc/php/conf.d/docker-php-ext-pdo_pgsql.ini, /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini, /usr/local/etc/php/conf.d/docker-php-ext-uuid.ini, /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini, /usr/local/etc/php/conf.d/docker-php-ext-zip.ini, /usr/local/etc/php/app.conf.d/10-app.ini, /usr/local/etc/php/app.conf.d/20-app.dev.ini</additional_php_ini>
<configuration_file>/usr/local/etc/php/php.ini</configuration_file> <configuration_file>/usr/local/etc/php/php.ini</configuration_file>
<configuration_options> <configuration_options>
<configuration_option name="include_path" value=".:/usr/local/lib/php" /> <configuration_option name="include_path" value=".:/usr/local/lib/php" />
</configuration_options> </configuration_options>
<debuggers> <debuggers>
<debugger_info debugger="xdebug" debugger_version="3.4.2"> <debugger_info debugger="xdebug" debugger_version="3.4.3">
<debug_extensions /> <debug_extensions />
</debugger_info> </debugger_info>
</debuggers> </debuggers>
@@ -244,8 +240,10 @@
<extension name="curl" /> <extension name="curl" />
<extension name="date" /> <extension name="date" />
<extension name="dom" /> <extension name="dom" />
<extension name="excimer" />
<extension name="fileinfo" /> <extension name="fileinfo" />
<extension name="filter" /> <extension name="filter" />
<extension name="gd" />
<extension name="hash" /> <extension name="hash" />
<extension name="iconv" /> <extension name="iconv" />
<extension name="intl" /> <extension name="intl" />
@@ -265,6 +263,7 @@
<extension name="sqlite3" /> <extension name="sqlite3" />
<extension name="standard" /> <extension name="standard" />
<extension name="tokenizer" /> <extension name="tokenizer" />
<extension name="uuid" />
<extension name="xdebug" /> <extension name="xdebug" />
<extension name="xml" /> <extension name="xml" />
<extension name="xmlreader" /> <extension name="xmlreader" />
@@ -276,7 +275,7 @@
</interpreter> </interpreter>
</phpInfoCache> </phpInfoCache>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.3" /> <component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
<component name="PhpStan"> <component name="PhpStan">
<PhpStan_settings> <PhpStan_settings>
<phpstan_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" tool_path="vendor/bin/phpstan" timeout="60000" /> <phpstan_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" tool_path="vendor/bin/phpstan" timeout="60000" />

1
.idea/symfony2.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="Symfony2PluginSettings"> <component name="Symfony2PluginSettings">
<option name="pluginEnabled" value="true" /> <option name="pluginEnabled" value="true" />
<option name="profilerCsvPath" value="" />
</component> </component>
</project> </project>

View File

@@ -1,7 +1,7 @@
#syntax=docker/dockerfile:1 #syntax=docker/dockerfile:1
# Versions # Versions
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
# The different stages of this Dockerfile are meant to be built into separate images # The different stages of this Dockerfile are meant to be built into separate images
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage # https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
@@ -31,7 +31,6 @@ RUN set -eux; \
intl \ intl \
opcache \ opcache \
zip \ zip \
uuid \
gd \ gd \
excimer-1.2.3 \ excimer-1.2.3 \
; ;

View File

@@ -1,5 +1,5 @@
import * as bootstrap from 'bootstrap'
import './bootstrap.js'; import './bootstrap.js';
import 'bootstrap/dist/css/bootstrap.min.css' import 'bootstrap/dist/css/bootstrap.min.css'
import * as bootstrap from 'bootstrap'
import './styles/backoffice.scss'; import './styles/backoffice.scss';

View File

@@ -2,7 +2,7 @@
"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": {

View File

@@ -0,0 +1,17 @@
import {Controller} from '@hotwired/stimulus';
import * as bootstrap from 'bootstrap'
export default class extends Controller {
connect() {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
}
clearQuiz() {
new bootstrap.Modal('#clearQuizModal').show();
}
deleteQuiz() {
new bootstrap.Modal('#deleteQuizModal').show();
}
}

View File

@@ -15,6 +15,7 @@ services:
# See https://xdebug.org/docs/all_settings#mode # See https://xdebug.org/docs/all_settings#mode
XDEBUG_MODE: "${XDEBUG_MODE:-off}" XDEBUG_MODE: "${XDEBUG_MODE:-off}"
MAILER_DSN: "smtp://mailer:1025" MAILER_DSN: "smtp://mailer:1025"
PHP_CS_FIXER_IGNORE_ENV: 1
extra_hosts: extra_hosts:
# Ensure that host.docker.internal is correctly defined on Linux # Ensure that host.docker.internal is correctly defined on Linux
- host.docker.internal:host-gateway - host.docker.internal:host-gateway

View File

@@ -8,6 +8,7 @@ services:
APP_SECRET: ${APP_SECRET} APP_SECRET: ${APP_SECRET}
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
MAILER_DSN: ${MAILER_DSN}
MAILER_SENDER: ${MAILER_SENDER} MAILER_SENDER: ${MAILER_SENDER}
SENTRY_DSN: ${SENTRY_DSN} SENTRY_DSN: ${SENTRY_DSN}
labels: labels:
@@ -23,6 +24,8 @@ services:
database: database:
networks: networks:
- internal - internal
ports:
- "5430:5432"
networks: networks:
web: web:
external: true external: true

View File

@@ -6,39 +6,39 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"require": { "require": {
"php": ">=8.3.15", "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.2.0", "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.2.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.2.*", "symfony/asset-mapper": "7.3.*",
"symfony/console": "7.2.*", "symfony/console": "7.3.*",
"symfony/dotenv": "7.2.*", "symfony/dotenv": "7.3.*",
"symfony/flex": "^2.7.0", "symfony/flex": "^2.8.2",
"symfony/form": "7.2.*", "symfony/form": "7.3.*",
"symfony/framework-bundle": "7.2.*", "symfony/framework-bundle": "7.3.*",
"symfony/mailer": "7.2.*", "symfony/mailer": "7.3.*",
"symfony/property-access": "7.2.*", "symfony/property-access": "7.3.*",
"symfony/property-info": "7.2.*", "symfony/property-info": "7.3.*",
"symfony/runtime": "7.2.*", "symfony/runtime": "7.3.*",
"symfony/security-bundle": "7.2.*", "symfony/security-bundle": "7.3.*",
"symfony/security-csrf": "7.2.*", "symfony/security-csrf": "7.3.*",
"symfony/serializer": "7.2.*", "symfony/serializer": "7.3.*",
"symfony/twig-bundle": "7.2.*", "symfony/twig-bundle": "7.3.*",
"symfony/uid": "7.2.*", "symfony/uid": "7.3.*",
"symfony/ux-turbo": "^2.26", "symfony/ux-turbo": "^2.30.0",
"symfony/yaml": "7.2.*", "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.1.6", "phpunit/phpunit": "^12.3.8",
"rector/rector": "^2.0.16", "rector/rector": "^2.1.6",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"symfony/browser-kit": "7.2.*", "symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.2.*", "symfony/css-selector": "7.3.*",
"symfony/maker-bundle": "^1.63.0", "symfony/maker-bundle": "^1.64.0",
"symfony/phpunit-bridge": "7.2.*", "symfony/phpunit-bridge": "7.3.*",
"symfony/stopwatch": "7.2.*", "symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.2.*", "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": {
@@ -95,7 +95,7 @@
"symfony/polyfill-php81": "*", "symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*", "symfony/polyfill-php82": "*",
"symfony/polyfill-php83": "*", "symfony/polyfill-php83": "*",
"symfony/polyfill-uuid": "*" "symfony/polyfill-php84": "*"
}, },
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
@@ -116,7 +116,7 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "7.2.*", "require": "7.3.*",
"docker": true "docker": true
} }
} }

1930
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,8 @@ doctrine:
dir: '%kernel.project_dir%/src/Entity' dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity' prefix: 'App\Entity'
alias: App alias: App
controller_resolver:
auto_mapping: false
when@test: when@test:
doctrine: doctrine:

View File

@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true

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

@@ -1,4 +1,4 @@
when@dev: when@dev:
_errors: _errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml' resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error prefix: /_error

View File

@@ -1,8 +1,8 @@
when@dev: when@dev:
web_profiler_wdt: web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt prefix: /_wdt
web_profiler_profiler: web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler prefix: /_profiler

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250607154730 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE season DROP CONSTRAINT FK_F0E45BA96706D6B
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE season ADD CONSTRAINT FK_F0E45BA96706D6B FOREIGN KEY (active_quiz_id) REFERENCES quiz (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE season DROP CONSTRAINT fk_f0e45ba96706d6b
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE season ADD CONSTRAINT fk_f0e45ba96706d6b FOREIGN KEY (active_quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250607184525 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE elimination DROP CONSTRAINT FK_5947284F853CD175
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE elimination ADD CONSTRAINT FK_5947284F853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE given_answer DROP CONSTRAINT FK_9AC61A30853CD175
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE given_answer ADD CONSTRAINT FK_9AC61A30853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE given_answer DROP CONSTRAINT fk_9ac61a30853cd175
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE given_answer ADD CONSTRAINT fk_9ac61a30853cd175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE elimination DROP CONSTRAINT fk_5947284f853cd175
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE elimination ADD CONSTRAINT fk_5947284f853cd175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
}

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

@@ -7,3 +7,4 @@ parameters:
- public/ - public/
- src/ - src/
- tests/ - tests/
treatPhpDocTypesAsCertain: false

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,9 +7,9 @@ namespace App\Command;
use App\Repository\SeasonRepository; use App\Repository\SeasonRepository;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
@@ -18,29 +18,19 @@ use Symfony\Component\Console\Style\SymfonyStyle;
name: 'app:claim-season', name: 'app:claim-season',
description: 'Give a user owner rights on a season', description: 'Give a user owner rights on a season',
)] )]
class ClaimSeasonCommand extends Command readonly class ClaimSeasonCommand
{ {
public function __construct( public function __construct(private UserRepository $userRepository, private SeasonRepository $seasonRepository, private EntityManagerInterface $entityManager) {}
private readonly UserRepository $userRepository,
private readonly SeasonRepository $seasonRepository,
private readonly EntityManagerInterface $entityManager)
{
parent::__construct();
}
protected function configure(): void public function __invoke(
{ #[Argument]
$this string $seasonCode,
->addArgument('email', InputArgument::REQUIRED, 'The email of the user thats claims the season') #[Argument]
->addArgument('season', InputArgument::REQUIRED, 'The season to claim') string $email,
; InputInterface $input,
} OutputInterface $output,
): int {
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email');
$seasonCode = $input->getArgument('season');
try { try {
$season = $this->seasonRepository->findOneBy(['seasonCode' => $seasonCode]); $season = $this->seasonRepository->findOneBy(['seasonCode' => $seasonCode]);

View File

@@ -5,9 +5,9 @@ declare(strict_types=1);
namespace App\Command; namespace App\Command;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
@@ -16,25 +16,17 @@ use Symfony\Component\Console\Style\SymfonyStyle;
name: 'app:make-admin', name: 'app:make-admin',
description: 'Give a user the role admin', description: 'Give a user the role admin',
)] )]
class MakeAdminCommand extends Command readonly class MakeAdminCommand
{ {
public function __construct(private readonly UserRepository $userRepository) public function __construct(private UserRepository $userRepository) {}
{
parent::__construct();
}
protected function configure(): void public function __invoke(
{ #[Argument]
$this string $email,
->addArgument('email', InputArgument::REQUIRED, 'The email of the user to make admin') InputInterface $input,
; OutputInterface $output,
} ): int {
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email');
try { try {
$this->userRepository->makeAdmin($email); $this->userRepository->makeAdmin($email);
} catch (\InvalidArgumentException) { } catch (\InvalidArgumentException) {

View File

@@ -10,6 +10,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBase
abstract class AbstractController extends AbstractBaseController abstract class AbstractController extends AbstractBaseController
{ {
protected const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}'; protected const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
protected const string CANDIDATE_HASH_REGEX = '[\w\-=]+'; protected const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
#[\Override] #[\Override]

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

@@ -43,7 +43,7 @@ final class BackofficeController extends AbstractController
]); ]);
} }
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)] #[Route('/backoffice/season/add', name: 'app_backoffice_season_add', priority: 10)]
public function addSeason(Request $request, EntityManagerInterface $em): Response public function addSeason(Request $request, EntityManagerInterface $em): Response
{ {
$season = new Season(); $season = new Season();

View File

@@ -13,13 +13,14 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
final class PrepareEliminationController extends AbstractController final class PrepareEliminationController extends AbstractController
{ {
#[Route( #[Route(
'/backoffice/elimination/{seasonCode}/{quiz}/prepare', '/backoffice/season/{seasonCode:season}/quiz/{quiz}/elimination/prepare',
name: 'app_prepare_elimination', name: 'app_prepare_elimination',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX], requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
)] )]
public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
{ {
@@ -31,7 +32,7 @@ final class PrepareEliminationController extends AbstractController
#[Route( #[Route(
'/backoffice/elimination/{elimination}', '/backoffice/elimination/{elimination}',
name: 'app_prepare_elimination_view', name: 'app_prepare_elimination_view',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX], requirements: ['elimination' => Requirement::UUID],
)] )]
public function viewElimination(Elimination $elimination, Request $request, EntityManagerInterface $em): Response public function viewElimination(Elimination $elimination, Request $request, EntityManagerInterface $em): Response
{ {
@@ -39,9 +40,10 @@ final class PrepareEliminationController extends AbstractController
$elimination->updateFromInputBag($request->request); $elimination->updateFromInputBag($request->request);
$em->flush(); $em->flush();
if (true === $request->request->getBoolean('start')) { if ($request->request->getBoolean('start')) {
return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]); return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]);
} }
$this->addFlash('success', 'Elimination updated'); $this->addFlash('success', 'Elimination updated');
} }

View File

@@ -5,15 +5,24 @@ declare(strict_types=1);
namespace App\Controller\Backoffice; namespace App\Controller\Backoffice;
use App\Controller\AbstractController; use App\Controller\AbstractController;
use App\Entity\Candidate;
use App\Entity\Quiz; use App\Entity\Quiz;
use App\Entity\Season; use App\Entity\Season;
use App\Exception\ErrorClearingQuizException;
use App\Repository\CandidateRepository; use App\Repository\CandidateRepository;
use App\Repository\QuizCandidateRepository;
use App\Repository\QuizRepository;
use App\Security\Voter\SeasonVoter; use App\Security\Voter\SeasonVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController] #[AsController]
#[IsGranted('ROLE_USER')] #[IsGranted('ROLE_USER')]
@@ -21,10 +30,13 @@ class QuizController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly CandidateRepository $candidateRepository, private readonly CandidateRepository $candidateRepository,
private readonly TranslatorInterface $translator,
) {} ) {}
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz', #[Route(
requirements: ['seasonCode' => self::SEASON_CODE_REGEX], '/backoffice/season/{seasonCode:season}/quiz/{quiz}',
name: 'app_backoffice_quiz',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
)] )]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')] #[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season, Quiz $quiz): Response public function index(Season $season, Quiz $quiz): Response
@@ -36,11 +48,13 @@ class QuizController extends AbstractController
]); ]);
} }
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable', #[Route(
requirements: ['seasonCode' => self::SEASON_CODE_REGEX], '/backoffice/season/{seasonCode:season}/quiz/{quiz}/enable',
name: 'app_backoffice_enable',
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): Response public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): RedirectResponse
{ {
$season->setActiveQuiz($quiz); $season->setActiveQuiz($quiz);
$em->flush(); $em->flush();
@@ -51,4 +65,56 @@ class QuizController extends AbstractController
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]); return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
} }
#[Route(
'/backoffice/quiz/{quiz}/clear',
name: 'app_backoffice_quiz_clear',
requirements: ['quiz' => Requirement::UUID],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
public function clearQuiz(Quiz $quiz, QuizRepository $quizRepository): RedirectResponse
{
try {
$quizRepository->clearQuiz($quiz);
$this->addFlash('success', $this->translator->trans('Quiz cleared'));
} catch (ErrorClearingQuizException) {
$this->addFlash('error', $this->translator->trans('Error clearing quiz'));
}
return $this->redirectToRoute('app_backoffice_quiz', ['seasonCode' => $quiz->getSeason()->getSeasonCode(), 'quiz' => $quiz->getId()]);
}
#[Route(
'/backoffice/quiz/{quiz}/delete',
name: 'app_backoffice_quiz_delete',
requirements: ['quiz' => Requirement::UUID],
)]
#[IsGranted(SeasonVoter::DELETE, subject: 'quiz')]
public function deleteQuiz(Quiz $quiz, QuizRepository $quizRepository): RedirectResponse
{
$quizRepository->deleteQuiz($quiz);
$this->addFlash('success', $this->translator->trans('Quiz deleted'));
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $quiz->getSeason()->getSeasonCode()]);
}
#[Route(
'/backoffice/quiz/{quiz}/candidate/{candidate}/modify_correction',
name: 'app_backoffice_modify_correction',
requirements: ['quiz' => Requirement::UUID, 'candidate' => Requirement::UUID],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
public function modifyCorrection(Quiz $quiz, Candidate $candidate, QuizCandidateRepository $quizCandidateRepository, Request $request): RedirectResponse
{
if (!$request->isMethod('POST')) {
throw new MethodNotAllowedHttpException(['POST']);
}
$corrections = (float) $request->request->get('corrections');
$quizCandidateRepository->setCorrectionsForCandidate($quiz, $candidate, $corrections);
return $this->redirectToRoute('app_backoffice_quiz', ['seasonCode' => $quiz->getSeason()->getSeasonCode(), 'quiz' => $quiz->getId()]);
}
} }

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,24 +27,35 @@ 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 EntityManagerInterface $em, public function __construct(
private readonly TranslatorInterface $translator,
private readonly EntityManagerInterface $em,
) {} ) {}
#[Route( #[Route(
'/backoffice/season/{seasonCode}', '/backoffice/season/{seasonCode:season}',
name: 'app_backoffice_season', name: 'app_backoffice_season',
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,
]); ]);
} }
#[Route( #[Route(
'/backoffice/season/{seasonCode}/add_candidate', '/backoffice/season/{seasonCode:season}/add-candidate',
name: 'app_backoffice_add_candidates', name: 'app_backoffice_add_candidates',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX], requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
priority: 10, priority: 10,
@@ -56,8 +68,8 @@ 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("\r\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();
@@ -69,7 +81,7 @@ class SeasonController extends AbstractController
} }
#[Route( #[Route(
'/backoffice/season/{seasonCode}/add', '/backoffice/season/{seasonCode:season}/add-quiz',
name: 'app_backoffice_quiz_add', name: 'app_backoffice_quiz_add',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX], requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
priority: 10, priority: 10,

View File

@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -27,7 +28,7 @@ final class EliminationController extends AbstractController
{ {
public function __construct(private readonly TranslatorInterface $translator) {} public function __construct(private readonly TranslatorInterface $translator) {}
#[Route('/elimination/{elimination}', name: 'app_elimination')] #[Route('/elimination/{elimination}', name: 'app_elimination', requirements: ['elimination' => Requirement::UUID])]
#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')] #[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]
public function index(#[MapEntity] Elimination $elimination, Request $request): Response public function index(#[MapEntity] Elimination $elimination, Request $request): Response
{ {
@@ -47,7 +48,7 @@ final class EliminationController extends AbstractController
]); ]);
} }
#[Route('/elimination/{elimination}/{candidateHash}', name: 'app_elimination_candidate')] #[Route('/elimination/{elimination}/{candidateHash}', name: 'app_elimination_candidate', requirements: ['elimination' => Requirement::UUID, 'candidateHash' => self::CANDIDATE_HASH_REGEX])]
#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')] #[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]
public function candidateScreen(Elimination $elimination, string $candidateHash, CandidateRepository $candidateRepository): Response public function candidateScreen(Elimination $elimination, string $candidateHash, CandidateRepository $candidateRepository): Response
{ {

View File

@@ -20,11 +20,10 @@ use App\Repository\GivenAnswerRepository;
use App\Repository\QuestionRepository; use App\Repository\QuestionRepository;
use App\Repository\QuizCandidateRepository; use App\Repository\QuizCandidateRepository;
use App\Repository\SeasonRepository; use App\Repository\SeasonRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -54,10 +53,9 @@ final class QuizController extends AbstractController
return $this->render('quiz/select_season.html.twig', ['form' => $form]); return $this->render('quiz/select_season.html.twig', ['form' => $form]);
} }
#[Route(path: '/{seasonCode}', name: 'app_quiz_enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])] #[Route(path: '/{seasonCode:season}', name: 'app_quiz_enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
public function enterName( public function enterName(
Request $request, Request $request,
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
Season $season, Season $season,
): Response { ): Response {
$form = $this->createForm(EnterNameType::class); $form = $this->createForm(EnterNameType::class);
@@ -74,12 +72,11 @@ final class QuizController extends AbstractController
} }
#[Route( #[Route(
path: '/{seasonCode}/{nameHash}', path: '/{seasonCode:season}/{nameHash}',
name: 'app_quiz_quiz_page', name: 'app_quiz_quiz_page',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX], requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
)] )]
public function quizPage( public function quizPage(
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
Season $season, Season $season,
string $nameHash, string $nameHash,
Request $request, Request $request,
@@ -109,7 +106,7 @@ final class QuizController extends AbstractController
$answer = $answerRepository->findOneBy(['id' => $request->request->get('answer')]); $answer = $answerRepository->findOneBy(['id' => $request->request->get('answer')]);
if (!$answer instanceof Answer) { if (!$answer instanceof Answer) {
throw new BadRequestException('Invalid Answer ID'); throw new BadRequestHttpException('Invalid Answer ID');
} }
$givenAnswer = new GivenAnswer($candidate, $answer->getQuestion()->getQuiz(), $answer); $givenAnswer = new GivenAnswer($candidate, $answer->getQuestion()->getQuiz(), $answer);
@@ -128,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

@@ -9,11 +9,13 @@ use App\Form\RegistrationFormType;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\Security\EmailVerifier; use App\Security\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -29,6 +31,7 @@ final class RegistrationController extends AbstractController
UserPasswordHasherInterface $userPasswordHasher, UserPasswordHasherInterface $userPasswordHasher,
Security $security, Security $security,
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
LoggerInterface $logger,
): Response { ): Response {
$user = new User(); $user = new User();
$form = $this->createForm(RegistrationFormType::class, $user); $form = $this->createForm(RegistrationFormType::class, $user);
@@ -43,13 +46,17 @@ final class RegistrationController extends AbstractController
$entityManager->persist($user); $entityManager->persist($user);
$entityManager->flush(); $entityManager->flush();
try {
// generate a signed url and email it to the user // generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user, $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail()) new TemplatedEmail()
->to((string) $user->getEmail()) ->to((string) $user->getEmail())
->subject($this->translator->trans('Please Confirm your Email')) ->subject($this->translator->trans('Please Confirm your Email'))
->htmlTemplate('backoffice/registration/confirmation_email.html.twig'), ->htmlTemplate('backoffice/registration/confirmation_email.html.twig'),
); );
} catch (TransportExceptionInterface $e) {
$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

@@ -44,18 +44,18 @@ class KrtekFixtures extends Fixture
private function createQuiz1(Season $season): Quiz private function createQuiz1(Season $season): Quiz
{ {
return (new Quiz()) return new Quiz()
->setName('Quiz 1') ->setName('Quiz 1')
->setSeason($season) ->setSeason($season)
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Is de Krtek een man of een vrouw?') ->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Vrouw', true)) ->addAnswer(new Answer('Vrouw', true))
->addAnswer(new Answer('Man')) ->addAnswer(new Answer('Man'))
->setOrdering(1), ->setOrdering(1),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Hoeveel broers heeft de Krtek?') ->setQuestion('Hoeveel broers heeft de Krtek?')
->addAnswer(new Answer('Geen', true)) ->addAnswer(new Answer('Geen', true))
->addAnswer(new Answer('1')) ->addAnswer(new Answer('1'))
@@ -63,7 +63,7 @@ class KrtekFixtures extends Fixture
->setOrdering(2), ->setOrdering(2),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Wat is de lievelingsfeestdag van de Krtek?') ->setQuestion('Wat is de lievelingsfeestdag van de Krtek?')
->addAnswer(new Answer('Geen')) ->addAnswer(new Answer('Geen'))
->addAnswer(new Answer('Diens eigen verjaardag')) ->addAnswer(new Answer('Diens eigen verjaardag'))
@@ -73,13 +73,13 @@ class KrtekFixtures extends Fixture
->setOrdering(3), ->setOrdering(3),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?') ->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?')
->addAnswer(new Answer('Met het OV', true)) ->addAnswer(new Answer('Met het OV', true))
->addAnswer(new Answer('Met de auto')) ->addAnswer(new Answer('Met de auto'))
->setOrdering(4), ->setOrdering(4),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Met wie keek de Krtek video bij binnenkomst?') ->setQuestion('Met wie keek de Krtek video bij binnenkomst?')
->addAnswer(new Answer('Claudia')) ->addAnswer(new Answer('Claudia'))
->addAnswer(new Answer('Eelco')) ->addAnswer(new Answer('Eelco'))
@@ -97,7 +97,7 @@ class KrtekFixtures extends Fixture
->setOrdering(5), ->setOrdering(5),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Welk advies zou de Krtek zichzelf als kind geven?') ->setQuestion('Welk advies zou de Krtek zichzelf als kind geven?')
->addAnswer(new Answer('Geef je vader een knuffel.')) ->addAnswer(new Answer('Geef je vader een knuffel.'))
->addAnswer(new Answer('Trek je wat minder aan van anderen.')) ->addAnswer(new Answer('Trek je wat minder aan van anderen.'))
@@ -110,7 +110,7 @@ class KrtekFixtures extends Fixture
->setOrdering(6), ->setOrdering(6),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Wat voor soort schoenen droeg de Krtek bij het diner?') ->setQuestion('Wat voor soort schoenen droeg de Krtek bij het diner?')
->addAnswer(new Answer('Sneakers')) ->addAnswer(new Answer('Sneakers'))
->addAnswer(new Answer('Wandel-/bergschoenen', true)) ->addAnswer(new Answer('Wandel-/bergschoenen', true))
@@ -121,7 +121,7 @@ class KrtekFixtures extends Fixture
->setOrdering(7), ->setOrdering(7),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Met welk vervoersmiddel reist de Krtek het liefste?') ->setQuestion('Met welk vervoersmiddel reist de Krtek het liefste?')
->addAnswer(new Answer('Fiets', true)) ->addAnswer(new Answer('Fiets', true))
->addAnswer(new Answer('Auto')) ->addAnswer(new Answer('Auto'))
@@ -129,14 +129,14 @@ class KrtekFixtures extends Fixture
->setOrdering(8), ->setOrdering(8),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Heeft de Krtek een eigen auto?') ->setQuestion('Heeft de Krtek een eigen auto?')
->addAnswer(new Answer('Ja')) ->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true)) ->addAnswer(new Answer('Nee', true))
->setOrdering(9), ->setOrdering(9),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Van wie is de quote die de Krtek gepakt heeft') ->setQuestion('Van wie is de quote die de Krtek gepakt heeft')
->addAnswer(new Answer('Karen')) ->addAnswer(new Answer('Karen'))
->addAnswer(new Answer('Gilles de Coster')) ->addAnswer(new Answer('Gilles de Coster'))
@@ -156,14 +156,14 @@ class KrtekFixtures extends Fixture
->setOrdering(10), ->setOrdering(10),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topitos uit iemands rugzak stelen om te kunnen winnen?') ->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topitos uit iemands rugzak stelen om te kunnen winnen?')
->addAnswer(new Answer('Ja')) ->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true)) ->addAnswer(new Answer('Nee', true))
->setOrdering(11), ->setOrdering(11),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('In wat voor bed slaapt de Krtek dit weekend?') ->setQuestion('In wat voor bed slaapt de Krtek dit weekend?')
->addAnswer(new Answer('Éénpersoons, losstaand bed')) ->addAnswer(new Answer('Éénpersoons, losstaand bed'))
->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true)) ->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true))
@@ -171,7 +171,7 @@ class KrtekFixtures extends Fixture
->setOrdering(12), ->setOrdering(12),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Hoeveel jaar heeft de Krtek gedaan over de middelbare school?') ->setQuestion('Hoeveel jaar heeft de Krtek gedaan over de middelbare school?')
->addAnswer(new Answer('5')) ->addAnswer(new Answer('5'))
->addAnswer(new Answer('6', true)) ->addAnswer(new Answer('6', true))
@@ -180,14 +180,14 @@ class KrtekFixtures extends Fixture
->setOrdering(13), ->setOrdering(13),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Waar zat de Krtek aan tafel bij het diner?') ->setQuestion('Waar zat de Krtek aan tafel bij het diner?')
->addAnswer(new Answer('Met de rug naar de accommodatie')) ->addAnswer(new Answer('Met de rug naar de accommodatie'))
->addAnswer(new Answer('Met de rug naar de buitenmuur', true)) ->addAnswer(new Answer('Met de rug naar de buitenmuur', true))
->setOrdering(14), ->setOrdering(14),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Wie is de Krtek?') ->setQuestion('Wie is de Krtek?')
->addAnswer(new Answer('Claudia', true)) ->addAnswer(new Answer('Claudia', true))
->addAnswer(new Answer('Eelco')) ->addAnswer(new Answer('Eelco'))
@@ -209,18 +209,18 @@ class KrtekFixtures extends Fixture
private function createQuiz2(Season $season): Quiz private function createQuiz2(Season $season): Quiz
{ {
return (new Quiz()) return new Quiz()
->setName('Quiz 2') ->setName('Quiz 2')
->setSeason($season) ->setSeason($season)
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Is de Krtek een man of een vrouw?') ->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Man')) ->addAnswer(new Answer('Man'))
->addAnswer(new Answer('Vrouw', true)) ->addAnswer(new Answer('Vrouw', true))
->setOrdering(1), ->setOrdering(1),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Heeft de Krtek dieetwensen of allergieën?') ->setQuestion('Heeft de Krtek dieetwensen of allergieën?')
->addAnswer(new Answer('nee')) ->addAnswer(new Answer('nee'))
->addAnswer(new Answer('De Krtek is vegetariër', true)) ->addAnswer(new Answer('De Krtek is vegetariër', true))
@@ -232,7 +232,7 @@ class KrtekFixtures extends Fixture
->setOrdering(2), ->setOrdering(2),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Hoe heet het huisdier/de huisdieren van de Krtek?') ->setQuestion('Hoe heet het huisdier/de huisdieren van de Krtek?')
->addAnswer(new Answer('Amy, Karel en Floyd')) ->addAnswer(new Answer('Amy, Karel en Floyd'))
->addAnswer(new Answer('Flip en Majoor')) ->addAnswer(new Answer('Flip en Majoor'))
@@ -244,7 +244,7 @@ class KrtekFixtures extends Fixture
->setOrdering(3), ->setOrdering(3),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Wat dronk de Krtek deze ochtend bij het ontbijt?') ->setQuestion('Wat dronk de Krtek deze ochtend bij het ontbijt?')
->addAnswer(new Answer('Koffie')) ->addAnswer(new Answer('Koffie'))
->addAnswer(new Answer('Thee')) ->addAnswer(new Answer('Thee'))
@@ -255,7 +255,7 @@ class KrtekFixtures extends Fixture
->setOrdering(4), ->setOrdering(4),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Waar ging de eerste vakantie die de Krtek zich nog herinnert heen?') ->setQuestion('Waar ging de eerste vakantie die de Krtek zich nog herinnert heen?')
->addAnswer(new Answer('Denemarken')) ->addAnswer(new Answer('Denemarken'))
->addAnswer(new Answer('Drenthe')) ->addAnswer(new Answer('Drenthe'))
@@ -267,7 +267,7 @@ class KrtekFixtures extends Fixture
->setOrdering(5), ->setOrdering(5),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Met welk groepje ging de Krtek als eerste het Douanespel in?') ->setQuestion('Met welk groepje ging de Krtek als eerste het Douanespel in?')
->addAnswer(new Answer('Het eerste groepje', true)) ->addAnswer(new Answer('Het eerste groepje', true))
->addAnswer(new Answer('Het tweede groepje')) ->addAnswer(new Answer('Het tweede groepje'))
@@ -277,7 +277,7 @@ class KrtekFixtures extends Fixture
->setOrdering(6), ->setOrdering(6),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Gelooft de Krtek ergens in?') ->setQuestion('Gelooft de Krtek ergens in?')
->addAnswer(new Answer('Nee')) ->addAnswer(new Answer('Nee'))
->addAnswer(new Answer('Het universum', true)) ->addAnswer(new Answer('Het universum', true))
@@ -286,14 +286,14 @@ class KrtekFixtures extends Fixture
->setOrdering(7), ->setOrdering(7),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?') ->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?')
->addAnswer(new Answer('Ja', true)) ->addAnswer(new Answer('Ja', true))
->addAnswer(new Answer('Nee')) ->addAnswer(new Answer('Nee'))
->setOrdering(8), ->setOrdering(8),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Hoe laat ging de Krtek gisteravond naar bed?') ->setQuestion('Hoe laat ging de Krtek gisteravond naar bed?')
->addAnswer(new Answer('Tussen 0:00 en 0:59 uur')) ->addAnswer(new Answer('Tussen 0:00 en 0:59 uur'))
->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true)) ->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true))
@@ -302,7 +302,7 @@ class KrtekFixtures extends Fixture
->setOrdering(9), ->setOrdering(9),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Hoeveel batterijen heeft de Krtek naar het bord gebracht bij het douanespel?') ->setQuestion('Hoeveel batterijen heeft de Krtek naar het bord gebracht bij het douanespel?')
->addAnswer(new Answer('1')) ->addAnswer(new Answer('1'))
->addAnswer(new Answer('2')) ->addAnswer(new Answer('2'))
@@ -311,7 +311,7 @@ class KrtekFixtures extends Fixture
->setOrdering(10), ->setOrdering(10),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Wat keek de Krtek als kind graag op TV?') ->setQuestion('Wat keek de Krtek als kind graag op TV?')
->addAnswer(new Answer('Digimon', true)) ->addAnswer(new Answer('Digimon', true))
->addAnswer(new Answer('Floris')) ->addAnswer(new Answer('Floris'))
@@ -322,7 +322,7 @@ class KrtekFixtures extends Fixture
->setOrdering(11), ->setOrdering(11),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Waarin zat op de heenreis de bagage van de Krtek (voornamelijk)?') ->setQuestion('Waarin zat op de heenreis de bagage van de Krtek (voornamelijk)?')
->addAnswer(new Answer('In koffer(s)', true)) ->addAnswer(new Answer('In koffer(s)', true))
->addAnswer(new Answer('In losse tas(sen)')) ->addAnswer(new Answer('In losse tas(sen)'))
@@ -330,7 +330,7 @@ class KrtekFixtures extends Fixture
->setOrdering(12), ->setOrdering(12),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Van welk geluid gaan de haren van de Krtek overeind staan?') ->setQuestion('Van welk geluid gaan de haren van de Krtek overeind staan?')
->addAnswer(new Answer('Een vork die door een metalen pan krast ')) ->addAnswer(new Answer('Een vork die door een metalen pan krast '))
->addAnswer(new Answer('Smakkende mensen')) ->addAnswer(new Answer('Smakkende mensen'))
@@ -343,14 +343,14 @@ class KrtekFixtures extends Fixture
->setOrdering(13), ->setOrdering(13),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Wilde de Krtek penningmeester worden?') ->setQuestion('Wilde de Krtek penningmeester worden?')
->addAnswer(new Answer('Ja')) ->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true)) ->addAnswer(new Answer('Nee', true))
->setOrdering(14), ->setOrdering(14),
) )
->addQuestion((new Question()) ->addQuestion(new Question()
->setQuestion('Wie is de Krtek?') ->setQuestion('Wie is de Krtek?')
->addAnswer(new Answer('Claudia', true)) ->addAnswer(new Answer('Claudia', true))
->addAnswer(new Answer('Eelco')) ->addAnswer(new Answer('Eelco'))

View File

@@ -18,6 +18,7 @@ use Symfony\Component\Uid\Uuid;
class Elimination class Elimination
{ {
public const string SCREEN_GREEN = 'green'; public const string SCREEN_GREEN = 'green';
public const string SCREEN_RED = 'red'; public const string SCREEN_RED = 'red';
#[ORM\Id] #[ORM\Id]
@@ -35,7 +36,7 @@ class Elimination
public function __construct( public function __construct(
#[ORM\ManyToOne(inversedBy: 'eliminations')] #[ORM\ManyToOne(inversedBy: 'eliminations')]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Quiz $quiz, private Quiz $quiz,
) {} ) {}
@@ -66,7 +67,7 @@ class Elimination
/** @param InputBag<bool|float|int|string> $inputBag */ /** @param InputBag<bool|float|int|string> $inputBag */
public function updateFromInputBag(InputBag $inputBag): self public function updateFromInputBag(InputBag $inputBag): self
{ {
foreach ($this->data as $name => $screenColour) { foreach (array_keys($this->data) as $name) {
$newColour = $inputBag->get('colour-'.mb_strtolower($name)); $newColour = $inputBag->get('colour-'.mb_strtolower($name));
if (\is_string($newColour)) { if (\is_string($newColour)) {
$this->data[$name] = $inputBag->get('colour-'.mb_strtolower($name)); $this->data[$name] = $inputBag->get('colour-'.mb_strtolower($name));

View File

@@ -31,7 +31,7 @@ class GivenAnswer
private Candidate $candidate, private Candidate $candidate,
#[ORM\ManyToOne] #[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Quiz $quiz, private Quiz $quiz,
#[ORM\ManyToOne(inversedBy: 'givenAnswers')] #[ORM\ManyToOne(inversedBy: 'givenAnswers')]

View File

@@ -43,10 +43,16 @@ class Season
private Collection $owners; private Collection $owners;
#[ORM\ManyToOne] #[ORM\ManyToOne]
#[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();
@@ -165,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

@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Exception;
class ErrorClearingQuizException extends \Exception {}

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',
'mimeTypes' => [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
], ], mimeTypesMessage: $this->translator->trans('Please upload a valid XLSX file')),
'mimeTypesMessage' => $this->translator->trans('Please upload a valid XLSX file'),
]),
], ],
]) ])
; ;

View File

@@ -10,7 +10,7 @@ class Base64
{ {
public static function base64UrlEncode(string $input): string public static function base64UrlEncode(string $input): string
{ {
return rtrim(strtr(base64_encode($input), '+/', '-_'), '='); return mb_rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
} }
/** @throws UrlException */ /** @throws UrlException */

View File

@@ -16,7 +16,7 @@ use Symfony\Component\Uid\Uuid;
/** /**
* @extends ServiceEntityRepository<Candidate> * @extends ServiceEntityRepository<Candidate>
* *
* @phpstan-type Result array{id: Uuid, name: string, 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> * @phpstan-type ResultList list<Result>
*/ */
class CandidateRepository extends ServiceEntityRepository class CandidateRepository extends ServiceEntityRepository
@@ -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,52 +56,22 @@ class CandidateRepository extends ServiceEntityRepository
/** @return ResultList */ /** @return ResultList */
public function getScores(Quiz $quiz): array public function getScores(Quiz $quiz): array
{ {
$scoreQb = $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') select
->join('c.givenAnswers', 'ga') c.id,
->join('ga.answer', 'a') c.name,
->where('ga.quiz = :quiz') sum(case when a.isRightAnswer = true then 1 else 0 end) as correct,
->groupBy('c.id') qc.corrections,
->setParameter('quiz', $quiz); max(ga.created) - qc.created as time,
(sum(case when a.isRightAnswer = true then 1 else 0 end) + qc.corrections) as score
$startTimeCorrectionQb = $this->createQueryBuilder('c', 'c.id') from App\Entity\Candidate c
->select('c.id', 'qc.corrections', 'max(ga.created) - qc.created as time') join c.givenAnswers ga
->join('c.quizData', 'qc') join ga.answer a
->join('c.givenAnswers', 'ga') join c.quizData qc
->where('qc.quiz = :quiz') where qc.quiz = :quiz and ga.quiz = :quiz
->groupBy('ga.quiz', 'c.id', 'qc.id') group by ga.quiz, c.id, qc.id
->setParameter('quiz', $quiz); order by score desc, time asc
DQL
$merged = array_merge_recursive( )->setParameter('quiz', $quiz)->getResult();
$scoreQb->getQuery()->getArrayResult(),
$startTimeCorrectionQb->getQuery()->getArrayResult(),
);
return $this->sortResults($this->calculateScore($merged));
}
/**
* @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'] ?? 0.0),
], $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

@@ -33,4 +33,15 @@ class QuizCandidateRepository extends ServiceEntityRepository
return true; return true;
} }
public function setCorrectionsForCandidate(Quiz $quiz, Candidate $candidate, float $corrections): void
{
$quizCandidate = $this->findOneBy(['candidate' => $candidate, 'quiz' => $quiz]);
if (!$quizCandidate instanceof QuizCandidate) {
throw new \InvalidArgumentException('Quiz candidate not found');
}
$quizCandidate->setCorrections($corrections);
$this->getEntityManager()->flush();
}
} }

View File

@@ -4,17 +4,59 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Entity\Elimination;
use App\Entity\GivenAnswer;
use App\Entity\Quiz; use App\Entity\Quiz;
use App\Entity\QuizCandidate;
use App\Exception\ErrorClearingQuizException;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
/** /**
* @extends ServiceEntityRepository<Quiz> * @extends ServiceEntityRepository<Quiz>
*/ */
class QuizRepository extends ServiceEntityRepository class QuizRepository extends ServiceEntityRepository
{ {
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry, private readonly LoggerInterface $logger)
{ {
parent::__construct($registry, Quiz::class); parent::__construct($registry, Quiz::class);
} }
/** @throws ErrorClearingQuizException */
public function clearQuiz(Quiz $quiz): void
{
$em = $this->getEntityManager();
$em->beginTransaction();
try {
$em->createQueryBuilder()
->delete()->from(QuizCandidate::class, 'qc')
->where('qc.quiz = :quiz')
->setParameter('quiz', $quiz)
->getQuery()->execute();
$em->createQueryBuilder()
->delete()->from(GivenAnswer::class, 'ga')
->where('ga.quiz = :quiz')
->setParameter('quiz', $quiz)
->getQuery()->execute();
$em->createQueryBuilder()
->delete()->from(Elimination::class, 'e')
->where('e.quiz = :quiz')
->setParameter('quiz', $quiz)
->getQuery()->execute();
} catch (\Throwable $throwable) {
$this->logger->error($throwable->getMessage());
$em->rollback();
throw new ErrorClearingQuizException(previous: $throwable);
}
$em->commit();
}
public function deleteQuiz(Quiz $quiz): void
{
$this->getEntityManager()->remove($quiz);
$this->getEntityManager()->flush();
}
} }

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

@@ -8,18 +8,19 @@ use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface; use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
class EmailVerifier readonly class EmailVerifier
{ {
public function __construct( public function __construct(
private readonly VerifyEmailHelperInterface $verifyEmailHelper, private VerifyEmailHelperInterface $verifyEmailHelper,
private readonly MailerInterface $mailer, private MailerInterface $mailer,
private readonly EntityManagerInterface $entityManager, private EntityManagerInterface $entityManager,
) {} ) {}
/** @throws TransportExceptionInterface */
public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $email): void public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $email): void
{ {
$signatureComponents = $this->verifyEmailHelper->generateSignature( $signatureComponents = $this->verifyEmailHelper->generateSignature(
@@ -39,7 +40,6 @@ class EmailVerifier
$this->mailer->send($email); $this->mailer->send($email);
} }
/** @throws VerifyEmailExceptionInterface */
public function handleEmailConfirmation(Request $request, User $user): void public function handleEmailConfirmation(Request $request, User $user): void
{ {
$this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), (string) $user->getEmail()); $this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), (string) $user->getEmail());

View File

@@ -4,10 +4,15 @@ declare(strict_types=1);
namespace App\Security\Voter; namespace App\Security\Voter;
use App\Entity\Answer;
use App\Entity\Candidate;
use App\Entity\Elimination; use App\Entity\Elimination;
use App\Entity\Question;
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> */
@@ -22,11 +27,18 @@ final class SeasonVoter extends Voter
protected function supports(string $attribute, mixed $subject): bool protected function supports(string $attribute, mixed $subject): bool
{ {
return \in_array($attribute, [self::EDIT, self::DELETE, self::ELIMINATION], true) return \in_array($attribute, [self::EDIT, self::DELETE, self::ELIMINATION], true)
&& ($subject instanceof Season || $subject instanceof Elimination); && (
$subject instanceof Season
|| $subject instanceof Elimination
|| $subject instanceof Quiz
|| $subject instanceof Candidate
|| $subject instanceof Answer
|| $subject instanceof Question
);
} }
/** @param Season|Elimination $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) {
@@ -37,7 +49,24 @@ final class SeasonVoter extends Voter
return true; return true;
} }
$season = $subject instanceof Season ? $subject : $subject->getQuiz()->getSeason(); switch (true) {
case $subject instanceof Answer:
$season = $subject->getQuestion()->getQuiz()->getSeason();
break;
case $subject instanceof Elimination:
case $subject instanceof Question:
$season = $subject->getQuiz()->getSeason();
break;
case $subject instanceof Candidate:
case $subject instanceof Quiz:
$season = $subject->getSeason();
break;
case $subject instanceof Season:
$season = $subject;
break;
default:
return false;
}
return match ($attribute) { return match ($attribute) {
self::EDIT, self::DELETE, self::ELIMINATION => $season->isOwner($user), self::EDIT, self::DELETE, self::ELIMINATION => $season->isOwner($user),

View File

@@ -8,8 +8,9 @@ use App\Entity\Answer;
use App\Entity\Question; use App\Entity\Question;
use App\Entity\Quiz; use App\Entity\Quiz;
use App\Exception\SpreadsheetDataException; use App\Exception\SpreadsheetDataException;
use PhpOffice\PhpSpreadsheet\Reader;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Writer;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
class QuizSpreadsheetService class QuizSpreadsheetService
@@ -64,7 +65,7 @@ class QuizSpreadsheetService
private function readSheet(File $file): Spreadsheet private function readSheet(File $file): Spreadsheet
{ {
return (new \PhpOffice\PhpSpreadsheet\Reader\Xlsx())->setReadDataOnly(true)->load($file->getRealPath()); return new Reader\Xlsx()->setReadDataOnly(true)->load($file->getRealPath());
} }
/** /**
@@ -72,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 = [];
@@ -109,15 +110,13 @@ 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 {}
private function toXlsx(Spreadsheet $spreadsheet): \Closure private function toXlsx(Spreadsheet $spreadsheet): \Closure
{ {
$writer = new Xlsx($spreadsheet); $writer = new Writer\Xlsx($spreadsheet);
return static fn () => $writer->save('php://output'); return static fn () => $writer->save('php://output');
} }

View File

@@ -213,6 +213,18 @@
"tests/bootstrap.php" "tests/bootstrap.php"
] ]
}, },
"symfony/property-info": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
},
"files": [
"config/packages/property_info.yaml"
]
},
"symfony/routing": { "symfony/routing": {
"version": "7.2", "version": "7.2",
"recipe": { "recipe": {

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">
@@ -11,6 +11,7 @@
{{ 'Add'|trans }} {{ 'Add'|trans }}
</a> </a>
</div> </div>
{% if seasons %}
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@@ -45,9 +46,10 @@
<a href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a> <a href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
</td> </td>
</tr> </tr>
{% else %}
EMPTY
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %}
{{ 'You have no seasons yet.'|trans }}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,14 +1,22 @@
{% 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"> <div class="py-2 btn-group" data-controller="bo--quiz">
<a class="btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}" <a class="btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}"
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a> href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
{% if quiz is same as (season.activeQuiz) %} {% if quiz is same as (season.activeQuiz) %}
<a class="btn btn-secondary" <a class="btn btn-secondary"
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a> href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a>
{% endif %} {% endif %}
<button class="btn btn-danger" data-action="click->bo--quiz#clearQuiz">
{{ 'Clear quiz...'|trans }}
</button>
<button class="btn btn-danger" data-action="click->bo--quiz#deleteQuiz">
{{ 'Delete Quiz...'|trans }}
</button>
</div> </div>
<div id="questions"> <div id="questions">
@@ -45,7 +53,7 @@
</div> </div>
</div> </div>
{% else %} {% else %}
EMPTY {{ 'EMPTY'|trans }}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -74,10 +82,10 @@
<thead> <thead>
<tr> <tr>
<th scope="col">{{ 'Candidate'|trans }}</th> <th scope="col">{{ 'Candidate'|trans }}</th>
<th scope="col">{{ 'Correct Answers'|trans }}</th> <th style="width: 15%" scope="col">{{ 'Correct Answers'|trans }}</th>
<th scope="col">{{ 'Corrections'|trans }}</th> <th style="width: 20%" scope="col">{{ 'Corrections'|trans }}</th>
<th scope="col">{{ 'Score'|trans }}</th> <th style="width: 10%" scope="col">{{ 'Score'|trans }}</th>
<th scope="col">{{ 'Time'|trans }}</th> <th style="width: 20%" scope="col">{{ 'Time'|trans }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -85,7 +93,21 @@
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}"> <tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
<td>{{ candidate.name }}</td> <td>{{ candidate.name }}</td>
<td>{{ candidate.correct|default('0') }}</td> <td>{{ candidate.correct|default('0') }}</td>
<td>{{ candidate.corrections|default('0') }}</td> <td>
<form method="post"
action="{{ path('app_backoffice_modify_correction', {quiz: quiz.id, candidate: candidate.id}) }}">
<div class="row">
<div class="col-8">
<input class="form-control form-control-sm" type="number"
value="{{ candidate.corrections }}" step="0.5"
name="corrections">
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary" type="submit">{{ 'Save'|trans }}</button>
</div>
</div>
</form>
</td>
<td>{{ candidate.score|default('x') }}</td> <td>{{ candidate.score|default('x') }}</td>
<td>{{ candidate.time }}</td> <td>{{ candidate.time }}</td>
</tr> </tr>
@@ -97,16 +119,48 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function () {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
});
</script>
{% endblock javascripts %}
{% block title %}
{# Modal Clear #}
<div class="modal fade" id="clearQuizModal" data-bs-backdrop="static"
tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<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>
</div>
<div class="modal-body">
{{ 'Are you sure you want to clear all the results? This will also delete al the eliminations.'|trans }}
</div>
<div class="modal-footer">
<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'|trans }}</a>
</div>
</div>
</div>
</div>
{# Modal Delete #}
<div class="modal fade" id="deleteQuizModal" data-bs-backdrop="static"
tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<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>
</div>
<div class="modal-body">
{{ 'Are you sure you want to delete this quiz?'|trans }}
</div>
<div class="modal-footer">
<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'|trans }}</a>
</div>
</div>
</div>
</div>
{% endblock %} {% 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

@@ -5,7 +5,7 @@
class="elimination-screen" id="{{ colour }}" class="elimination-screen" id="{{ colour }}"
alt="Screen with colour {{ colour }}" alt="Screen with colour {{ colour }}"
data-controller="elimination" data-controller="elimination"
data-action="click->elimination#next" data-action="click->elimination#next keydown@document->elimination#next"
tabindex="0" tabindex="0"
> >

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') }}">
{% if season.settings.confirmAnswers == false %}
{% for answer in question.answers %} {% for answer in question.answers %}
<div> <div class="py-2">
<button class="btn btn-outline-success" <button class="btn btn-outline-success"
type="submit" type="submit"
name="answer" name="answer"
value="{{ answer.id }}">{{ answer.text }}</button> value="{{ answer.id }}">{{ answer.text }}</button>
</div> </div>
{% else %}
Weirdly enough this question has no answers...
{% endfor %} {% endfor %}
{% else %}
{% for answer in question.answers %}
<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

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Tests\Security\Voter;
use App\Entity\Answer;
use App\Entity\Candidate;
use App\Entity\Elimination;
use App\Entity\Question;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Entity\User;
use App\Security\Voter\SeasonVoter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\Stub;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
final class SeasonVoterTest extends TestCase
{
private SeasonVoter $seasonVoter;
private TokenInterface&Stub $token;
protected function setUp(): void
{
$this->seasonVoter = new SeasonVoter();
$this->token = $this->createStub(TokenInterface::class);
$user = $this->createStub(User::class);
$this->token->method('getUser')->willReturn($user);
}
#[DataProvider('typesProvider')]
public function testWithTypes(mixed $subject): void
{
$this->assertSame(VoterInterface::ACCESS_GRANTED, $this->seasonVoter->vote($this->token, $subject, ['SEASON_EDIT']));
}
public function testNotOwnerWillReturnDenied(): void
{
$season = self::createStub(Season::class);
$season->method('isOwner')->willReturn(false);
$this->assertSame(VoterInterface::ACCESS_DENIED, $this->seasonVoter->vote($this->token, $season, ['SEASON_EDIT']));
}
public static function typesProvider(): \Generator
{
$season = self::createStub(Season::class);
$season->method('isOwner')->willReturn(true);
$quiz = self::createStub(Quiz::class);
$quiz->method('getSeason')->willReturn($season);
$elimination = self::createStub(Elimination::class);
$elimination->method('getQuiz')->willReturn($quiz);
$candidate = self::createStub(Candidate::class);
$candidate->method('getSeason')->willReturn($season);
$question = self::createStub(Question::class);
$question->method('getQuiz')->willReturn($quiz);
$answer = self::createStub(Answer::class);
$answer->method('getQuestion')->willReturn($question);
yield 'Season' => [$season];
yield 'Elimination' => [$elimination];
yield 'Quiz' => [$quiz];
yield 'Candidate' => [$candidate];
yield 'Question' => [$question];
yield 'Answer' => [$answer];
}
}

View File

@@ -6,7 +6,7 @@ use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php'; require dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); new Dotenv()->bootEnv(dirname(__DIR__).'/.env');
if ($_SERVER['APP_DEBUG']) { if ($_SERVER['APP_DEBUG']) {
umask(0000); umask(0000);

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>
@@ -49,6 +57,10 @@
<source>Candidates</source> <source>Candidates</source>
<target>Kandidaten</target> <target>Kandidaten</target>
</trans-unit> </trans-unit>
<trans-unit id="FNY513f" resname="Clear quiz...">
<source>Clear quiz...</source>
<target>Test leegmaken...</target>
</trans-unit>
<trans-unit id="sFpB4C2" resname="Correct Answers"> <trans-unit id="sFpB4C2" resname="Correct Answers">
<source>Correct Answers</source> <source>Correct Answers</source>
<target>Goede antwoorden</target> <target>Goede antwoorden</target>
@@ -77,10 +89,18 @@
<source>Deactivate Quiz</source> <source>Deactivate Quiz</source>
<target>Deactiveer test</target> <target>Deactiveer test</target>
</trans-unit> </trans-unit>
<trans-unit id="p9GNNI3" resname="Delete Quiz...">
<source>Delete Quiz...</source>
<target>Test verwijderen...</target>
</trans-unit>
<trans-unit id="R9yHzHv" resname="Download Template"> <trans-unit id="R9yHzHv" resname="Download Template">
<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>
@@ -91,7 +111,11 @@
</trans-unit> </trans-unit>
<trans-unit id="RnI7jJT" resname="Enter your name"> <trans-unit id="RnI7jJT" resname="Enter your name">
<source>Enter your name</source> <source>Enter your name</source>
<target>Voor je naam in</target> <target>Voer je naam in</target>
</trans-unit>
<trans-unit id="HNMwvRn" resname="Error clearing quiz">
<source>Error clearing quiz</source>
<target>Fout bij leegmaken test</target>
</trans-unit> </trans-unit>
<trans-unit id="OGiIhMH" resname="Green"> <trans-unit id="OGiIhMH" resname="Green">
<source>Green</source> <source>Green</source>
@@ -125,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>
@@ -145,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>
@@ -177,10 +213,18 @@
<source>Quiz Added!</source> <source>Quiz Added!</source>
<target>Test toegevoegd!</target> <target>Test toegevoegd!</target>
</trans-unit> </trans-unit>
<trans-unit id="vXN8b2w" resname="Quiz cleared">
<source>Quiz cleared</source>
<target>Test leeggemaakt</target>
</trans-unit>
<trans-unit id="LbVe.2c" resname="Quiz completed"> <trans-unit id="LbVe.2c" resname="Quiz completed">
<source>Quiz completed</source> <source>Quiz completed</source>
<target>Test voltooid</target> <target>Test voltooid</target>
</trans-unit> </trans-unit>
<trans-unit id="XdfTTMD" resname="Quiz deleted">
<source>Quiz deleted</source>
<target>Test verwijderd</target>
</trans-unit>
<trans-unit id="frxoIkW" resname="Quiz name"> <trans-unit id="frxoIkW" resname="Quiz name">
<source>Quiz name</source> <source>Quiz name</source>
<target>Testnaam</target> <target>Testnaam</target>
@@ -233,14 +277,14 @@
<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>
</trans-unit> </trans-unit>
<trans-unit id="2QO7aYC" resname="Start Elimination">
<source>Start Elimination</source>
<target>Start eliminatie</target>
</trans-unit>
<trans-unit id="9m8DOBg" resname="Submit"> <trans-unit id="9m8DOBg" resname="Submit">
<source>Submit</source> <source>Submit</source>
<target>Verstuur</target> <target>Verstuur</target>
@@ -253,10 +297,22 @@
<source>There are no answers for this question</source> <source>There are no answers for this question</source>
<target>Er zijn geen antwoorden voor deze vraag</target> <target>Er zijn geen antwoorden voor deze vraag</target>
</trans-unit> </trans-unit>
<trans-unit id=".LrcTyU" resname="There is no active quiz">
<source>There is no active quiz</source>
<target>Er is geen test actief</target>
</trans-unit>
<trans-unit id="Dptvysv" resname="Time"> <trans-unit id="Dptvysv" resname="Time">
<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.">
<source>You have no seasons yet.</source>
<target>Je hebt nog geen seizoenen.</target>
</trans-unit>
<trans-unit id="vVQAP9A" resname="Your Seasons"> <trans-unit id="vVQAP9A" resname="Your Seasons">
<source>Your Seasons</source> <source>Your Seasons</source>
<target>Jouw seizoenen</target> <target>Jouw seizoenen</target>