mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-07 13:14:20 +01:00
Compare commits
30 Commits
3e724ff1fb
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
cfb69c8dab
|
|||
|
35ec71302b
|
|||
|
b7a570928a
|
|||
|
d80436534f
|
|||
|
|
14e2dd490e | ||
|
|
68e54b1110 | ||
|
|
e615b2cdea | ||
|
|
4b45a2c557 | ||
| c6fe553341 | |||
|
69a2b9c811
|
|||
|
f31a7d527d
|
|||
| ed3cf7644f | |||
| 77d21b004f | |||
|
379fafcd16
|
|||
|
7586d2d8ac
|
|||
|
9e41376244
|
|||
| 2bfef94bbe | |||
|
a8c4cba968
|
|||
|
d5566d4737
|
|||
|
366bc36520
|
|||
|
79b24b0d44
|
|||
|
7f93680987
|
|||
|
bdbff32256
|
|||
| ebadc24b59 | |||
| e0075fdcdc | |||
| 06aafefffc | |||
| ff6534fa81 | |||
| 6a77df402d | |||
| 79236d84e9 | |||
| beb8d13dde |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal 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"
|
||||
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request: ~
|
||||
workflow_dispatch: ~
|
||||
|
||||
@@ -18,10 +20,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Lint Dockerfile
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Docker images
|
||||
uses: docker/bake-action@v4
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
pull: true
|
||||
load: true
|
||||
@@ -50,16 +54,20 @@ jobs:
|
||||
- name: Run migrations
|
||||
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
|
||||
- name: Run PHPUnit
|
||||
if: false # Remove this line when the tests are ready
|
||||
run: docker compose exec -T php vendor/bin/phpunit
|
||||
- name: Doctrine Schema Validator
|
||||
run: docker compose exec -T php bin/console -e test doctrine:schema:validate
|
||||
lint:
|
||||
name: Docker Lint
|
||||
deploy:
|
||||
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
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Lint Dockerfile
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
|
||||
- shell: bash
|
||||
env:
|
||||
PORTAINER_WEBHOOK: ${{secrets.PORTAINER_WEBHOOK}}
|
||||
run: |
|
||||
curl -v -X POST "$PORTAINER_WEBHOOK"
|
||||
|
||||
2
.idea/TijdVoorDeTest.iml
generated
2
.idea/TijdVoorDeTest.iml
generated
@@ -134,7 +134,6 @@
|
||||
<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/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/guzzlehttp/psr7" />
|
||||
<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/symfony/stimulus-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/ux-turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
23
.idea/php.xml
generated
23
.idea/php.xml
generated
@@ -7,11 +7,6 @@
|
||||
</laravel_pint_by_interpreter>
|
||||
</laravel_pint_settings>
|
||||
</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">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
@@ -108,7 +103,6 @@
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||
<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/mime" />
|
||||
<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/symfony/ux-turbo" />
|
||||
<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>
|
||||
</component>
|
||||
<component name="PhpInterpreters">
|
||||
<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">
|
||||
<type_data command="EXEC" />
|
||||
<dockerComposeConfigurationPaths>
|
||||
@@ -219,15 +215,15 @@
|
||||
</component>
|
||||
<component name="PhpInterpretersPhpInfoCache">
|
||||
<phpInfoCache>
|
||||
<interpreter name="Compose PHP 8.3">
|
||||
<phpinfo binary_type="PHP" php_cgi="/usr/local/bin/php-cgi" php_cli="/usr/local/bin/php" path_separator=":" version="8.3.19">
|
||||
<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>
|
||||
<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.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-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_options>
|
||||
<configuration_option name="include_path" value=".:/usr/local/lib/php" />
|
||||
</configuration_options>
|
||||
<debuggers>
|
||||
<debugger_info debugger="xdebug" debugger_version="3.4.2">
|
||||
<debugger_info debugger="xdebug" debugger_version="3.4.3">
|
||||
<debug_extensions />
|
||||
</debugger_info>
|
||||
</debuggers>
|
||||
@@ -244,8 +240,10 @@
|
||||
<extension name="curl" />
|
||||
<extension name="date" />
|
||||
<extension name="dom" />
|
||||
<extension name="excimer" />
|
||||
<extension name="fileinfo" />
|
||||
<extension name="filter" />
|
||||
<extension name="gd" />
|
||||
<extension name="hash" />
|
||||
<extension name="iconv" />
|
||||
<extension name="intl" />
|
||||
@@ -265,6 +263,7 @@
|
||||
<extension name="sqlite3" />
|
||||
<extension name="standard" />
|
||||
<extension name="tokenizer" />
|
||||
<extension name="uuid" />
|
||||
<extension name="xdebug" />
|
||||
<extension name="xml" />
|
||||
<extension name="xmlreader" />
|
||||
@@ -276,7 +275,7 @@
|
||||
</interpreter>
|
||||
</phpInfoCache>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.3" />
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||
<component name="PhpStan">
|
||||
<PhpStan_settings>
|
||||
<phpstan_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" tool_path="vendor/bin/phpstan" timeout="60000" />
|
||||
|
||||
1
.idea/symfony2.xml
generated
1
.idea/symfony2.xml
generated
@@ -2,5 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="Symfony2PluginSettings">
|
||||
<option name="pluginEnabled" value="true" />
|
||||
<option name="profilerCsvPath" value="" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,7 +1,7 @@
|
||||
#syntax=docker/dockerfile:1
|
||||
|
||||
# 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
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
|
||||
@@ -31,7 +31,6 @@ RUN set -eux; \
|
||||
intl \
|
||||
opcache \
|
||||
zip \
|
||||
uuid \
|
||||
gd \
|
||||
excimer-1.2.3 \
|
||||
;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as bootstrap from 'bootstrap'
|
||||
import './bootstrap.js';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import * as bootstrap from 'bootstrap'
|
||||
|
||||
import './styles/backoffice.scss';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
||||
|
||||
17
assets/controllers/bo/quiz_controller.js
Normal file
17
assets/controllers/bo/quiz_controller.js
Normal 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();
|
||||
}
|
||||
}
|
||||
10
assets/controllers/elimination_controller.js
Normal file
10
assets/controllers/elimination_controller.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {Controller} from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
next() {
|
||||
const currentUrl = window.location.href;
|
||||
const urlParts = currentUrl.split('/');
|
||||
urlParts.pop();
|
||||
window.location.href = urlParts.join('/');
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,3 @@ import * as bootstrap from 'bootstrap'
|
||||
|
||||
import './styles/app.scss'
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Check if we're on the elimination candidate screen
|
||||
const eliminationScreen = document.querySelector('.elimination-screen');
|
||||
if (eliminationScreen) {
|
||||
// Add event listener for any keypress
|
||||
document.addEventListener('keydown', function (event) {
|
||||
// Get the current URL
|
||||
const currentUrl = window.location.href;
|
||||
// Extract the elimination ID from the URL
|
||||
const urlParts = currentUrl.split('/');
|
||||
// Remove the candidate hash (last part of the URL)
|
||||
urlParts.pop();
|
||||
// Construct the URL to the main elimination page
|
||||
const redirectUrl = urlParts.join('/');
|
||||
// Redirect to the main elimination page
|
||||
window.location.href = redirectUrl;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ services:
|
||||
# See https://xdebug.org/docs/all_settings#mode
|
||||
XDEBUG_MODE: "${XDEBUG_MODE:-off}"
|
||||
MAILER_DSN: "smtp://mailer:1025"
|
||||
PHP_CS_FIXER_IGNORE_ENV: 1
|
||||
extra_hosts:
|
||||
# Ensure that host.docker.internal is correctly defined on Linux
|
||||
- host.docker.internal:host-gateway
|
||||
|
||||
@@ -8,6 +8,7 @@ services:
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
||||
MAILER_DSN: ${MAILER_DSN}
|
||||
MAILER_SENDER: ${MAILER_SENDER}
|
||||
SENTRY_DSN: ${SENTRY_DSN}
|
||||
labels:
|
||||
@@ -23,6 +24,8 @@ services:
|
||||
database:
|
||||
networks:
|
||||
- internal
|
||||
ports:
|
||||
- "5430:5432"
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
|
||||
@@ -6,39 +6,39 @@
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.3.15",
|
||||
"php": ">=8.4",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/dbal": "^4.2.3",
|
||||
"doctrine/doctrine-bundle": "^2.14.0",
|
||||
"doctrine/dbal": "^4.3.3",
|
||||
"doctrine/doctrine-bundle": "^2.16.1",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4.2",
|
||||
"doctrine/orm": "^3.3.3",
|
||||
"easycorp/easyadmin-bundle": "^4.24.7",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.2",
|
||||
"phpoffice/phpspreadsheet": "^4.2.0",
|
||||
"phpstan/phpdoc-parser": "^2.1",
|
||||
"doctrine/orm": "^3.5.2",
|
||||
"easycorp/easyadmin-bundle": "^4.25.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.3",
|
||||
"phpoffice/phpspreadsheet": "^5.1",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"sentry/sentry-symfony": "^5.2",
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/asset-mapper": "7.2.*",
|
||||
"symfony/console": "7.2.*",
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/flex": "^2.7.0",
|
||||
"symfony/form": "7.2.*",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/mailer": "7.2.*",
|
||||
"symfony/property-access": "7.2.*",
|
||||
"symfony/property-info": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/security-csrf": "7.2.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/uid": "7.2.*",
|
||||
"symfony/ux-turbo": "^2.26",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"symfonycasts/sass-bundle": "^0.8.2",
|
||||
"symfonycasts/verify-email-bundle": "^1.17.3",
|
||||
"sentry/sentry-symfony": "^5.4",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/asset-mapper": "7.3.*",
|
||||
"symfony/console": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/flex": "^2.8.2",
|
||||
"symfony/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/mailer": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
"symfony/property-info": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/security-csrf": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/uid": "7.3.*",
|
||||
"symfony/ux-turbo": "^2.30.0",
|
||||
"symfony/yaml": "7.3.*",
|
||||
"symfonycasts/sass-bundle": "^0.8.3",
|
||||
"symfonycasts/verify-email-bundle": "^1.17.4",
|
||||
"thecodingmachine/safe": "^3.3.0",
|
||||
"twig/extra-bundle": "^3.21",
|
||||
"twig/intl-extra": "^3.21",
|
||||
@@ -46,23 +46,23 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"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/phpstan": "^2.1.17",
|
||||
"phpstan/phpstan-doctrine": "^2.0.3",
|
||||
"phpstan/phpstan-phpunit": "^2.0.6",
|
||||
"phpstan/phpstan-symfony": "^2.0.6",
|
||||
"phpunit/phpunit": "^12.1.6",
|
||||
"rector/rector": "^2.0.16",
|
||||
"phpstan/phpstan": "^2.1.22",
|
||||
"phpstan/phpstan-doctrine": "^2.0.5",
|
||||
"phpstan/phpstan-phpunit": "^2.0.7",
|
||||
"phpstan/phpstan-symfony": "^2.0.8",
|
||||
"phpunit/phpunit": "^12.3.8",
|
||||
"rector/rector": "^2.1.6",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "7.2.*",
|
||||
"symfony/css-selector": "7.2.*",
|
||||
"symfony/maker-bundle": "^1.63.0",
|
||||
"symfony/phpunit-bridge": "7.2.*",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*",
|
||||
"symfony/browser-kit": "7.3.*",
|
||||
"symfony/css-selector": "7.3.*",
|
||||
"symfony/maker-bundle": "^1.64.0",
|
||||
"symfony/phpunit-bridge": "7.3.*",
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*",
|
||||
"thecodingmachine/phpstan-safe-rule": "^1.4.1",
|
||||
"vincentlanglet/twig-cs-fixer": "^3.7.1"
|
||||
"vincentlanglet/twig-cs-fixer": "^3.9.0"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
@@ -95,7 +95,7 @@
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*",
|
||||
"symfony/polyfill-php83": "*",
|
||||
"symfony/polyfill-uuid": "*"
|
||||
"symfony/polyfill-php84": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
@@ -116,7 +116,7 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.2.*",
|
||||
"require": "7.3.*",
|
||||
"docker": true
|
||||
}
|
||||
}
|
||||
|
||||
1930
composer.lock
generated
1930
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,8 @@ doctrine:
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
@@ -37,7 +37,7 @@ security:
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
- { path: ^/backoffice, roles: ROLE_USER }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||
prefix: /_error
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||
prefix: /_profiler
|
||||
|
||||
90
migrations/Version20250606192337.php
Normal file
90
migrations/Version20250606192337.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250606192337 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Ze Big migration';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE quiz_candidate (id UUID NOT NULL, corrections DOUBLE PRECISION NOT NULL, created TIMESTAMP(0) WITH TIME ZONE NOT NULL, quiz_id UUID NOT NULL, candidate_id UUID NOT NULL, PRIMARY KEY(id))
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_CED2FFA2853CD175 ON quiz_candidate (quiz_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_CED2FFA291BD8781 ON quiz_candidate (candidate_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE UNIQUE INDEX UNIQ_CED2FFA291BD8781853CD175 ON quiz_candidate (candidate_id, quiz_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE quiz_candidate ADD CONSTRAINT FK_CED2FFA2853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE quiz_candidate ADD CONSTRAINT FK_CED2FFA291BD8781 FOREIGN KEY (candidate_id) REFERENCES candidate (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE correction DROP CONSTRAINT fk_a29da1b891bd8781
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE correction DROP CONSTRAINT fk_a29da1b8853cd175
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE correction
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE elimination ALTER created TYPE TIMESTAMP(0) WITH TIME ZONE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE given_answer ALTER created TYPE TIMESTAMP(0) WITH TIME ZONE
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE correction (id UUID NOT NULL, candidate_id UUID NOT NULL, quiz_id UUID NOT NULL, amount DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE UNIQUE INDEX uniq_a29da1b891bd8781853cd175 ON correction (candidate_id, quiz_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX idx_a29da1b8853cd175 ON correction (quiz_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX idx_a29da1b891bd8781 ON correction (candidate_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE correction ADD CONSTRAINT fk_a29da1b891bd8781 FOREIGN KEY (candidate_id) REFERENCES candidate (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE correction ADD CONSTRAINT fk_a29da1b8853cd175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE quiz_candidate DROP CONSTRAINT FK_CED2FFA2853CD175
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE quiz_candidate DROP CONSTRAINT FK_CED2FFA291BD8781
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE quiz_candidate
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE given_answer ALTER created TYPE TIMESTAMP(0) WITHOUT TIME ZONE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE elimination ALTER created TYPE TIMESTAMP(0) WITHOUT TIME ZONE
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
42
migrations/Version20250606195952.php
Normal file
42
migrations/Version20250606195952.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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 Version20250606195952 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
delete from given_answer where answer_id is null
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE given_answer ALTER answer_id TYPE UUID
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE given_answer ALTER answer_id SET NOT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE given_answer ALTER answer_id TYPE UUID
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE given_answer ALTER answer_id DROP NOT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
41
migrations/Version20250607154730.php
Normal file
41
migrations/Version20250607154730.php
Normal 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);
|
||||
}
|
||||
}
|
||||
53
migrations/Version20250607184525.php
Normal file
53
migrations/Version20250607184525.php
Normal 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);
|
||||
}
|
||||
}
|
||||
48
migrations/Version20250610210417.php
Normal file
48
migrations/Version20250610210417.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,4 @@ parameters:
|
||||
- public/
|
||||
- src/
|
||||
- tests/
|
||||
treatPhpDocTypesAsCertain: false
|
||||
|
||||
41
src/Command/AddSettingsCommand.php
Normal file
41
src/Command/AddSettingsCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ namespace App\Command;
|
||||
use App\Repository\SeasonRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
@@ -18,29 +18,19 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
name: 'app:claim-season',
|
||||
description: 'Give a user owner rights on a season',
|
||||
)]
|
||||
class ClaimSeasonCommand extends Command
|
||||
readonly class ClaimSeasonCommand
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly SeasonRepository $seasonRepository,
|
||||
private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
public function __construct(private UserRepository $userRepository, private SeasonRepository $seasonRepository, private EntityManagerInterface $entityManager) {}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('email', InputArgument::REQUIRED, 'The email of the user thats claims the season')
|
||||
->addArgument('season', InputArgument::REQUIRED, 'The season to claim')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string $seasonCode,
|
||||
#[Argument]
|
||||
string $email,
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
): int {
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$email = $input->getArgument('email');
|
||||
$seasonCode = $input->getArgument('season');
|
||||
|
||||
try {
|
||||
$season = $this->seasonRepository->findOneBy(['seasonCode' => $seasonCode]);
|
||||
|
||||
@@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace App\Command;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
@@ -16,25 +16,17 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
name: 'app:make-admin',
|
||||
description: 'Give a user the role admin',
|
||||
)]
|
||||
class MakeAdminCommand extends Command
|
||||
readonly class MakeAdminCommand
|
||||
{
|
||||
public function __construct(private readonly UserRepository $userRepository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
public function __construct(private UserRepository $userRepository) {}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('email', InputArgument::REQUIRED, 'The email of the user to make admin')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string $email,
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
): int {
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$email = $input->getArgument('email');
|
||||
|
||||
try {
|
||||
$this->userRepository->makeAdmin($email);
|
||||
} catch (\InvalidArgumentException) {
|
||||
|
||||
@@ -9,6 +9,10 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBase
|
||||
|
||||
abstract class AbstractController extends AbstractBaseController
|
||||
{
|
||||
protected const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
|
||||
|
||||
protected const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
|
||||
|
||||
#[\Override]
|
||||
protected function addFlash(FlashType|string $type, mixed $message): void
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\Answer;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
/** @extends AbstractCrudController<Answer> */
|
||||
class AnswerCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\Candidate;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
/** @extends AbstractCrudController<Candidate> */
|
||||
class CandidateCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Answer;
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\Correction;
|
||||
use App\Entity\GivenAnswer;
|
||||
use App\Entity\Question;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\QuizCandidate;
|
||||
use App\Entity\Season;
|
||||
use App\Entity\User;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
|
||||
@@ -58,7 +58,7 @@ class DashboardController extends AbstractDashboardController
|
||||
yield MenuItem::linkToCrud('Quiz', 'fas fa-list', Quiz::class);
|
||||
yield MenuItem::linkToCrud('Question', 'fas fa-list', Question::class);
|
||||
yield MenuItem::linkToCrud('Candidate', 'fas fa-list', Candidate::class);
|
||||
yield MenuItem::linkToCrud('Correction', 'fas fa-list', Correction::class);
|
||||
yield MenuItem::linkToCrud('Correction', 'fas fa-list', QuizCandidate::class);
|
||||
yield MenuItem::linkToCrud('User', 'fas fa-list', User::class);
|
||||
yield MenuItem::linkToCrud('Given Answer', 'fas fa-list', GivenAnswer::class);
|
||||
yield MenuItem::linkToCrud('Answer', 'fas fa-list', Answer::class);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\GivenAnswer;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
/** @extends AbstractCrudController<GivenAnswer> */
|
||||
class GivenAnswerCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\Question;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
/** @extends AbstractCrudController<Question> */
|
||||
class QuestionCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
|
||||
@@ -4,13 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Correction;
|
||||
use App\Entity\QuizCandidate;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
class CorrectionCrudController extends AbstractCrudController
|
||||
/** @extends AbstractCrudController<QuizCandidate> */
|
||||
class QuizCorrectionCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
{
|
||||
return Correction::class;
|
||||
return QuizCandidate::class;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\Quiz;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
/** @extends AbstractCrudController<Quiz> */
|
||||
class QuizCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\Season;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
/** @extends AbstractCrudController<Season> */
|
||||
class SeasonCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\User;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
|
||||
/** @extends AbstractCrudController<User> */
|
||||
class UserCrudController extends AbstractCrudController
|
||||
{
|
||||
public static function getEntityFqcn(): string
|
||||
|
||||
@@ -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
|
||||
{
|
||||
$season = new Season();
|
||||
|
||||
@@ -4,19 +4,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Backoffice;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Elimination;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Factory\EliminationFactory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
|
||||
final class PrepareEliminationController extends AbstractController
|
||||
{
|
||||
#[Route('/backoffice/elimination/{seasonCode}/{quiz}/prepare', name: 'app_prepare_elimination')]
|
||||
#[Route(
|
||||
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/elimination/prepare',
|
||||
name: 'app_prepare_elimination',
|
||||
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
|
||||
)]
|
||||
public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
|
||||
{
|
||||
$elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
|
||||
@@ -24,16 +29,21 @@ final class PrepareEliminationController extends AbstractController
|
||||
return $this->redirectToRoute('app_prepare_elimination_view', ['elimination' => $elimination->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/elimination/{elimination}', name: 'app_prepare_elimination_view')]
|
||||
#[Route(
|
||||
'/backoffice/elimination/{elimination}',
|
||||
name: 'app_prepare_elimination_view',
|
||||
requirements: ['elimination' => Requirement::UUID],
|
||||
)]
|
||||
public function viewElimination(Elimination $elimination, Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$elimination->updateFromInputBag($request->request);
|
||||
$em->flush();
|
||||
|
||||
if (true === $request->request->getBoolean('start')) {
|
||||
if ($request->request->getBoolean('start')) {
|
||||
return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]);
|
||||
}
|
||||
|
||||
$this->addFlash('success', 'Elimination updated');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,24 @@ declare(strict_types=1);
|
||||
namespace App\Controller\Backoffice;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Exception\ErrorClearingQuizException;
|
||||
use App\Repository\CandidateRepository;
|
||||
use App\Repository\QuizCandidateRepository;
|
||||
use App\Repository\QuizRepository;
|
||||
use App\Security\Voter\SeasonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsController]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
@@ -21,9 +30,14 @@ class QuizController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CandidateRepository $candidateRepository,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz')]
|
||||
#[Route(
|
||||
'/backoffice/season/{seasonCode:season}/quiz/{quiz}',
|
||||
name: 'app_backoffice_quiz',
|
||||
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
|
||||
)]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function index(Season $season, Quiz $quiz): Response
|
||||
{
|
||||
@@ -34,9 +48,13 @@ class QuizController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable')]
|
||||
#[Route(
|
||||
'/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')]
|
||||
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
|
||||
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): RedirectResponse
|
||||
{
|
||||
$season->setActiveQuiz($quiz);
|
||||
$em->flush();
|
||||
@@ -47,4 +65,56 @@ class QuizController extends AbstractController
|
||||
|
||||
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()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Enum\FlashType;
|
||||
use App\Form\AddCandidatesFormType;
|
||||
use App\Form\SettingsForm;
|
||||
use App\Form\UploadQuizFormType;
|
||||
use App\Security\Voter\SeasonVoter;
|
||||
use App\Service\QuizSpreadsheetService;
|
||||
@@ -26,19 +27,39 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
#[IsGranted('ROLE_USER')]
|
||||
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('/backoffice/season/{seasonCode}', name: 'app_backoffice_season')]
|
||||
#[Route(
|
||||
'/backoffice/season/{seasonCode:season}',
|
||||
name: 'app_backoffice_season',
|
||||
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
|
||||
)]
|
||||
#[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', [
|
||||
'season' => $season,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
|
||||
#[Route(
|
||||
'/backoffice/season/{seasonCode:season}/add-candidate',
|
||||
name: 'app_backoffice_add_candidates',
|
||||
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
|
||||
priority: 10,
|
||||
)]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function addCandidates(Season $season, Request $request): Response
|
||||
{
|
||||
@@ -47,8 +68,8 @@ class SeasonController extends AbstractController
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$candidates = $form->get('candidates')->getData();
|
||||
foreach (explode("\r\n", (string) $candidates) as $candidate) {
|
||||
$season->addCandidate(new Candidate($candidate));
|
||||
foreach (explode("\n", (string) $candidates) as $candidate) {
|
||||
$season->addCandidate(new Candidate(mb_rtrim($candidate)));
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
@@ -59,7 +80,12 @@ class SeasonController extends AbstractController
|
||||
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/season/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
|
||||
#[Route(
|
||||
'/backoffice/season/{seasonCode:season}/add-quiz',
|
||||
name: 'app_backoffice_quiz_add',
|
||||
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
|
||||
priority: 10,
|
||||
)]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
@@ -27,7 +28,7 @@ final class EliminationController extends AbstractController
|
||||
{
|
||||
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')]
|
||||
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')]
|
||||
public function candidateScreen(Elimination $elimination, string $candidateHash, CandidateRepository $candidateRepository): Response
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Entity\Answer;
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\GivenAnswer;
|
||||
use App\Entity\Question;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Enum\FlashType;
|
||||
use App\Form\EnterNameType;
|
||||
@@ -17,25 +18,21 @@ use App\Repository\AnswerRepository;
|
||||
use App\Repository\CandidateRepository;
|
||||
use App\Repository\GivenAnswerRepository;
|
||||
use App\Repository\QuestionRepository;
|
||||
use App\Repository\QuizCandidateRepository;
|
||||
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\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsController]
|
||||
final class QuizController extends AbstractController
|
||||
{
|
||||
public const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
|
||||
|
||||
private const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
|
||||
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
#[Route(path: '/', name: 'app_quiz_selectseason', methods: ['GET', 'POST'])]
|
||||
#[Route(path: '/', name: 'app_quiz_select_season', methods: ['GET', 'POST'])]
|
||||
public function selectSeason(Request $request, SeasonRepository $seasonRepository): Response
|
||||
{
|
||||
$form = $this->createForm(SelectSeasonType::class);
|
||||
@@ -47,19 +44,18 @@ final class QuizController extends AbstractController
|
||||
if ([] === $seasonRepository->findBy(['seasonCode' => $seasonCode])) {
|
||||
$this->addFlash(FlashType::Warning, $this->translator->trans('Invalid season code'));
|
||||
|
||||
return $this->redirectToRoute('app_quiz_selectseason');
|
||||
return $this->redirectToRoute('app_quiz_select_season');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_quiz_entername', ['seasonCode' => $seasonCode]);
|
||||
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $seasonCode]);
|
||||
}
|
||||
|
||||
return $this->render('quiz/select_season.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{seasonCode}', name: 'app_quiz_entername', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
|
||||
#[Route(path: '/{seasonCode:season}', name: 'app_quiz_enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
|
||||
public function enterName(
|
||||
Request $request,
|
||||
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
|
||||
Season $season,
|
||||
): Response {
|
||||
$form = $this->createForm(EnterNameType::class);
|
||||
@@ -69,49 +65,54 @@ final class QuizController extends AbstractController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$name = $form->get('name')->getData();
|
||||
|
||||
return $this->redirectToRoute('app_quiz_quizpage', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64UrlEncode($name)]);
|
||||
return $this->redirectToRoute('app_quiz_quiz_page', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64UrlEncode($name)]);
|
||||
}
|
||||
|
||||
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/{seasonCode}/{nameHash}',
|
||||
name: 'app_quiz_quizpage',
|
||||
path: '/{seasonCode:season}/{nameHash}',
|
||||
name: 'app_quiz_quiz_page',
|
||||
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
|
||||
)]
|
||||
public function quizPage(
|
||||
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
|
||||
Season $season,
|
||||
string $nameHash,
|
||||
Request $request,
|
||||
CandidateRepository $candidateRepository,
|
||||
QuestionRepository $questionRepository,
|
||||
AnswerRepository $answerRepository,
|
||||
GivenAnswerRepository $givenAnswerRepository,
|
||||
Request $request,
|
||||
QuizCandidateRepository $quizCandidateRepository,
|
||||
): Response {
|
||||
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
|
||||
|
||||
if (!$candidate instanceof Candidate) {
|
||||
$this->addFlash(FlashType::Danger, $this->translator->trans('Candidate not found'));
|
||||
|
||||
return $this->redirectToRoute('app_quiz_entername', ['seasonCode' => $season->getSeasonCode()]);
|
||||
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
$quiz = $season->getActiveQuiz();
|
||||
|
||||
if (!$quiz instanceof Quiz) {
|
||||
$this->addFlash(FlashType::Warning, $this->translator->trans('There is no active quiz'));
|
||||
|
||||
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$answer = $answerRepository->findOneBy(['id' => $request->request->get('answer')]);
|
||||
|
||||
if (!$answer instanceof Answer) {
|
||||
throw new BadRequestException('Invalid Answer ID');
|
||||
throw new BadRequestHttpException('Invalid Answer ID');
|
||||
}
|
||||
|
||||
$givenAnswer = (new GivenAnswer())
|
||||
->setCandidate($candidate)
|
||||
->setAnswer($answer)
|
||||
->setQuiz($answer->getQuestion()->getQuiz());
|
||||
$givenAnswer = new GivenAnswer($candidate, $answer->getQuestion()->getQuiz(), $answer);
|
||||
$givenAnswerRepository->save($givenAnswer);
|
||||
|
||||
return $this->redirectToRoute('app_quiz_quizpage', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => $nameHash]);
|
||||
return $this->redirectToRoute('app_quiz_quiz_page', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => $nameHash]);
|
||||
}
|
||||
|
||||
$question = $questionRepository->findNextQuestionForCandidate($candidate);
|
||||
@@ -119,10 +120,11 @@ final class QuizController extends AbstractController
|
||||
if (!$question instanceof Question) {
|
||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz completed'));
|
||||
|
||||
return $this->redirectToRoute('app_quiz_entername', ['seasonCode' => $season->getSeasonCode()]);
|
||||
return $this->redirectToRoute('app_quiz_enter_name', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
// TODO One first question record time
|
||||
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]);
|
||||
$quizCandidateRepository->createIfNotExist($quiz, $candidate);
|
||||
|
||||
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question, 'season' => $season]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ use App\Form\RegistrationFormType;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\EmailVerifier;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
@@ -29,6 +31,7 @@ final class RegistrationController extends AbstractController
|
||||
UserPasswordHasherInterface $userPasswordHasher,
|
||||
Security $security,
|
||||
EntityManagerInterface $entityManager,
|
||||
LoggerInterface $logger,
|
||||
): Response {
|
||||
$user = new User();
|
||||
$form = $this->createForm(RegistrationFormType::class, $user);
|
||||
@@ -43,13 +46,17 @@ final class RegistrationController extends AbstractController
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
|
||||
// generate a signed url and email it to the user
|
||||
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
(new TemplatedEmail())
|
||||
->to((string) $user->getEmail())
|
||||
->subject($this->translator->trans('Please Confirm your Email'))
|
||||
->htmlTemplate('backoffice/registration/confirmation_email.html.twig'),
|
||||
);
|
||||
try {
|
||||
// generate a signed url and email it to the user
|
||||
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
new TemplatedEmail()
|
||||
->to((string) $user->getEmail())
|
||||
->subject($this->translator->trans('Please Confirm your Email'))
|
||||
->htmlTemplate('backoffice/registration/confirmation_email.html.twig'),
|
||||
);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$logger->error($e->getMessage());
|
||||
}
|
||||
|
||||
$response = $security->login($user, 'form_login', 'main');
|
||||
\assert($response instanceof Response);
|
||||
|
||||
@@ -44,18 +44,18 @@ class KrtekFixtures extends Fixture
|
||||
|
||||
private function createQuiz1(Season $season): Quiz
|
||||
{
|
||||
return (new Quiz())
|
||||
return new Quiz()
|
||||
->setName('Quiz 1')
|
||||
->setSeason($season)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Is de Krtek een man of een vrouw?')
|
||||
->addAnswer(new Answer('Vrouw', true))
|
||||
->addAnswer(new Answer('Man'))
|
||||
->setOrdering(1),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Hoeveel broers heeft de Krtek?')
|
||||
->addAnswer(new Answer('Geen', true))
|
||||
->addAnswer(new Answer('1'))
|
||||
@@ -63,7 +63,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(2),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Wat is de lievelingsfeestdag van de Krtek?')
|
||||
->addAnswer(new Answer('Geen'))
|
||||
->addAnswer(new Answer('Diens eigen verjaardag'))
|
||||
@@ -73,13 +73,13 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(3),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?')
|
||||
->addAnswer(new Answer('Met het OV', true))
|
||||
->addAnswer(new Answer('Met de auto'))
|
||||
->setOrdering(4),
|
||||
)
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Met wie keek de Krtek video bij binnenkomst?')
|
||||
->addAnswer(new Answer('Claudia'))
|
||||
->addAnswer(new Answer('Eelco'))
|
||||
@@ -97,7 +97,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(5),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Welk advies zou de Krtek zichzelf als kind geven?')
|
||||
->addAnswer(new Answer('Geef je vader een knuffel.'))
|
||||
->addAnswer(new Answer('Trek je wat minder aan van anderen.'))
|
||||
@@ -110,7 +110,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(6),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Wat voor soort schoenen droeg de Krtek bij het diner?')
|
||||
->addAnswer(new Answer('Sneakers'))
|
||||
->addAnswer(new Answer('Wandel-/bergschoenen', true))
|
||||
@@ -121,7 +121,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(7),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Met welk vervoersmiddel reist de Krtek het liefste?')
|
||||
->addAnswer(new Answer('Fiets', true))
|
||||
->addAnswer(new Answer('Auto'))
|
||||
@@ -129,14 +129,14 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(8),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Heeft de Krtek een eigen auto?')
|
||||
->addAnswer(new Answer('Ja'))
|
||||
->addAnswer(new Answer('Nee', true))
|
||||
->setOrdering(9),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Van wie is de quote die de Krtek gepakt heeft')
|
||||
->addAnswer(new Answer('Karen'))
|
||||
->addAnswer(new Answer('Gilles de Coster'))
|
||||
@@ -156,14 +156,14 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(10),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topito’s uit iemands rugzak stelen om te kunnen winnen?')
|
||||
->addAnswer(new Answer('Ja'))
|
||||
->addAnswer(new Answer('Nee', true))
|
||||
->setOrdering(11),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('In wat voor bed slaapt de Krtek dit weekend?')
|
||||
->addAnswer(new Answer('Éénpersoons, losstaand bed'))
|
||||
->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true))
|
||||
@@ -171,7 +171,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(12),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Hoeveel jaar heeft de Krtek gedaan over de middelbare school?')
|
||||
->addAnswer(new Answer('5'))
|
||||
->addAnswer(new Answer('6', true))
|
||||
@@ -180,14 +180,14 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(13),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->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 buitenmuur', true))
|
||||
->setOrdering(14),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Wie is de Krtek?')
|
||||
->addAnswer(new Answer('Claudia', true))
|
||||
->addAnswer(new Answer('Eelco'))
|
||||
@@ -209,18 +209,18 @@ class KrtekFixtures extends Fixture
|
||||
|
||||
private function createQuiz2(Season $season): Quiz
|
||||
{
|
||||
return (new Quiz())
|
||||
return new Quiz()
|
||||
->setName('Quiz 2')
|
||||
->setSeason($season)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Is de Krtek een man of een vrouw?')
|
||||
->addAnswer(new Answer('Man'))
|
||||
->addAnswer(new Answer('Vrouw', true))
|
||||
->setOrdering(1),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Heeft de Krtek dieetwensen of allergieën?')
|
||||
->addAnswer(new Answer('nee'))
|
||||
->addAnswer(new Answer('De Krtek is vegetariër', true))
|
||||
@@ -232,7 +232,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(2),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Hoe heet het huisdier/de huisdieren van de Krtek?')
|
||||
->addAnswer(new Answer('Amy, Karel en Floyd'))
|
||||
->addAnswer(new Answer('Flip en Majoor'))
|
||||
@@ -244,7 +244,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(3),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Wat dronk de Krtek deze ochtend bij het ontbijt?')
|
||||
->addAnswer(new Answer('Koffie'))
|
||||
->addAnswer(new Answer('Thee'))
|
||||
@@ -255,7 +255,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(4),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Waar ging de eerste vakantie die de Krtek zich nog herinnert heen?')
|
||||
->addAnswer(new Answer('Denemarken'))
|
||||
->addAnswer(new Answer('Drenthe'))
|
||||
@@ -267,7 +267,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(5),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Met welk groepje ging de Krtek als eerste het Douanespel in?')
|
||||
->addAnswer(new Answer('Het eerste groepje', true))
|
||||
->addAnswer(new Answer('Het tweede groepje'))
|
||||
@@ -277,7 +277,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(6),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Gelooft de Krtek ergens in?')
|
||||
->addAnswer(new Answer('Nee'))
|
||||
->addAnswer(new Answer('Het universum', true))
|
||||
@@ -286,14 +286,14 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(7),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?')
|
||||
->addAnswer(new Answer('Ja', true))
|
||||
->addAnswer(new Answer('Nee'))
|
||||
->setOrdering(8),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Hoe laat ging de Krtek gisteravond naar bed?')
|
||||
->addAnswer(new Answer('Tussen 0:00 en 0:59 uur'))
|
||||
->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true))
|
||||
@@ -302,7 +302,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(9),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Hoeveel batterijen heeft de Krtek naar het bord gebracht bij het douanespel?')
|
||||
->addAnswer(new Answer('1'))
|
||||
->addAnswer(new Answer('2'))
|
||||
@@ -311,7 +311,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(10),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Wat keek de Krtek als kind graag op TV?')
|
||||
->addAnswer(new Answer('Digimon', true))
|
||||
->addAnswer(new Answer('Floris'))
|
||||
@@ -322,7 +322,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(11),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Waarin zat op de heenreis de bagage van de Krtek (voornamelijk)?')
|
||||
->addAnswer(new Answer('In koffer(s)', true))
|
||||
->addAnswer(new Answer('In losse tas(sen)'))
|
||||
@@ -330,7 +330,7 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(12),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->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('Smakkende mensen'))
|
||||
@@ -343,14 +343,14 @@ class KrtekFixtures extends Fixture
|
||||
->setOrdering(13),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Wilde de Krtek penningmeester worden?')
|
||||
->addAnswer(new Answer('Ja'))
|
||||
->addAnswer(new Answer('Nee', true))
|
||||
->setOrdering(14),
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->addQuestion(new Question()
|
||||
->setQuestion('Wie is de Krtek?')
|
||||
->addAnswer(new Answer('Claudia', true))
|
||||
->addAnswer(new Answer('Eelco'))
|
||||
|
||||
@@ -116,16 +116,6 @@ class Answer
|
||||
return $this->givenAnswers;
|
||||
}
|
||||
|
||||
public function addGivenAnswer(GivenAnswer $givenAnswer): static
|
||||
{
|
||||
if (!$this->givenAnswers->contains($givenAnswer)) {
|
||||
$this->givenAnswers->add($givenAnswer);
|
||||
$givenAnswer->setAnswer($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrdering(): int
|
||||
{
|
||||
return $this->ordering;
|
||||
|
||||
@@ -21,7 +21,7 @@ class Candidate
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private ?Uuid $id = null;
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'candidates')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
@@ -35,9 +35,9 @@ class Candidate
|
||||
#[ORM\OneToMany(targetEntity: GivenAnswer::class, mappedBy: 'candidate', orphanRemoval: true)]
|
||||
private Collection $givenAnswers;
|
||||
|
||||
/** @var Collection<int, Correction> */
|
||||
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'candidate', orphanRemoval: true)]
|
||||
private Collection $corrections;
|
||||
/** @var Collection<int, QuizCandidate> */
|
||||
#[ORM\OneToMany(targetEntity: QuizCandidate::class, mappedBy: 'candidate', orphanRemoval: true)]
|
||||
private Collection $quizData;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(length: 16)]
|
||||
@@ -45,10 +45,10 @@ class Candidate
|
||||
) {
|
||||
$this->answersOnCandidate = new ArrayCollection();
|
||||
$this->givenAnswers = new ArrayCollection();
|
||||
$this->corrections = new ArrayCollection();
|
||||
$this->quizData = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -108,30 +108,10 @@ class Candidate
|
||||
return $this->givenAnswers;
|
||||
}
|
||||
|
||||
public function addGivenAnswer(GivenAnswer $givenAnswer): static
|
||||
/** @return Collection<int, QuizCandidate> */
|
||||
public function getQuizData(): Collection
|
||||
{
|
||||
if (!$this->givenAnswers->contains($givenAnswer)) {
|
||||
$this->givenAnswers->add($givenAnswer);
|
||||
$givenAnswer->setCandidate($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, Correction> */
|
||||
public function getCorrections(): Collection
|
||||
{
|
||||
return $this->corrections;
|
||||
}
|
||||
|
||||
public function addCorrection(Correction $correction): static
|
||||
{
|
||||
if (!$this->corrections->contains($correction)) {
|
||||
$this->corrections->add($correction);
|
||||
$correction->setCandidate($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
return $this->quizData;
|
||||
}
|
||||
|
||||
public function getNameHash(): string
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\CorrectionRepository;
|
||||
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: CorrectionRepository::class)]
|
||||
#[ORM\UniqueConstraint(columns: ['candidate_id', 'quiz_id'])]
|
||||
class Correction
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private ?Uuid $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'corrections')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Candidate $candidate;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'corrections')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Quiz $quiz;
|
||||
|
||||
#[ORM\Column]
|
||||
private float $amount = 0;
|
||||
|
||||
public function getId(): ?Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCandidate(): Candidate
|
||||
{
|
||||
return $this->candidate;
|
||||
}
|
||||
|
||||
public function setCandidate(Candidate $candidate): static
|
||||
{
|
||||
$this->candidate = $candidate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuiz(): Quiz
|
||||
{
|
||||
return $this->quiz;
|
||||
}
|
||||
|
||||
public function setQuiz(Quiz $quiz): static
|
||||
{
|
||||
$this->quiz = $quiz;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAmount(): ?float
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
public function setAmount(float $amount): static
|
||||
{
|
||||
$this->amount = $amount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
class Elimination
|
||||
{
|
||||
public const string SCREEN_GREEN = 'green';
|
||||
|
||||
public const string SCREEN_RED = 'red';
|
||||
|
||||
#[ORM\Id]
|
||||
@@ -30,12 +31,12 @@ class Elimination
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $data = [];
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: false)]
|
||||
private \DateTimeImmutable $created;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(inversedBy: 'eliminations')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private Quiz $quiz,
|
||||
) {}
|
||||
|
||||
@@ -66,7 +67,7 @@ class Elimination
|
||||
/** @param InputBag<bool|float|int|string> $inputBag */
|
||||
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));
|
||||
if (\is_string($newColour)) {
|
||||
$this->data[$name] = $inputBag->get('colour-'.mb_strtolower($name));
|
||||
|
||||
@@ -22,22 +22,24 @@ class GivenAnswer
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Candidate $candidate;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Quiz $quiz;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Answer $answer = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: false)]
|
||||
private \DateTimeImmutable $created;
|
||||
|
||||
public function getId(): ?Uuid
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Candidate $candidate,
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private Quiz $quiz,
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Answer $answer,
|
||||
) {}
|
||||
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -47,37 +49,16 @@ class GivenAnswer
|
||||
return $this->candidate;
|
||||
}
|
||||
|
||||
public function setCandidate(Candidate $candidate): static
|
||||
{
|
||||
$this->candidate = $candidate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuiz(): ?Quiz
|
||||
public function getQuiz(): Quiz
|
||||
{
|
||||
return $this->quiz;
|
||||
}
|
||||
|
||||
public function setQuiz(Quiz $quiz): static
|
||||
{
|
||||
$this->quiz = $quiz;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAnswer(): ?Answer
|
||||
public function getAnswer(): Answer
|
||||
{
|
||||
return $this->answer;
|
||||
}
|
||||
|
||||
public function setAnswer(?Answer $answer): static
|
||||
{
|
||||
$this->answer = $answer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreated(): \DateTimeImmutable
|
||||
{
|
||||
return $this->created;
|
||||
|
||||
@@ -20,7 +20,7 @@ class Question
|
||||
#[ORM\Column(type: UuidType::NAME)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private ?Uuid $id = null;
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
|
||||
private int $ordering;
|
||||
@@ -45,7 +45,7 @@ class Question
|
||||
$this->answers = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class Quiz
|
||||
#[ORM\Column(type: UuidType::NAME)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private ?Uuid $id = null;
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(length: 64)]
|
||||
private string $name;
|
||||
@@ -34,9 +34,9 @@ class Quiz
|
||||
#[ORM\OrderBy(['ordering' => 'ASC'])]
|
||||
private Collection $questions;
|
||||
|
||||
/** @var Collection<int, Correction> */
|
||||
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
|
||||
private Collection $corrections;
|
||||
/** @var Collection<int, QuizCandidate> */
|
||||
#[ORM\OneToMany(targetEntity: QuizCandidate::class, mappedBy: 'quiz', orphanRemoval: true)]
|
||||
private Collection $candidateData;
|
||||
|
||||
#[ORM\Column(nullable: false, options: ['default' => 1])]
|
||||
private int $dropouts = 1;
|
||||
@@ -49,11 +49,11 @@ class Quiz
|
||||
public function __construct()
|
||||
{
|
||||
$this->questions = new ArrayCollection();
|
||||
$this->corrections = new ArrayCollection();
|
||||
$this->candidateData = new ArrayCollection();
|
||||
$this->eliminations = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -98,20 +98,10 @@ class Quiz
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, Correction> */
|
||||
public function getCorrections(): Collection
|
||||
/** @return Collection<int, QuizCandidate> */
|
||||
public function getCandidateData(): Collection
|
||||
{
|
||||
return $this->corrections;
|
||||
}
|
||||
|
||||
public function addCorrection(Correction $correction): static
|
||||
{
|
||||
if (!$this->corrections->contains($correction)) {
|
||||
$this->corrections->add($correction);
|
||||
$correction->setQuiz($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
return $this->candidateData;
|
||||
}
|
||||
|
||||
public function getDropouts(): int
|
||||
|
||||
79
src/Entity/QuizCandidate.php
Normal file
79
src/Entity/QuizCandidate.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\QuizCandidateRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Safe\DateTimeImmutable;
|
||||
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
|
||||
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity(repositoryClass: QuizCandidateRepository::class)]
|
||||
#[ORM\UniqueConstraint(columns: ['candidate_id', 'quiz_id'])]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class QuizCandidate
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column]
|
||||
private float $corrections = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)]
|
||||
private \DateTimeImmutable $created;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(inversedBy: 'candidateData')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Quiz $quiz,
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'quizData')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Candidate $candidate,
|
||||
) {}
|
||||
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCandidate(): Candidate
|
||||
{
|
||||
return $this->candidate;
|
||||
}
|
||||
|
||||
public function getQuiz(): Quiz
|
||||
{
|
||||
return $this->quiz;
|
||||
}
|
||||
|
||||
public function getCorrections(): ?float
|
||||
{
|
||||
return $this->corrections;
|
||||
}
|
||||
|
||||
public function setCorrections(float $corrections): static
|
||||
{
|
||||
$this->corrections = $corrections;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreated(): \DateTimeImmutable
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$this->created = new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ class Season
|
||||
#[ORM\Column(type: UuidType::NAME)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private ?Uuid $id = null;
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(length: 64)]
|
||||
private string $name;
|
||||
@@ -43,16 +43,22 @@ class Season
|
||||
private Collection $owners;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?Quiz $ActiveQuiz = null;
|
||||
|
||||
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?SeasonSettings $settings = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->settings = new SeasonSettings();
|
||||
$this->quizzes = new ArrayCollection();
|
||||
$this->candidates = new ArrayCollection();
|
||||
$this->owners = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -165,4 +171,16 @@ class Season
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSettings(): ?SeasonSettings
|
||||
{
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public function setSettings(SeasonSettings $settings): static
|
||||
{
|
||||
$this->settings = $settings;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
57
src/Entity/SeasonSettings.php
Normal file
57
src/Entity/SeasonSettings.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private ?Uuid $id = null;
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
private string $email;
|
||||
@@ -51,7 +51,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
$this->seasons = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
7
src/Exception/ErrorClearingQuizException.php
Normal file
7
src/Exception/ErrorClearingQuizException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class ErrorClearingQuizException extends \Exception {}
|
||||
@@ -39,15 +39,8 @@ class RegistrationFormType extends AbstractType
|
||||
'second_options' => ['label' => $this->translator->trans('Repeat Password')],
|
||||
'mapped' => false,
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'Please enter a password',
|
||||
]),
|
||||
new Length([
|
||||
'min' => 8,
|
||||
'minMessage' => 'Your password should be at least {{ limit }} characters',
|
||||
// max length allowed by Symfony for security reasons
|
||||
'max' => 4096,
|
||||
]),
|
||||
new NotBlank(message: 'Please enter a password'),
|
||||
new Length(min: 8, max: 4096, minMessage: 'Your password should be at least {{ limit }} characters'),
|
||||
],
|
||||
'translation_domain' => false,
|
||||
])
|
||||
|
||||
35
src/Form/SettingsForm.php
Normal file
35
src/Form/SettingsForm.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,9 @@ class UploadQuizFormType extends AbstractType
|
||||
'required' => true,
|
||||
'translation_domain' => false,
|
||||
'constraints' => [
|
||||
new File([
|
||||
'maxSize' => '1024k',
|
||||
'mimeTypes' => [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
],
|
||||
'mimeTypesMessage' => $this->translator->trans('Please upload a valid XLSX file'),
|
||||
]),
|
||||
new File(maxSize: '1024k', mimeTypes: [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
], mimeTypesMessage: $this->translator->trans('Please upload a valid XLSX file')),
|
||||
],
|
||||
])
|
||||
;
|
||||
|
||||
@@ -10,7 +10,7 @@ class Base64
|
||||
{
|
||||
public static function base64UrlEncode(string $input): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
|
||||
return mb_rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
/** @throws UrlException */
|
||||
|
||||
@@ -5,12 +5,10 @@ declare(strict_types=1);
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\Correction;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Helpers\Base64;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Safe\Exceptions\UrlException;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
@@ -18,7 +16,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
/**
|
||||
* @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>
|
||||
*/
|
||||
class CandidateRepository extends ServiceEntityRepository
|
||||
@@ -36,12 +34,14 @@ class CandidateRepository extends ServiceEntityRepository
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createQueryBuilder('c')
|
||||
->where('c.season = :season')
|
||||
->andWhere('lower(c.name) = lower(:name)')
|
||||
->setParameter('season', $season)
|
||||
return $this->getEntityManager()->createQuery(<<<DQL
|
||||
select c from App\Entity\Candidate c
|
||||
where c.season = :season
|
||||
and lower(c.name) = lower(:name)
|
||||
DQL
|
||||
)->setParameter('season', $season)
|
||||
->setParameter('name', $name)
|
||||
->getQuery()->getOneOrNullResult();
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function save(Candidate $candidate, bool $flush = true): void
|
||||
@@ -56,46 +56,22 @@ class CandidateRepository extends ServiceEntityRepository
|
||||
/** @return ResultList */
|
||||
public function getScores(Quiz $quiz): array
|
||||
{
|
||||
$scoreTimeQb = $this->createQueryBuilder('c', 'c.id')
|
||||
->select('c.id', 'c.name', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'max(ga.created) - min(ga.created) as time')
|
||||
->join('c.givenAnswers', 'ga')
|
||||
->join('ga.answer', 'a')
|
||||
->where('ga.quiz = :quiz')
|
||||
->groupBy('c.id')
|
||||
->setParameter('quiz', $quiz);
|
||||
|
||||
$correctionsQb = $this->createQueryBuilder('c', 'c.id')
|
||||
->select('c.id', 'cor.amount as corrections')
|
||||
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
|
||||
->setParameter('quiz', $quiz);
|
||||
|
||||
$merged = array_merge_recursive($scoreTimeQb->getQuery()->getArrayResult(), $correctionsQb->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;
|
||||
return $this->getEntityManager()->createQuery(<<<DQL
|
||||
select
|
||||
c.id,
|
||||
c.name,
|
||||
sum(case when a.isRightAnswer = true then 1 else 0 end) as correct,
|
||||
qc.corrections,
|
||||
max(ga.created) - qc.created as time,
|
||||
(sum(case when a.isRightAnswer = true then 1 else 0 end) + qc.corrections) as score
|
||||
from App\Entity\Candidate c
|
||||
join c.givenAnswers ga
|
||||
join ga.answer a
|
||||
join c.quizData qc
|
||||
where qc.quiz = :quiz and ga.quiz = :quiz
|
||||
group by ga.quiz, c.id, qc.id
|
||||
order by score desc, time asc
|
||||
DQL
|
||||
)->setParameter('quiz', $quiz)->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,29 +17,4 @@ class EliminationRepository extends ServiceEntityRepository
|
||||
{
|
||||
parent::__construct($registry, Elimination::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Elimination[] Returns an array of Elimination objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('e')
|
||||
// ->andWhere('e.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('e.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Elimination
|
||||
// {
|
||||
// return $this->createQueryBuilder('e')
|
||||
// ->andWhere('e.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\GivenAnswer;
|
||||
use App\Entity\Question;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
@@ -22,22 +21,21 @@ class QuestionRepository extends ServiceEntityRepository
|
||||
|
||||
public function findNextQuestionForCandidate(Candidate $candidate): ?Question
|
||||
{
|
||||
$qb = $this->createQueryBuilder('q');
|
||||
|
||||
return $qb->join('q.quiz', 'qz')
|
||||
->andWhere($qb->expr()->notIn('q.id', $this->getEntityManager()->createQueryBuilder()
|
||||
->select('q1')
|
||||
->from(GivenAnswer::class, 'ga')
|
||||
->join('ga.answer', 'a')
|
||||
->join('a.question', 'q1')
|
||||
->andWhere($qb->expr()->isNotNull('ga.answer'))
|
||||
->andWhere('ga.candidate = :candidate')
|
||||
->andWhere('q1.quiz = :quiz')
|
||||
->getDQL()))
|
||||
->andWhere('qz = :quiz')
|
||||
return $this->getEntityManager()->createQuery(<<<DQL
|
||||
select q from App\Entity\Question q
|
||||
join q.quiz qz
|
||||
where q.id not in (
|
||||
select q1.id from App\Entity\GivenAnswer ga
|
||||
join ga.answer a
|
||||
join a.question q1
|
||||
where ga.candidate = :candidate
|
||||
and q1.quiz = :quiz
|
||||
)
|
||||
and qz = :quiz
|
||||
DQL)
|
||||
->setMaxResults(1)
|
||||
->setParameter('candidate', $candidate)
|
||||
->setParameter('quiz', $candidate->getSeason()->getActiveQuiz())
|
||||
->getQuery()->getOneOrNullResult();
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
|
||||
47
src/Repository/QuizCandidateRepository.php
Normal file
47
src/Repository/QuizCandidateRepository.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\QuizCandidate;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<QuizCandidate>
|
||||
*/
|
||||
class QuizCandidateRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, QuizCandidate::class);
|
||||
}
|
||||
|
||||
/** @return bool true if a new entry was created */
|
||||
public function createIfNotExist(Quiz $quiz, Candidate $candidate): bool
|
||||
{
|
||||
if (0 !== $this->count(['candidate' => $candidate, 'quiz' => $quiz])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$quizCandidate = new QuizCandidate($quiz, $candidate);
|
||||
$this->getEntityManager()->persist($quizCandidate);
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,59 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Elimination;
|
||||
use App\Entity\GivenAnswer;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\QuizCandidate;
|
||||
use App\Exception\ErrorClearingQuizException;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Quiz>
|
||||
*/
|
||||
class QuizRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
public function __construct(ManagerRegistry $registry, private readonly LoggerInterface $logger)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,9 @@ class SeasonRepository extends ServiceEntityRepository
|
||||
/** @return list<Season> Returns an array of Season objects */
|
||||
public function getSeasonsForUser(User $user): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('s')
|
||||
->where(':user MEMBER OF s.owners')
|
||||
->orderBy('s.name')
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
return $this->getEntityManager()->createQuery(<<<DQL
|
||||
select s from App\Entity\Season s where :user member of s.owners order by s.name
|
||||
DQL
|
||||
)->setParameter('user', $user)->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Correction;
|
||||
use App\Entity\SeasonSettings;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Correction>
|
||||
* @extends ServiceEntityRepository<SeasonSettings>
|
||||
*/
|
||||
class CorrectionRepository extends ServiceEntityRepository
|
||||
class SeasonSettingsRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Correction::class);
|
||||
parent::__construct($registry, SeasonSettings::class);
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,19 @@ use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
|
||||
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
|
||||
|
||||
class EmailVerifier
|
||||
readonly class EmailVerifier
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VerifyEmailHelperInterface $verifyEmailHelper,
|
||||
private readonly MailerInterface $mailer,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private VerifyEmailHelperInterface $verifyEmailHelper,
|
||||
private MailerInterface $mailer,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/** @throws TransportExceptionInterface */
|
||||
public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $email): void
|
||||
{
|
||||
$signatureComponents = $this->verifyEmailHelper->generateSignature(
|
||||
@@ -39,7 +40,6 @@ class EmailVerifier
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
|
||||
/** @throws VerifyEmailExceptionInterface */
|
||||
public function handleEmailConfirmation(Request $request, User $user): void
|
||||
{
|
||||
$this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), (string) $user->getEmail());
|
||||
|
||||
@@ -4,10 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\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 Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/** @extends Voter<string, Season> */
|
||||
@@ -22,11 +27,18 @@ final class SeasonVoter extends Voter
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
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 */
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
/** @param Season|Elimination|Quiz|Candidate|Answer|Question $subject */
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
if (!$user instanceof User) {
|
||||
@@ -37,7 +49,24 @@ final class SeasonVoter extends Voter
|
||||
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) {
|
||||
self::EDIT, self::DELETE, self::ELIMINATION => $season->isOwner($user),
|
||||
|
||||
@@ -8,8 +8,9 @@ use App\Entity\Answer;
|
||||
use App\Entity\Question;
|
||||
use App\Entity\Quiz;
|
||||
use App\Exception\SpreadsheetDataException;
|
||||
use PhpOffice\PhpSpreadsheet\Reader;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Writer;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
|
||||
class QuizSpreadsheetService
|
||||
@@ -64,7 +65,7 @@ class QuizSpreadsheetService
|
||||
|
||||
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
|
||||
*/
|
||||
private function fillQuizFromArray(Quiz $quiz, array $sheet): Quiz
|
||||
private function fillQuizFromArray(Quiz $quiz, array $sheet): void
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
@@ -109,15 +110,13 @@ class QuizSpreadsheetService
|
||||
if ([] !== $errors) {
|
||||
throw new SpreadsheetDataException($errors);
|
||||
}
|
||||
|
||||
return $quiz;
|
||||
}
|
||||
|
||||
public function quizToXlsx(Quiz $quiz): void {}
|
||||
|
||||
private function toXlsx(Spreadsheet $spreadsheet): \Closure
|
||||
{
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$writer = new Writer\Xlsx($spreadsheet);
|
||||
|
||||
return static fn () => $writer->save('php://output');
|
||||
}
|
||||
|
||||
12
symfony.lock
12
symfony.lock
@@ -213,6 +213,18 @@
|
||||
"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": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% block importmap %}{{ importmap('backoffice') }}{% endblock %}
|
||||
{% block title %}Tijd voor de test | {% endblock %}
|
||||
{% block nav %}{{ include('backoffice/nav.html.twig') }}{% endblock %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
|
||||
{% block title %}Hello BackofficeController!{% endblock %}
|
||||
{% block title %}{{ parent() }}Backoffice{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
@@ -11,43 +11,45 @@
|
||||
{{ 'Add'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<th scope="col">{{ 'Owner(s)'|trans }}</th>
|
||||
{% endif %}
|
||||
<th scope="col">{{ 'Name'|trans }}</th>
|
||||
<th scope="col">{{ 'Active Quiz'|trans }}</th>
|
||||
<th scope="col">{{ 'Season Code'|trans }}</th>
|
||||
<th scope="col">{{ 'Manage'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for season in seasons %}
|
||||
<tr class="align-middle">
|
||||
{% if seasons %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
|
||||
<th scope="col">{{ 'Owner(s)'|trans }}</th>
|
||||
{% endif %}
|
||||
<td>{{ season.name }}</td>
|
||||
<td>
|
||||
{% if season.activeQuiz %}
|
||||
{{ season.activeQuiz.name }}
|
||||
{% else %}
|
||||
{{ 'No active quiz'|trans }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a {% if season.activeQuiz %}href="{{ path('app_quiz_entername', {seasonCode: season.seasonCode}) }}"
|
||||
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
|
||||
</td>
|
||||
<th scope="col">{{ 'Name'|trans }}</th>
|
||||
<th scope="col">{{ 'Active Quiz'|trans }}</th>
|
||||
<th scope="col">{{ 'Season Code'|trans }}</th>
|
||||
<th scope="col">{{ 'Manage'|trans }}</th>
|
||||
</tr>
|
||||
{% else %}
|
||||
EMPTY
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for season in seasons %}
|
||||
<tr class="align-middle">
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
|
||||
{% endif %}
|
||||
<td>{{ season.name }}</td>
|
||||
<td>
|
||||
{% if season.activeQuiz %}
|
||||
{{ season.activeQuiz.name }}
|
||||
{% else %}
|
||||
{{ 'No active quiz'|trans }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a {% if season.activeQuiz %}href="{{ path('app_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
|
||||
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{{ 'You have no seasons yet.'|trans }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
|
||||
{% block title %}{{ parent() }}{{ quiz.season.name }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<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 %}"
|
||||
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
|
||||
{% if quiz is same as (season.activeQuiz) %}
|
||||
<a class="btn btn-secondary"
|
||||
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a>
|
||||
{% 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 id="questions">
|
||||
@@ -45,7 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
EMPTY
|
||||
{{ 'EMPTY'|trans }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,10 +82,10 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ 'Candidate'|trans }}</th>
|
||||
<th scope="col">{{ 'Correct Answers'|trans }}</th>
|
||||
<th scope="col">{{ 'Corrections'|trans }}</th>
|
||||
<th scope="col">{{ 'Score'|trans }}</th>
|
||||
<th scope="col">{{ 'Time'|trans }}</th>
|
||||
<th style="width: 15%" scope="col">{{ 'Correct Answers'|trans }}</th>
|
||||
<th style="width: 20%" scope="col">{{ 'Corrections'|trans }}</th>
|
||||
<th style="width: 10%" scope="col">{{ 'Score'|trans }}</th>
|
||||
<th style="width: 20%" scope="col">{{ 'Time'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -85,7 +93,21 @@
|
||||
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
|
||||
<td>{{ candidate.name }}</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.time }}</td>
|
||||
</tr>
|
||||
@@ -97,16 +119,48 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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 %}
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
|
||||
{{ parent() }}Backoffice
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
{% block title %}{{ parent() }}{{ season.name }}{% endblock %}
|
||||
{% block body %}
|
||||
<h2 class="py-2">{{ 'Season'|trans }}: {{ season.name }}</h2>
|
||||
<div class="row">
|
||||
@@ -21,13 +22,18 @@
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<h4 class="py-2 pe-2">{{ 'Candidates'|trans }}</h4>
|
||||
<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>
|
||||
<ul>
|
||||
{% for candidate in season.candidates %}
|
||||
<li>{{ candidate.name }}</li>{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<h4 class="py-2 pe-2">{{ 'Settings'|trans }}</h4>
|
||||
</div>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
<div class="col-12 col-md-3"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
{% extends 'quiz/base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<img src="{{ asset("img/#{colour}.png") }}" class="elimination-screen" id="{{ colour }}"
|
||||
alt="Screen with colour {{ colour }}">
|
||||
<img src="{{ asset("img/#{colour}.png") }}"
|
||||
class="elimination-screen" id="{{ colour }}"
|
||||
alt="Screen with colour {{ colour }}"
|
||||
data-controller="elimination"
|
||||
data-action="click->elimination#next keydown@document->elimination#next"
|
||||
tabindex="0"
|
||||
>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
{% extends 'quiz/base.html.twig' %}
|
||||
{% block body %}
|
||||
{{ question.question }}<br/>
|
||||
<h4>
|
||||
{% if season.settings.showNumbers %}
|
||||
({{ question.ordering }}/{{ question.quiz.questions.count }})
|
||||
{% endif %}{{ question.question }}<br/>
|
||||
</h4>
|
||||
<form method="post">
|
||||
<input type="hidden" name="token" value="{{ csrf_token('question') }}">
|
||||
{% for answer in question.answers %}
|
||||
<div>
|
||||
<button class="btn btn-outline-success"
|
||||
type="submit"
|
||||
name="answer"
|
||||
value="{{ answer.id }}">{{ answer.text }}</button>
|
||||
</div>
|
||||
{% if season.settings.confirmAnswers == false %}
|
||||
{% for answer in question.answers %}
|
||||
<div class="py-2">
|
||||
<button class="btn btn-outline-success"
|
||||
type="submit"
|
||||
name="answer"
|
||||
value="{{ answer.id }}">{{ answer.text }}</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
Weirdly enough this question has no answers...
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
{% endblock body %}
|
||||
|
||||
77
tests/Security/Voter/SeasonVoterTest.php
Normal file
77
tests/Security/Voter/SeasonVoterTest.php
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
new Dotenv()->bootEnv(dirname(__DIR__).'/.env');
|
||||
|
||||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
<source>Already have an account? Log in</source>
|
||||
<target>Heb je al een account? Log in</target>
|
||||
</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">
|
||||
<source>Back</source>
|
||||
<target>Terug</target>
|
||||
@@ -49,6 +57,10 @@
|
||||
<source>Candidates</source>
|
||||
<target>Kandidaten</target>
|
||||
</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">
|
||||
<source>Correct Answers</source>
|
||||
<target>Goede antwoorden</target>
|
||||
@@ -77,10 +89,18 @@
|
||||
<source>Deactivate Quiz</source>
|
||||
<target>Deactiveer test</target>
|
||||
</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">
|
||||
<source>Download Template</source>
|
||||
<target>Download sjabloon</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="FfYlwX8" resname="EMPTY">
|
||||
<source>EMPTY</source>
|
||||
<target>LEEG</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="JZi_tm0" resname="Email">
|
||||
<source>Email</source>
|
||||
<target>E-mail</target>
|
||||
@@ -91,7 +111,11 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="RnI7jJT" resname="Enter your name">
|
||||
<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 id="OGiIhMH" resname="Green">
|
||||
<source>Green</source>
|
||||
@@ -125,6 +149,14 @@
|
||||
<source>Name</source>
|
||||
<target>Naam</target>
|
||||
</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">
|
||||
<source>No active quiz</source>
|
||||
<target>Geen actieve test</target>
|
||||
@@ -145,9 +177,13 @@
|
||||
<source>Password</source>
|
||||
<target>Wachtwoord</target>
|
||||
</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">
|
||||
<source>Please Confirm your Email</source>
|
||||
<target>messages</target>
|
||||
<target>Bevestig je e-mailadres alsjeblieft</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="lSX_PHJ" resname="Please sign in">
|
||||
<source>Please sign in</source>
|
||||
@@ -177,10 +213,18 @@
|
||||
<source>Quiz Added!</source>
|
||||
<target>Test toegevoegd!</target>
|
||||
</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">
|
||||
<source>Quiz completed</source>
|
||||
<target>Test voltooid</target>
|
||||
</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">
|
||||
<source>Quiz name</source>
|
||||
<target>Testnaam</target>
|
||||
@@ -233,14 +277,14 @@
|
||||
<source>Seasons</source>
|
||||
<target>Seizoenen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="VXFwlwn" resname="Settings">
|
||||
<source>Settings</source>
|
||||
<target>Instellingen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="pNIxNSX" resname="Sign in">
|
||||
<source>Sign in</source>
|
||||
<target>Log in</target>
|
||||
</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">
|
||||
<source>Submit</source>
|
||||
<target>Verstuur</target>
|
||||
@@ -253,10 +297,22 @@
|
||||
<source>There are no answers for this question</source>
|
||||
<target>Er zijn geen antwoorden voor deze vraag</target>
|
||||
</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">
|
||||
<source>Time</source>
|
||||
<target>Tijd</target>
|
||||
</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">
|
||||
<source>Your Seasons</source>
|
||||
<target>Jouw seizoenen</target>
|
||||
|
||||
Reference in New Issue
Block a user