38 Commits

Author SHA1 Message Date
f6715de330 Add trailing comma in multi line. 2025-06-05 15:25:03 +02:00
0be29190e6 Fix scss 2025-06-01 21:36:39 +02:00
c6f9b57c60 Refactor translations to XLIFF format, enhance elimination workflows, and update compose configuration
This commit switches translations from YAML to XLIFF format for better standardization, updates the elimination preparation process with UI and functionality improvements, tweaks form structures, adjusts compose.override.yaml for improved asset handling, and optimizes back office usability with refined translation handling.
2025-06-01 21:28:56 +02:00
cd5946bda8 Hotfix Sass build 2025-06-01 15:45:15 +02:00
25aa8b8622 Introduce importmap, enhance elimination workflows, and update assets
This commit adds initial importmap configuration to manage assets, updates the elimination preparation workflow with form enhancements and database changes, introduces new styles and JS assets, refines translations, and improves entity handling with an input bag update method.
2025-06-01 15:40:47 +02:00
d3e5cb0569 Refactor elimination feature and improve backoffice usability
This commit introduces a refactored EliminationFactory for better modularity, updates the elimination preparation process, and adds functionality to view eliminations. Backoffice templates and forms have been reorganized, minor translations were corrected, and additional assets like styles and flashes were included for enhanced user experience.
2025-06-01 15:40:47 +02:00
e0350c8c31 WIP 2025-06-01 15:40:47 +02:00
58bda32f09 Update compose.yaml
Add always to db
2025-05-08 17:08:16 +02:00
0cd23f906e Create devcontainer.json 2025-05-05 12:34:04 +02:00
Marijn Doeve
f1fe827337 Add Sentry Replay 2025-04-30 14:59:41 +02:00
Marijn Doeve
ca08df26b2 Add Sentry config 2025-04-29 08:34:55 +02:00
Marijn Doeve
60723657c2 Add missing env vars 2025-04-29 08:17:50 +02:00
09d74ed327 Add lost file 2025-04-29 07:55:17 +02:00
49b7c0f5d5 Add Sheet upload function 2025-04-28 23:01:37 +02:00
9bae324447 Enhance login and navigation templates with authentication checks 2025-04-23 23:35:08 +02:00
4712c01688 Add quiz management features and improve UI 2025-04-23 23:23:22 +02:00
4e22feb256 Update framework.yaml 2025-04-22 23:49:20 +02:00
0f07e7eabf Phpstan 2025-04-22 23:42:07 +02:00
7b05e52d95 A lot 2025-04-22 22:25:18 +02:00
66b57ea84a Secure admin 2025-04-21 19:12:48 +02:00
daeda4a9b5 Add Sentry integration and configuration 2025-04-21 18:54:43 +02:00
057bc778ed Fixes 2025-04-21 18:03:54 +02:00
f44b63a91f Error code for wrong season code 2025-04-21 15:47:27 +02:00
acd85bfc2b Fix csrf-tokens 2025-04-21 14:09:02 +02:00
4863fad3ba Move fixtures to prod 2025-04-21 13:21:16 +02:00
87104889d1 trailing spaces 2025-04-21 00:33:45 +02:00
2c6eb2ecb7 New caddy 2025-04-21 00:27:19 +02:00
fe00270637 Labels 2025-04-20 21:51:41 +02:00
e967a8da63 add network to db 2025-04-20 21:41:00 +02:00
ffb7df5895 Update postgres version: 2025-04-20 21:39:30 +02:00
4885f746f6 Change ports 2025-04-20 21:37:17 +02:00
4bcab2724a Implement email verification feature, add registration form, and update user entity for verification status 2025-04-20 19:34:27 +02:00
c70f713f7e Refactor Base64 encoding/decoding methods for consistency, update controller routes, and improve CI configuration
Some checks failed
CI / Tests (push) Failing after 9m39s
CI / Docker Lint (push) Successful in 4s
2025-04-14 18:30:18 +02:00
31e6ed406b Refactor routes for consistency, update season codes, and add Justfile for Docker commands 2025-04-02 22:12:55 +02:00
acf5c06fcc Refactor code for improved readability and consistency; add flash message handling and enhance quiz functionality
Some checks failed
CI / Tests (push) Failing after 9s
CI / Docker Lint (push) Successful in 4s
2025-03-12 23:18:13 +01:00
448daed6ea wip 12-03-2025
Some checks failed
CI / Tests (push) Failing after 11s
CI / Docker Lint (push) Successful in 3s
2025-03-12 09:28:36 +01:00
f7b4b98da4 Refactor YAML and Twig files for consistent indentation and formatting
Some checks failed
CI / Tests (push) Failing after 10m32s
CI / Docker Lint (push) Successful in 9s
2025-03-05 22:47:59 +01:00
0ccce51af8 Add AbstractController, implement flash message handling, and refactor repositories 2025-03-05 21:01:57 +01:00
148 changed files with 8228 additions and 2055 deletions

View File

@@ -0,0 +1,6 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {"ghcr.io/devcontainers/features/php:1": {
"version": "8.3"
}}
}

View File

@@ -33,6 +33,7 @@ indent_size = 4
[*.{yaml,yml}] [*.{yaml,yml}]
trim_trailing_whitespace = false trim_trailing_whitespace = false
indent_size = 2
[.github/workflows/*.yml] [.github/workflows/*.yml]
indent_size = 2 indent_size = 2
@@ -53,5 +54,5 @@ indent_size = 2
[*.*Dockerfile] [*.*Dockerfile]
indent_style = tab indent_style = tab
[*.*Caddyfile] [{*.*Caddyfile,Caddyfile}]
indent_style = tab indent_style = tab

8
.env
View File

@@ -28,3 +28,11 @@ APP_SECRET=
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
MAILER_DSN=null://null
###< symfony/mailer ###
###> sentry/sentry-symfony ###
SENTRY_DSN=
###< sentry/sentry-symfony ###

View File

@@ -2,3 +2,4 @@
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
APP_SECRET=e26b9552d9e7f969b160373effaa7690 APP_SECRET=e26b9552d9e7f969b160373effaa7690
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
MAILER_SENDER=info@tijdvoordetest.nl

4
.env.test Normal file
View File

@@ -0,0 +1,4 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999

View File

@@ -16,14 +16,11 @@ jobs:
name: Tests name: Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- - name: Set up Docker Buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- - name: Build Docker images
name: Build Docker images
uses: docker/bake-action@v4 uses: docker/bake-action@v4
with: with:
pull: true pull: true
@@ -35,42 +32,34 @@ jobs:
*.cache-from=type=gha,scope=${{github.ref}} *.cache-from=type=gha,scope=${{github.ref}}
*.cache-from=type=gha,scope=refs/heads/main *.cache-from=type=gha,scope=refs/heads/main
*.cache-to=type=gha,scope=${{github.ref}},mode=max *.cache-to=type=gha,scope=${{github.ref}},mode=max
- - name: Start services
name: Start services run: docker compose up php database --wait --no-build
run: docker compose up --wait --no-build - name: Lint Twig templates
- run: docker compose exec -T php bin/console lint:twig --format=github templates
name: Check HTTP reachability - name: Coding Style
run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none
- name: Twig Coding Style
run: docker compose exec -T php vendor/bin/twig-cs-fixer check
- name: Check HTTP reachability
run: curl -v --fail-with-body http://localhost run: curl -v --fail-with-body http://localhost
- - name: Check Mercure reachability
name: Check HTTPS reachability if: false
if: false # Remove this line when the homepage will be configured, or change the path to check
run: curl -vk --fail-with-body https://localhost
-
name: Check Mercure reachability
run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test
- - name: Create test database
name: Create test database
if: false # Remove this line if Doctrine ORM is installed
run: docker compose exec -T php bin/console -e test doctrine:database:create run: docker compose exec -T php bin/console -e test doctrine:database:create
- - name: Run migrations
name: Run migrations
if: false # Remove this line if Doctrine Migrations is installed
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
- - name: Run PHPUnit
name: Run PHPUnit if: false # Remove this line when the tests are ready
if: false # Remove this line if PHPUnit is installed run: docker compose exec -T php vendor/bin/phpunit
run: docker compose exec -T php bin/phpunit - name: Doctrine Schema Validator
-
name: Doctrine Schema Validator
if: false # Remove this line if Doctrine ORM is installed
run: docker compose exec -T php bin/console -e test doctrine:schema:validate run: docker compose exec -T php bin/console -e test doctrine:schema:validate
lint: lint:
name: Docker Lint name: Docker Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- - name: Lint Dockerfile
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.1.0 uses: hadolint/hadolint-action@v3.1.0

20
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/frankenphp/data
### Generated by gibo (https://github.com/simonwhitaker/gibo) ### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/6eeebe6f49678aacd8311ce079842c971b3ebe96/Symfony.gitignore ### https://raw.github.com/github/gitignore/6eeebe6f49678aacd8311ce079842c971b3ebe96/Symfony.gitignore
@@ -32,7 +34,6 @@
/bin/* /bin/*
!bin/console !bin/console
!bin/symfony_requirements !bin/symfony_requirements
/vendor/
# Assets and user uploads # Assets and user uploads
/web/bundles/ /web/bundles/
@@ -40,7 +41,6 @@
# PHPUnit # PHPUnit
/app/phpunit.xml /app/phpunit.xml
/phpunit.xml
# Build data # Build data
/build/ /build/
@@ -103,5 +103,19 @@ phpstan.neon
###< friendsofphp/php-cs-fixer ### ###< friendsofphp/php-cs-fixer ###
###> phpunit/phpunit ### ###> phpunit/phpunit ###
/phpunit.xml /phpunit.xml
.phpunit.result.cache /.phpunit.cache/
###< phpunit/phpunit ### ###< phpunit/phpunit ###
###> vincentlanglet/twig-cs-fixer ###
/.twig-cs-fixer.cache
###< vincentlanglet/twig-cs-fixer ###
###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###

View File

@@ -6,7 +6,6 @@
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" /> <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" />
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/ndjson-react" /> <excludeFolder url="file://$MODULE_DIR$/vendor/clue/ndjson-react" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" /> <excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" /> <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" /> <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" /> <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" />
@@ -48,8 +47,6 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/rector/rector" /> <excludeFolder url="file://$MODULE_DIR$/vendor/rector/rector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/runtime/frankenphp-symfony" /> <excludeFolder url="file://$MODULE_DIR$/vendor/runtime/frankenphp-symfony" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
@@ -84,8 +81,6 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php83" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
@@ -106,7 +101,6 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/form" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/form" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-icu" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-icu" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-bundle" />
@@ -133,9 +127,47 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/html-extra" /> <excludeFolder url="file://$MODULE_DIR$/vendor/twig/html-extra" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/data-fixtures" /> <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/data-fixtures" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-fixtures-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/vincentlanglet/twig-cs-fixer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/vendor/egulias/email-validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
<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" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sentry/sentry" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sentry/sentry-symfony" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/psr-http-message-bridge" />
<excludeFolder url="file://$MODULE_DIR$/.phpunit.cache" />
<excludeFolder url="file://$MODULE_DIR$/var" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/phpunit-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/serializer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/maennchen/zipstream-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/complex" />
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/matrix" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset-mapper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/sass-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/intl-extra" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="bootstrap" level="application" /> <orderEntry type="library" name="bootstrap" level="application" />
<orderEntry type="library" name="bootstrap-icons" level="application" />
</component> </component>
</module> </module>

View File

@@ -1,7 +1,133 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="ForgottenDebugOutputInspection" enabled="true" level="ERROR" enabled_by_default="true">
<option name="configuration">
<list>
<option value="\Codeception\Util\Debug::debug" />
<option value="\Codeception\Util\Debug::pause" />
<option value="\Doctrine\Common\Util\Debug::dump" />
<option value="\Doctrine\Common\Util\Debug::export" />
<option value="\Illuminate\Support\Debug\Dumper::dump" />
<option value="\Symfony\Component\Debug\Debug::enable" />
<option value="\Symfony\Component\Debug\DebugClassLoader::enable" />
<option value="\Symfony\Component\Debug\ErrorHandler::register" />
<option value="\Symfony\Component\Debug\ExceptionHandler::register" />
<option value="\TYPO3\CMS\Core\Utility\DebugUtility::debug" />
<option value="\Zend\Debug\Debug::dump" />
<option value="\Zend\Di\Display\Console::export" />
<option value="dd" />
<option value="debug_print_backtrace" />
<option value="debug_zval_dump" />
<option value="dpm" />
<option value="dpq" />
<option value="dsm" />
<option value="dump" />
<option value="dvm" />
<option value="error_log" />
<option value="kpr" />
<option value="phpinfo" />
<option value="print_r" />
<option value="trap" />
<option value="var_dump" />
<option value="var_export" />
<option value="wp_die" />
<option value="xdebug_break" />
<option value="xdebug_call_class" />
<option value="xdebug_call_file" />
<option value="xdebug_call_function" />
<option value="xdebug_call_line" />
<option value="xdebug_code_coverage_started" />
<option value="xdebug_debug_zval" />
<option value="xdebug_debug_zval_stdout" />
<option value="xdebug_dump_superglobals" />
<option value="xdebug_enable" />
<option value="xdebug_get_code_coverage" />
<option value="xdebug_get_collected_errors" />
<option value="xdebug_get_declared_vars" />
<option value="xdebug_get_function_stack" />
<option value="xdebug_get_headers" />
<option value="xdebug_get_monitored_functions" />
<option value="xdebug_get_profiler_filename" />
<option value="xdebug_get_stack_depth" />
<option value="xdebug_get_tracefile_name" />
<option value="xdebug_is_enabled" />
<option value="xdebug_memory_usage" />
<option value="xdebug_peak_memory_usage" />
<option value="xdebug_print_function_stack" />
<option value="xdebug_start_code_coverage" />
<option value="xdebug_start_error_collection" />
<option value="xdebug_start_function_monitor" />
<option value="xdebug_start_trace" />
<option value="xdebug_stop_code_coverage" />
<option value="xdebug_stop_error_collection" />
<option value="xdebug_stop_function_monitor" />
<option value="xdebug_stop_trace" />
<option value="xdebug_time_index" />
<option value="xdebug_var_dump" />
</list>
</option>
<option name="migratedIntoUserSpace" value="true" />
</inspection_tool>
<inspection_tool class="PhpCSFixerValidationInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="PhpCSFixerValidationInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpFullyQualifiedNameUsageInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="IGNORE_GLOBAL_NAMESPACE" value="true" />
</inspection_tool>
<inspection_tool class="PhpStanGlobal" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="PhpStanGlobal" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="SecurityAdvisoriesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="optionConfiguration">
<list>
<option value="barryvdh/laravel-debugbar" />
<option value="behat/behat" />
<option value="brianium/paratest" />
<option value="codeception/codeception" />
<option value="codedungeon/phpunit-result-printer" />
<option value="composer/composer" />
<option value="doctrine/coding-standard" />
<option value="filp/whoops" />
<option value="friendsofphp/php-cs-fixer" />
<option value="humbug/humbug" />
<option value="infection/infection" />
<option value="jakub-onderka/php-parallel-lint" />
<option value="johnkary/phpunit-speedtrap" />
<option value="kalessil/production-dependencies-guard" />
<option value="mikey179/vfsStream" />
<option value="mockery/mockery" />
<option value="mybuilder/phpunit-accelerator" />
<option value="orchestra/testbench" />
<option value="pdepend/pdepend" />
<option value="phan/phan" />
<option value="phing/phing" />
<option value="phpcompatibility/php-compatibility" />
<option value="phpmd/phpmd" />
<option value="phpro/grumphp" />
<option value="phpspec/phpspec" />
<option value="phpspec/prophecy" />
<option value="phpstan/phpstan" />
<option value="phpunit/phpunit" />
<option value="povils/phpmnd" />
<option value="roave/security-advisories" />
<option value="satooshi/php-coveralls" />
<option value="sebastian/phpcpd" />
<option value="slevomat/coding-standard" />
<option value="spatie/phpunit-watcher" />
<option value="squizlabs/php_codesniffer" />
<option value="sstalle/php7cc" />
<option value="symfony/debug" />
<option value="symfony/maker-bundle" />
<option value="symfony/phpunit-bridge" />
<option value="symfony/var-dumper" />
<option value="vimeo/psalm" />
<option value="wimg/php-compatibility" />
<option value="wp-coding-standards/wpcs" />
<option value="yiisoft/yii2-coding-standards" />
<option value="yiisoft/yii2-debug" />
<option value="yiisoft/yii2-gii" />
<option value="zendframework/zend-coding-standard" />
<option value="zendframework/zend-debug" />
<option value="zendframework/zend-test" />
</list>
</option>
</inspection_tool>
</profile> </profile>
</component> </component>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="JavaScriptLibraryMappings"> <component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{bootstrap}" /> <file url="PROJECT" libraries="{bootstrap, bootstrap-icons}" />
</component> </component>
</project> </project>

View File

@@ -5,7 +5,7 @@
<tool tool_name="PHPUnit"> <tool tool_name="PHPUnit">
<cache> <cache>
<versions> <versions>
<info id="interpreter-c1266788-d465-407a-ac5d-1f67a9cf3e8a" version="11.5.2" /> <info id="interpreter-96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" version="12.1.2" />
</versions> </versions>
</cache> </cache>
</tool> </tool>

299
.idea/php.xml generated
View File

@@ -1,8 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="LaravelPint">
<laravel_pint_settings>
<laravel_pint_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8">
<option name="timeout" value="30000" />
</laravel_pint_by_interpreter>
</laravel_pint_settings>
</component>
<component name="MessDetector"> <component name="MessDetector">
<phpmd_settings> <phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" timeout="30000" /> <phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="30000" />
</phpmd_settings> </phpmd_settings>
</component> </component>
<component name="MessDetectorOptionsConfiguration"> <component name="MessDetectorOptionsConfiguration">
@@ -10,7 +17,7 @@
</component> </component>
<component name="PHPCSFixerOptionsConfiguration"> <component name="PHPCSFixerOptionsConfiguration">
<option name="codingStandard" value="Custom" /> <option name="codingStandard" value="Custom" />
<option name="rulesetPath" value="/app/.php-cs-fixer.dist.php" /> <option name="rulesetPath" value=".php-cs-fixer.dist.php" />
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
<component name="PHPCodeSnifferOptionsConfiguration"> <component name="PHPCodeSnifferOptionsConfiguration">
@@ -19,14 +26,14 @@
</component> </component>
<component name="PhpCSFixer"> <component name="PhpCSFixer">
<phpcsfixer_settings> <phpcsfixer_settings>
<phpcs_fixer_by_interpreter interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" tool_path="/app/vendor/bin/php-cs-fixer" timeout="30000" /> <phpcs_fixer_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" standards="DoctrineAnnotation;PER;PER-CS;PER-CS1.0;PER-CS2.0;PHP54Migration;PHP56Migration;PHP70Migration;PHP71Migration;PHP73Migration;PHP74Migration;PHP80Migration;PHP81Migration;PHP82Migration;PHP83Migration;PHP84Migration;PHPUnit100Migration;PHPUnit30Migration;PHPUnit32Migration;PHPUnit35Migration;PHPUnit43Migration;PHPUnit48Migration;PHPUnit50Migration;PHPUnit52Migration;PHPUnit54Migration;PHPUnit55Migration;PHPUnit56Migration;PHPUnit57Migration;PHPUnit60Migration;PHPUnit75Migration;PHPUnit84Migration;PHPUnit91Migration;PSR1;PSR12;PSR2;PhpCsFixer;Symfony" tool_path="vendor/bin/php-cs-fixer" timeout="30000" />
<phpcs_fixer_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" standards="DoctrineAnnotation;PER;PER-CS;PER-CS1.0;PER-CS2.0;PHP54Migration;PHP56Migration;PHP70Migration;PHP71Migration;PHP73Migration;PHP74Migration;PHP80Migration;PHP81Migration;PHP82Migration;PHP83Migration;PHP84Migration;PHPUnit100Migration;PHPUnit30Migration;PHPUnit32Migration;PHPUnit35Migration;PHPUnit43Migration;PHPUnit48Migration;PHPUnit50Migration;PHPUnit52Migration;PHPUnit54Migration;PHPUnit55Migration;PHPUnit56Migration;PHPUnit57Migration;PHPUnit60Migration;PHPUnit75Migration;PHPUnit84Migration;PHPUnit91Migration;PSR1;PSR12;PSR2;PhpCsFixer;Symfony" tool_path="/app/vendor/bin/php-cs-fixer" timeout="30000" /> <PhpCSFixerConfiguration deletedFromTheList="true" standards="PSR1;PSR2;Symfony;DoctrineAnnotation;PHP70Migration;PHP71Migration" tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
<PhpCSFixerConfiguration deletedFromTheList="true" tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" /> <phpcs_fixer_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" standards="DoctrineAnnotation;PER;PER-CS;PER-CS1.0;PER-CS2.0;PHP54Migration;PHP56Migration;PHP70Migration;PHP71Migration;PHP73Migration;PHP74Migration;PHP80Migration;PHP81Migration;PHP82Migration;PHP83Migration;PHP84Migration;PHPUnit100Migration;PHPUnit30Migration;PHPUnit32Migration;PHPUnit35Migration;PHPUnit43Migration;PHPUnit48Migration;PHPUnit50Migration;PHPUnit52Migration;PHPUnit54Migration;PHPUnit55Migration;PHPUnit56Migration;PHPUnit57Migration;PHPUnit60Migration;PHPUnit75Migration;PHPUnit84Migration;PHPUnit91Migration;PSR1;PSR12;PSR2;PhpCsFixer;Symfony" tool_path="vendor/bin/php-cs-fixer" timeout="30000" />
</phpcsfixer_settings> </phpcsfixer_settings>
</component> </component>
<component name="PhpCodeSniffer"> <component name="PhpCodeSniffer">
<phpcs_settings> <phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" timeout="30000" /> <phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="30000" />
</phpcs_settings> </phpcs_settings>
</component> </component>
<component name="PhpExternalFormatter"> <component name="PhpExternalFormatter">
@@ -34,140 +41,169 @@
</component> </component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" /> <path value="$PROJECT_DIR$/vendor/react/stream" />
<path value="$PROJECT_DIR$/vendor/symfony/string" /> <path value="$PROJECT_DIR$/vendor/react/promise" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
<path value="$PROJECT_DIR$/vendor/symfony/config" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/rector/rector" />
<path value="$PROJECT_DIR$/vendor/react/event-loop" /> <path value="$PROJECT_DIR$/vendor/react/event-loop" />
<path value="$PROJECT_DIR$/vendor/react/cache" /> <path value="$PROJECT_DIR$/vendor/react/cache" />
<path value="$PROJECT_DIR$/vendor/react/stream" />
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
<path value="$PROJECT_DIR$/vendor/react/promise" />
<path value="$PROJECT_DIR$/vendor/react/dns" />
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/react/socket" />
<path value="$PROJECT_DIR$/vendor/react/child-process" /> <path value="$PROJECT_DIR$/vendor/react/child-process" />
<path value="$PROJECT_DIR$/vendor/evenement/evenement" /> <path value="$PROJECT_DIR$/vendor/jean85/pretty-package-versions" />
<path value="$PROJECT_DIR$/vendor/react/dns" />
<path value="$PROJECT_DIR$/vendor/react/socket" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/rector/rector" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry-symfony" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-symfony" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-doctrine" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/phpstan/extension-installer" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
<path value="$PROJECT_DIR$/vendor/symfony/form" />
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
<path value="$PROJECT_DIR$/vendor/symfony/config" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
<path value="$PROJECT_DIR$/vendor/symfony/asset-mapper" />
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/phpunit-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" /> <path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" /> <path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
<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" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-twig-component" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/easycorp/easyadmin-bundle" />
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" /> <path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" /> <path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" /> <path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" /> <path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" /> <path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" /> <path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" /> <path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" /> <path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" /> <path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" /> <path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" /> <path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" /> <path value="$PROJECT_DIR$/vendor/symfonycasts/sass-bundle" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" /> <path value="$PROJECT_DIR$/vendor/masterminds/html5" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" /> <path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" /> <path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" /> <path value="$PROJECT_DIR$/vendor/vincentlanglet/twig-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" /> <path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" /> <path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
<path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" /> <path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
<path value="$PROJECT_DIR$/vendor/phpstan/extension-installer" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-doctrine" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-symfony" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/phpstan-safe-rule" /> <path value="$PROJECT_DIR$/vendor/thecodingmachine/phpstan-safe-rule" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" /> <path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/symfony/form" /> <path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/twig/html-extra" /> <path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" /> <path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/easycorp/easyadmin-bundle" /> <path value="$PROJECT_DIR$/vendor/twig/intl-extra" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" /> <path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" /> <path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-twig-component" /> <path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" /> <path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/symfony/intl" /> <path value="$PROJECT_DIR$/vendor/twig/html-extra" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" /> <path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
</include_path> </include_path>
</component> </component>
<component name="PhpInterpreters"> <component name="PhpInterpreters">
<interpreters> <interpreters>
<interpreter id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" name="php" home="docker-compose://DATA" auto="false" debugger_id="php.debugger.XDebug"> <interpreter id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" name="Compose PHP 8.3" home="docker-compose://DATA" auto="false" debugger_id="php.debugger.XDebug">
<remote_data INTERPRETER_PATH="php" HELPERS_PATH="/opt/.phpstorm_helpers" VALID="true" RUN_AS_ROOT_VIA_SUDO="false" DOCKER_ACCOUNT_NAME="Colima" DOCKER_COMPOSE_SERVICE_NAME="php" DOCKER_REMOTE_PROJECT_PATH="/opt/project"> <remote_data INTERPRETER_PATH="php" HELPERS_PATH="/opt/.phpstorm_helpers" VALID="true" RUN_AS_ROOT_VIA_SUDO="false" DOCKER_ACCOUNT_NAME="Colima" DOCKER_COMPOSE_SERVICE_NAME="php" DOCKER_REMOTE_PROJECT_PATH="/opt/project">
<type_data command="EXEC" /> <type_data command="EXEC" />
<dockerComposeConfigurationPaths> <dockerComposeConfigurationPaths>
@@ -181,15 +217,15 @@
</component> </component>
<component name="PhpInterpretersPhpInfoCache"> <component name="PhpInterpretersPhpInfoCache">
<phpInfoCache> <phpInfoCache>
<interpreter name="php"> <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.15"> <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> <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>
<configuration_file>/usr/local/etc/php/php.ini</configuration_file> <configuration_file>/usr/local/etc/php/php.ini</configuration_file>
<configuration_options> <configuration_options>
<configuration_option name="include_path" value=".:/usr/local/lib/php" /> <configuration_option name="include_path" value=".:/usr/local/lib/php" />
</configuration_options> </configuration_options>
<debuggers> <debuggers>
<debugger_info debugger="xdebug" debugger_version="3.4.0"> <debugger_info debugger="xdebug" debugger_version="3.4.2">
<debug_extensions /> <debug_extensions />
</debugger_info> </debugger_info>
</debuggers> </debuggers>
@@ -241,25 +277,24 @@
<component name="PhpProjectSharedConfiguration" php_language_level="8.3" /> <component name="PhpProjectSharedConfiguration" php_language_level="8.3" />
<component name="PhpStan"> <component name="PhpStan">
<PhpStan_settings> <PhpStan_settings>
<phpstan_by_interpreter interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" tool_path="/app/vendor/bin/phpstan" timeout="60000" /> <phpstan_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" tool_path="vendor/bin/phpstan" timeout="60000" />
<phpstan_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="afbe50d9-a569-498c-8dcc-cf68a5d73813" timeout="60000" />
<PhpStanConfiguration deletedFromTheList="true" tool_path="$PROJECT_DIR$/vendor/bin/phpstan" /> <PhpStanConfiguration deletedFromTheList="true" tool_path="$PROJECT_DIR$/vendor/bin/phpstan" />
<phpstan_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" tool_path="vendor/bin/phpstan" timeout="60000" />
</PhpStan_settings> </PhpStan_settings>
</component> </component>
<component name="PhpStanOptionsConfiguration"> <component name="PhpStanOptionsConfiguration">
<option name="config" value="$PROJECT_DIR$/phpstan.dist.neon" /> <option name="config" value="phpstan.dist.neon" />
<option name="level" value="8" /> <option name="fullProject" value="true" />
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
<component name="PhpUnit"> <component name="PhpUnit">
<phpunit_settings> <phpunit_settings>
<phpunit_by_interpreter interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" custom_loader_path="/app/vendor/autoload.php" phpunit_phar_path="" /> <phpunit_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" configuration_file_path="phpunit.xml.dist" custom_loader_path="vendor/autoload.php" phpunit_phar_path="" use_configuration_file="true" />
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings> </phpunit_settings>
</component> </component>
<component name="Psalm"> <component name="Psalm">
<Psalm_settings> <Psalm_settings>
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="afbe50d9-a569-498c-8dcc-cf68a5d73813" timeout="60000" /> <psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="60000" />
</Psalm_settings> </Psalm_settings>
</component> </component>
<component name="PsalmOptionsConfiguration"> <component name="PsalmOptionsConfiguration">

16
.idea/remote-mappings.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteMappingsManager">
<list>
<list>
<remote-mappings server-id="php@96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8">
<settings>
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</settings>
</remote-mappings>
</list>
</list>
</component>
</project>

8
.idea/sonarlint.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SonarLintProjectSettings">
<option name="bindingEnabled" value="true" />
<option name="projectKey" value="MarijnDoeve_TijdVoorDeTest" />
<option name="serverId" value="SonarQube" />
</component>
</project>

6
.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="PostgreSQL" />
</component>
</project>

View File

@@ -1,17 +1,23 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
$finder = (new PhpCsFixer\Finder()) $finder = (new Finder())
->in(__DIR__) ->in(__DIR__)
->exclude('var') ->exclude('var')
->exclude('bin')
; ;
return (new PhpCsFixer\Config()) return (new Config())
->setParallelConfig(ParallelConfigFactory::detect())
->setRules([ ->setRules([
'@Symfony' => true, '@Symfony' => true,
'@Symfony:risky' => true, '@Symfony:risky' => true,
'declare_strict_types' => true, 'declare_strict_types' => true,
'fully_qualified_strict_types' => ['import_symbols' => true],
'linebreak_after_opening_tag' => true, 'linebreak_after_opening_tag' => true,
'mb_str_functions' => true, 'mb_str_functions' => true,
'no_php4_constructor' => true, 'no_php4_constructor' => true,
@@ -19,11 +25,13 @@ return (new PhpCsFixer\Config())
'no_useless_else' => true, 'no_useless_else' => true,
'no_useless_return' => true, 'no_useless_return' => true,
'php_unit_strict' => true, 'php_unit_strict' => true,
'phpdoc_line_span' => ['const' => 'single', 'method' => 'single', 'property' => 'single'],
'phpdoc_order' => true, 'phpdoc_order' => true,
'single_line_empty_body' => true,
'strict_comparison' => true, 'strict_comparison' => true,
'strict_param' => true, 'strict_param' => true,
'blank_line_between_import_groups' => false, 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arguments', 'array_destructuring', 'arrays', 'match', 'parameters']],
'phpdoc_line_span' => ['const' => 'single', 'method' => 'single', 'property' => 'single'],
]) ])
->setRiskyAllowed(true) ->setRiskyAllowed(true)
->setFinder($finder) ->setFinder($finder)

View File

@@ -31,6 +31,9 @@ RUN set -eux; \
intl \ intl \
opcache \ opcache \
zip \ zip \
uuid \
gd \
excimer-1.2.3 \
; ;
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser # https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
@@ -67,6 +70,8 @@ RUN set -eux; \
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/ COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
RUN git config --global --add safe.directory /app
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ] CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ]
# Prod FrankenPHP image # Prod FrankenPHP image
@@ -94,4 +99,7 @@ RUN set -eux; \
composer dump-autoload --classmap-authoritative --no-dev; \ composer dump-autoload --classmap-authoritative --no-dev; \
composer dump-env prod; \ composer dump-env prod; \
composer run-script --no-dev post-install-cmd; \ composer run-script --no-dev post-install-cmd; \
chmod +x bin/console; sync; chmod +x bin/console; \
bin/console sass:build; \
bin/console asset-map:compile --no-debug --quiet --no-ansi; \
sync;

41
Justfile Normal file
View File

@@ -0,0 +1,41 @@
up:
docker compose up -d
down *args:
docker compose down {{ args }} --remove-orphans
stop:
docker compose stop
exec *args:
docker compose exec php {{ args }}
[no-exit-message]
shell:
@docker compose exec php bash
bash: shell
migrate: up
docker compose run --rm php bin/console doctrine:migrations:migrate --no-interaction
fixtures:
docker compose exec php bin/console doctrine:fixtures:load --purge-with-truncate --no-interaction
translations:
docker compose exec php bin/console translation:extract --force --format=xliff --sort=asc --clean nl
fix-cs:
docker compose exec php vendor/bin/php-cs-fixer fix
docker compose exec php vendor/bin/twig-cs-fixer fix
rector *args:
docker compose exec php vendor/bin/rector {{ args }}
phpstan *args:
docker compose exec php vendor/bin/phpstan analyse {{ args }}
[confirm]
clean:
docker compose down -v --remove-orphans
rm -rf vendor var assets/vendor public/assets public/bundles .php-cs-fixer.cache .twig-cs-fixer.cache

View File

@@ -6,39 +6,33 @@
- WIDM-tests met een variabel aantal vragen. - WIDM-tests met een variabel aantal vragen.
- Vragen in een vaste volgorde zijn samen één test (een vraag kan niet bij - Vragen in een vaste volgorde zijn samen één test (een vraag kan niet bij
meerdere test horen). meerdere tests horen).
- Vragen hebben 2 of meer antwoordmogelijkheden. Slechts één vraag is correct. - Vragen hebben 2 of meer antwoordmogelijkheden. Slechts één antwoord is correct.
- Meerdere test samen vormen een seizoen. - Meerdere test samen vormen een seizoen.
- Een seizoen heeft één of geen actieve tests, als er een test actief is kan - Een seizoen heeft één of geen actieve tests, als er een test actief is kan
Uitsluitend die test gemaakt worden. uitsluitend die test gemaakt worden.
- Kandidaten kunnen een test maximaal 1 keer invullen. Indien ingeschakeld - Kandidaten kunnen een test maximaal 1 keer invullen.
kunnen alleen vooraf ingevoerde namen gebruikt worden in een test.
- Vanaf het moment dat de kandidaat op start klikt na het intypen van hun naam - Vanaf het moment dat de kandidaat op start klikt na het intypen van hun naam
gaat de tijd lopen. Deze stopt na het aanklikken van een antwoord op de laatste gaat de tijd lopen. Deze stopt na het aanklikken van een antwoord op de laatste
vraag van de test. vraag van de test.
- Achtergrondmuziek - Achtergrondmuziek
### Schermen kijken ### Schermen kijken
- Nadat een speler een test heeft gemaakt (of vooraf als de namen vooraf - Nadat een speler een test heeft gemaakt (of vooraf als de namen vooraf
ingevoerd zijn) kunnen jokers toegekend worden aan de test van kandidaat. Een ingevoerd zijn) kunnen jokers toegekend worden aan de test van kandidaat. Een
positief getal om antwoorden goed te rekenen, een negatief getal om positief getal om antwoorden goed te rekenen, een negatief getal om
antwoorden fout te rekenen. Een vrijstelling kan gegeven worden door evenveel antwoorden fout te rekenen.
of meer jokers toe te kennen als dat er vragen in een test zitten.
- Vooraf kan gekozen worden hoe veel afvallers er zijn. - Vooraf kan gekozen worden hoe veel afvallers er zijn.
- Bij het kijken naam rode en groene schermen wordt een naam ingevoerd. Er - Bij het kijken naam rode en groene schermen wordt een naam ingevoerd. Er
wordt een rood of groen scherm getoond. wordt een rood of groen scherm getoond.
- Spelers kunnen geforceerd op groen of rood gezet worden, deze worden dan niet - Spelers kunnen geforceerd op groen of rood gezet worden, deze worden dan niet
meegenomen in de berekening van de slechtste speler. meegenomen in de berekening van de slechtste speler.
### Statistieken ### Statistieken
TBD TBD
## Nice to haves ## Nice to haves
- Optie voor antwoord geven in twee klikken (selecteren en volgende). - Optie voor antwoord geven in twee klikken (selecteren en volgende).

4
assets/backoffice.js Normal file
View File

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

View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

BIN
assets/img/green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

BIN
assets/img/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

24
assets/quiz.js Normal file
View File

@@ -0,0 +1,24 @@
import 'bootstrap/dist/css/bootstrap.min.css'
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;
});
}
});

26
assets/styles/app.scss Normal file
View File

@@ -0,0 +1,26 @@
html, body {
height: 100%;
background-image: url("../img/background.png");
background-position: center center;
background-repeat: no-repeat;
background-color: black;
color: white;
display: grid;
align-items: center;
justify-self: center;
}
.elimination-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
object-fit: contain;
background-color: white;
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}

View File

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
declare(strict_types=1);
use App\Kernel; use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;

View File

@@ -8,17 +8,43 @@ services:
- ./:/app - ./:/app
- ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro - ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro
- ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro - ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro
# If you develop on Mac or Windows you can remove the vendor/ directory - ./frankenphp/data:/data
# from the bind-mount for better performance by enabling the next line: - sass:/app/var/sass
#- /app/vendor
environment: environment:
MERCURE_EXTRA_DIRECTIVES: demo MERCURE_EXTRA_DIRECTIVES: demo
# See https://xdebug.org/docs/all_settings#mode # See https://xdebug.org/docs/all_settings#mode
XDEBUG_MODE: "${XDEBUG_MODE:-off}" XDEBUG_MODE: "${XDEBUG_MODE:-off}"
MAILER_DSN: "smtp://mailer:1025"
extra_hosts: extra_hosts:
# Ensure that host.docker.internal is correctly defined on Linux # Ensure that host.docker.internal is correctly defined on Linux
- host.docker.internal:host-gateway - host.docker.internal:host-gateway
tty: true tty: true
ports:
# HTTP
- target: 80
published: ${HTTP_PORT:-80}
protocol: tcp
# HTTPS
- target: 443
published: ${HTTPS_PORT:-443}
protocol: tcp
# HTTP/3
- target: 443
published: ${HTTP3_PORT:-443}
protocol: udp
sass:
image: ${IMAGES_PREFIX:-}app-php
volumes:
- ./:/app:ro
- sass:/app/var/sass
entrypoint: ''
depends_on:
- php
command:
- bin/console
- sass:build
- --watch
- -v
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
@@ -27,4 +53,18 @@ services:
database: database:
ports: ports:
- "5432:5432" - "5432:5432"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
mailer:
image: axllent/mailpit
ports:
- "1025"
- "8025"
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
###< symfony/mailer ###
volumes:
sass:

View File

@@ -8,3 +8,23 @@ services:
APP_SECRET: ${APP_SECRET} APP_SECRET: ${APP_SECRET}
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
MAILER_SENDER: ${MAILER_SENDER}
SENTRY_DSN: ${SENTRY_DSN}
labels:
- "traefik.enable=true"
- "traefik.http.routers.tvdt.rule=Host(`tijdvoordetest.nl`)"
- "traefik.http.routers.tvdt.entrypoints=websecure"
- "traefik.http.routers.tvdt.tls.certresolver=marijndoeve"
- "traefik.http.routers.tvdt.service=tvdt"
- "traefik.http.services.tvdt.loadbalancer.server.port=80"
networks:
- web
- internal
database:
networks:
- internal
networks:
web:
external: true
internal:
external: false

View File

@@ -7,36 +7,20 @@ services:
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8} DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-16}&charset=${POSTGRES_CHARSET:-utf8}
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration # Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure} MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure} MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure}
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# The two next lines can be removed after initial installation
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
STABILITY: ${STABILITY:-stable}
volumes: volumes:
- caddy_data:/data - caddy_data:/data
- caddy_config:/config - caddy_config:/config
ports:
# HTTP
- target: 80
published: ${HTTP_PORT:-80}
protocol: tcp
# HTTPS
- target: 443
published: ${HTTPS_PORT:-443}
protocol: tcp
# HTTP/3
- target: 443
published: ${HTTP3_PORT:-443}
protocol: udp
# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service # Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
database: database:
image: postgres:${POSTGRES_VERSION:-16}-alpine image: postgres:${POSTGRES_VERSION:-16}-alpine
environment: environment:
@@ -45,22 +29,21 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app} POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck: healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"] test: [ "CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}" ]
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 60s start_period: 60s
volumes: volumes:
- database_data:/var/lib/postgresql/data:rw - database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! restart: always
# - ./docker/db/data:/var/lib/postgresql/data:rw ###< doctrine/doctrine-bundle ###
###< doctrine/doctrine-bundle ###
volumes: volumes:
caddy_data: caddy_data:
caddy_config: caddy_config:
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
database_data: database_data:
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###

View File

@@ -1,5 +1,5 @@
{ {
"name": "symfony/skeleton", "name": "marijndoeve/tijdvoordetest",
"type": "project", "type": "project",
"license": "MIT", "license": "MIT",
"description": "A minimal Symfony project recommended to create bare bones applications", "description": "A minimal Symfony project recommended to create bare bones applications",
@@ -9,39 +9,59 @@
"php": ">=8.3.15", "php": ">=8.3.15",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"doctrine/dbal": "^3", "doctrine/dbal": "^4.2.3",
"doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-bundle": "^2.14.0",
"doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3.3",
"easycorp/easyadmin-bundle": "^4.23", "easycorp/easyadmin-bundle": "^4.24.7",
"phpdocumentor/reflection-docblock": "^5.6.2",
"phpoffice/phpspreadsheet": "^4.2.0",
"phpstan/phpdoc-parser": "^2.1",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"sentry/sentry-symfony": "^5.2",
"symfony/asset": "7.2.*", "symfony/asset": "7.2.*",
"symfony/asset-mapper": "7.2.*",
"symfony/console": "7.2.*", "symfony/console": "7.2.*",
"symfony/dotenv": "7.2.*", "symfony/dotenv": "7.2.*",
"symfony/flex": "^2.4.7", "symfony/flex": "^2.7.0",
"symfony/form": "7.2.*", "symfony/form": "7.2.*",
"symfony/framework-bundle": "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/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*", "symfony/security-bundle": "7.2.*",
"symfony/security-csrf": "7.2.*",
"symfony/serializer": "7.2.*",
"symfony/twig-bundle": "7.2.*", "symfony/twig-bundle": "7.2.*",
"symfony/uid": "7.2.*", "symfony/uid": "7.2.*",
"symfony/yaml": "7.2.*", "symfony/yaml": "7.2.*",
"thecodingmachine/safe": "^2.5" "symfonycasts/sass-bundle": "^0.8.2",
"symfonycasts/verify-email-bundle": "^1.17.3",
"thecodingmachine/safe": "^3.3.0",
"twig/extra-bundle": "^3.21",
"twig/intl-extra": "^3.21",
"twig/twig": "^3.21.1"
}, },
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.0", "doctrine/doctrine-fixtures-bundle": "^4.1",
"friendsofphp/php-cs-fixer": "^3.65", "friendsofphp/php-cs-fixer": "^3.75.0",
"phpstan/extension-installer": "^1.4", "phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.0", "phpstan/phpstan": "^2.1.17",
"phpstan/phpstan-doctrine": "^2.0", "phpstan/phpstan-doctrine": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-phpunit": "^2.0.6",
"phpstan/phpstan-symfony": "^2.0", "phpstan/phpstan-symfony": "^2.0.6",
"phpunit/phpunit": "^11", "phpunit/phpunit": "^12.1.6",
"rector/rector": "^2.0", "rector/rector": "^2.0.16",
"symfony/maker-bundle": "^1.62.1", "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/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*", "symfony/web-profiler-bundle": "7.2.*",
"thecodingmachine/phpstan-safe-rule": "^1.3" "thecodingmachine/phpstan-safe-rule": "^1.4.1",
"vincentlanglet/twig-cs-fixer": "^3.7.1"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
@@ -66,17 +86,21 @@
"replace": { "replace": {
"symfony/polyfill-ctype": "*", "symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*", "symfony/polyfill-iconv": "*",
"symfony/polyfill-mbstring": "*",
"symfony/polyfill-php72": "*", "symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*", "symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*", "symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*", "symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*", "symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*" "symfony/polyfill-php82": "*",
"symfony/polyfill-php83": "*",
"symfony/polyfill-uuid": "*"
}, },
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
"cache:clear": "symfony-cmd", "cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd" "assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
}, },
"post-install-cmd": [ "post-install-cmd": [
"@auto-scripts" "@auto-scripts"

5291
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,35 @@
<?php <?php
declare(strict_types=1);
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle;
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
use EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle;
use Sentry\SentryBundle\SentryBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
use Symfony\UX\TwigComponent\TwigComponentBundle;
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;
use Symfonycasts\SassBundle\SymfonycastsSassBundle;
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
return [ return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], MakerBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], TwigBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], SecurityBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], WebProfilerBundle::class => ['dev' => true, 'test' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], TwigExtraBundle::class => ['all' => true],
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], TwigComponentBundle::class => ['all' => true],
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true], EasyAdminBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
SymfonyCastsVerifyEmailBundle::class => ['all' => true],
SentryBundle::class => ['prod' => true],
SymfonycastsSassBundle::class => ['all' => true],
]; ];

View File

@@ -0,0 +1,15 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
excluded_patterns:
- '*/assets/styles/_*.scss'
- '*/assets/styles/**/_*.scss'
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn

View File

@@ -1,11 +0,0 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View File

@@ -4,9 +4,17 @@ framework:
# Note that the session will be started ONLY if you read or write from it. # Note that the session will be started ONLY if you read or write from it.
session: true session: true
form:
csrf_protection:
enabled: true
#esi: true #esi: true
#fragments: true #fragments: true
when@prod:
framework:
# shortcut for private IP address ranges of your proxy
trusted_proxies: 'private_ranges'
# or, if your proxy instead uses the "Forwarded" header
trusted_headers: [ 'forwarded' ]
when@test: when@test:
framework: framework:

View File

@@ -0,0 +1,7 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'
envelope:
sender: '%env(MAILER_SENDER)%'
headers:
From: 'Tijd voor de test <%env(MAILER_SENDER)%>'

View File

@@ -18,11 +18,12 @@ security:
lazy: true lazy: true
provider: app_user_provider provider: app_user_provider
form_login: form_login:
login_path: app_login login_path: app_login_login
check_path: app_login check_path: app_login_login
enable_csrf: true enable_csrf: true
default_target_path: app_backoffice_index
logout: logout:
path: app_logout path: app_login_logout
# where to redirect after logout # where to redirect after logout
# target: app_any_route # target: app_any_route
@@ -35,7 +36,7 @@ security:
# Easy way to control access for large sections of your site # Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER } # - { path: ^/profile, roles: ROLE_USER }
when@test: when@test:

View File

@@ -0,0 +1,31 @@
when@prod:
sentry:
dsn: '%env(SENTRY_DSN)%'
# Add request headers, cookies, IP address and the authenticated user
# see https://docs.sentry.io/platforms/php/data-management/data-collected/ for more info
# send_default_pii: true
options:
traces_sample_rate: 1.0
profiles_sample_rate: 1.0
ignore_exceptions:
- 'Symfony\Component\ErrorHandler\Error\FatalError'
- 'Symfony\Component\Debug\Exception\FatalErrorException'
# If you are using Monolog, you also need this additional configuration to log the errors correctly:
# https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration
# register_error_listener: false
# register_error_handler: false
# monolog:
# handlers:
# sentry:
# type: sentry
# level: !php/const Monolog\Logger::ERROR
# hub_id: Sentry\State\HubInterface
# Uncomment these lines to register a log message processor that resolves PSR-3 placeholders
# https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration
# services:
# Monolog\Processor\PsrLogMessageProcessor:
# tags: { name: monolog.processor, handler: sentry }

View File

@@ -11,3 +11,6 @@ opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000 opcache.max_accelerated_files = 20000
opcache.memory_consumption = 256 opcache.memory_consumption = 256
opcache.enable_file_override = 1 opcache.enable_file_override = 1
; for Sentry
zend.exception_ignore_args = Off

35
importmap.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'quiz' => [
'path' => './assets/quiz.js',
'entrypoint' => true,
],
'backoffice' => [
'path' => './assets/backoffice.js',
'entrypoint' => true,
],
'bootstrap' => [
'version' => '5.3.6',
],
'@popperjs/core' => [
'version' => '2.11.8',
],
'bootstrap/dist/css/bootstrap.min.css' => [
'version' => '5.3.6',
'type' => 'css',
],
];

View File

@@ -0,0 +1,119 @@
<?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 Version20250311213417 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('ALTER TABLE answer ALTER id TYPE UUID');
$this->addSql('ALTER TABLE answer ALTER question_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN answer.id IS \'\'');
$this->addSql('COMMENT ON COLUMN answer.question_id IS \'\'');
$this->addSql('ALTER TABLE answer_candidate ALTER answer_id TYPE UUID');
$this->addSql('ALTER TABLE answer_candidate ALTER candidate_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN answer_candidate.answer_id IS \'\'');
$this->addSql('COMMENT ON COLUMN answer_candidate.candidate_id IS \'\'');
$this->addSql('ALTER TABLE candidate ALTER id TYPE UUID');
$this->addSql('ALTER TABLE candidate ALTER season_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN candidate.id IS \'\'');
$this->addSql('COMMENT ON COLUMN candidate.season_id IS \'\'');
$this->addSql('ALTER TABLE correction ALTER id TYPE UUID');
$this->addSql('ALTER TABLE correction ALTER candidate_id TYPE UUID');
$this->addSql('ALTER TABLE correction ALTER quiz_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN correction.id IS \'\'');
$this->addSql('COMMENT ON COLUMN correction.candidate_id IS \'\'');
$this->addSql('COMMENT ON COLUMN correction.quiz_id IS \'\'');
$this->addSql('ALTER TABLE given_answer ALTER id TYPE UUID');
$this->addSql('ALTER TABLE given_answer ALTER candidate_id TYPE UUID');
$this->addSql('ALTER TABLE given_answer ALTER quiz_id TYPE UUID');
$this->addSql('ALTER TABLE given_answer ALTER answer_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN given_answer.id IS \'\'');
$this->addSql('COMMENT ON COLUMN given_answer.candidate_id IS \'\'');
$this->addSql('COMMENT ON COLUMN given_answer.quiz_id IS \'\'');
$this->addSql('COMMENT ON COLUMN given_answer.answer_id IS \'\'');
$this->addSql('ALTER TABLE question ALTER id TYPE UUID');
$this->addSql('ALTER TABLE question ALTER quiz_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN question.id IS \'\'');
$this->addSql('COMMENT ON COLUMN question.quiz_id IS \'\'');
$this->addSql('ALTER TABLE quiz ADD dropouts INT DEFAULT NULL');
$this->addSql('ALTER TABLE quiz ALTER id TYPE UUID');
$this->addSql('ALTER TABLE quiz ALTER season_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN quiz.id IS \'\'');
$this->addSql('COMMENT ON COLUMN quiz.season_id IS \'\'');
$this->addSql('ALTER TABLE season ALTER id TYPE UUID');
$this->addSql('ALTER TABLE season ALTER active_quiz_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN season.id IS \'\'');
$this->addSql('COMMENT ON COLUMN season.active_quiz_id IS \'\'');
$this->addSql('ALTER TABLE season_user ALTER season_id TYPE UUID');
$this->addSql('ALTER TABLE season_user ALTER user_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN season_user.season_id IS \'\'');
$this->addSql('COMMENT ON COLUMN season_user.user_id IS \'\'');
$this->addSql('ALTER TABLE "user" ALTER id TYPE UUID');
$this->addSql('COMMENT ON COLUMN "user".id IS \'\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE candidate ALTER id TYPE UUID');
$this->addSql('ALTER TABLE candidate ALTER season_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN candidate.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN candidate.season_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE correction ALTER id TYPE UUID');
$this->addSql('ALTER TABLE correction ALTER candidate_id TYPE UUID');
$this->addSql('ALTER TABLE correction ALTER quiz_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN correction.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN correction.candidate_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN correction.quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE given_answer ALTER id TYPE UUID');
$this->addSql('ALTER TABLE given_answer ALTER candidate_id TYPE UUID');
$this->addSql('ALTER TABLE given_answer ALTER quiz_id TYPE UUID');
$this->addSql('ALTER TABLE given_answer ALTER answer_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN given_answer.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN given_answer.candidate_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN given_answer.quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN given_answer.answer_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE answer_candidate ALTER answer_id TYPE UUID');
$this->addSql('ALTER TABLE answer_candidate ALTER candidate_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN answer_candidate.answer_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN answer_candidate.candidate_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE quiz DROP dropouts');
$this->addSql('ALTER TABLE quiz ALTER id TYPE UUID');
$this->addSql('ALTER TABLE quiz ALTER season_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN quiz.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN quiz.season_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE season ALTER id TYPE UUID');
$this->addSql('ALTER TABLE season ALTER active_quiz_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN season.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN season.active_quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE answer ALTER id TYPE UUID');
$this->addSql('ALTER TABLE answer ALTER question_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN answer.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN answer.question_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE season_user ALTER season_id TYPE UUID');
$this->addSql('ALTER TABLE season_user ALTER user_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN season_user.season_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN season_user.user_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE question ALTER id TYPE UUID');
$this->addSql('ALTER TABLE question ALTER quiz_id TYPE UUID');
$this->addSql('COMMENT ON COLUMN question.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN question.quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE "user" ALTER id TYPE UUID');
$this->addSql('COMMENT ON COLUMN "user".id IS \'(DC2Type:uuid)\'');
}
}

View File

@@ -0,0 +1,31 @@
<?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 Version20250402185128 extends AbstractMigration
{
public function getDescription(): string
{
return 'add elimination table';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE elimination (id UUID NOT NULL, data JSON NOT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE elimination');
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250420111904 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add is_verified column to user table';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE "user" ADD is_verified BOOLEAN NOT NULL
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE "user" DROP is_verified
SQL);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250420125040 extends AbstractMigration
{
public function getDescription(): string
{
return 'Drop preregister_candidates column from season table';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE season DROP preregister_candidates
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE season ADD preregister_candidates BOOLEAN NOT NULL DEFAULT true
SQL);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250427174822 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add ordering to question and answer';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE answer ADD ordering SMALLINT DEFAULT 0 NOT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE question ADD ordering SMALLINT DEFAULT 0 NOT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE answer DROP ordering
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE question DROP ordering
SQL);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250504101440 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'
CREATE UNIQUE INDEX UNIQ_C8B28E445E237E064EC001D1 ON candidate (name, season_id)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_A412FA925E237E064EC001D1 ON quiz (name, season_id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP INDEX UNIQ_A412FA925E237E064EC001D1
SQL);
$this->addSql(<<<'SQL'
DROP INDEX UNIQ_C8B28E445E237E064EC001D1
SQL);
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250521192752 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE elimination ADD quiz_id UUID NOT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE elimination ADD CONSTRAINT FK_5947284F853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_5947284F853CD175 ON elimination (quiz_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE quiz ALTER dropouts SET DEFAULT 1
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE quiz ALTER dropouts SET NOT NULL
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE quiz ALTER dropouts DROP DEFAULT
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE quiz ALTER dropouts DROP NOT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE elimination DROP CONSTRAINT FK_5947284F853CD175
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_5947284F853CD175
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE elimination DROP quiz_id
SQL);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250521194035 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE elimination ADD created TIMESTAMP(0) WITHOUT TIME ZONE
SQL);
$this->addSql(<<<'SQL'
UPDATE elimination SET created = NOW()
SQL
);
$this->addSql(<<<'SQL'
ALTER TABLE elimination ALTER created SET NOT NULL
SQL
);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE elimination DROP created
SQL);
}
}

View File

@@ -1,4 +1,5 @@
parameters: parameters:
editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'
level: 8 level: 8
paths: paths:
- bin/ - bin/

34
phpunit.dist.xml Normal file
View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
<extensions>
</extensions>
</phpunit>

View File

@@ -7,8 +7,8 @@ use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return static function (array $context): Kernel { return static function (array $context): Kernel {
$appEnv = !empty($context['APP_ENV']) ? (string) $context['APP_ENV'] : 'prod'; $appEnv = empty($context['APP_ENV']) ? 'prod' : (string) $context['APP_ENV'];
$appDebug = !empty($context['APP_DEBUG']) ? filter_var($context['APP_DEBUG'], \FILTER_VALIDATE_BOOL) : 'prod' !== $appEnv; $appDebug = empty($context['APP_DEBUG']) ? 'prod' !== $appEnv : filter_var($context['APP_DEBUG'], \FILTER_VALIDATE_BOOL);
return new Kernel($appEnv, $appDebug); return new Kernel($appEnv, $appDebug);
}; };

View File

@@ -11,6 +11,8 @@ return RectorConfig::configure()
__DIR__.'/src', __DIR__.'/src',
__DIR__.'/tests', __DIR__.'/tests',
]) ])
->withSymfonyContainerXml('var/cache/dev/App_KernelDevDebugContainer.xml')
->withParallel()
->withPhpSets() ->withPhpSets()
->withPreparedSets( ->withPreparedSets(
deadCode: true, deadCode: true,
@@ -26,6 +28,6 @@ return RectorConfig::configure()
doctrineCodeQuality: true, doctrineCodeQuality: true,
symfonyCodeQuality: true, symfonyCodeQuality: true,
) )
->withComposerBased(twig: true, doctrine: true, phpunit: true) ->withAttributesSets(all: true)
->withAttributesSets() ->withComposerBased(twig: true, doctrine: true, phpunit: true, symfony: true)
; ;

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\SeasonRepository;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
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;
#[AsCommand(
name: 'app:claim-season',
description: 'Give a user owner rights on a season',
)]
class ClaimSeasonCommand extends Command
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly SeasonRepository $seasonRepository,
private readonly EntityManagerInterface $entityManager)
{
parent::__construct();
}
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
{
$io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email');
$seasonCode = $input->getArgument('season');
try {
$season = $this->seasonRepository->findOneBy(['seasonCode' => $seasonCode]);
if (null === $season) {
throw new \InvalidArgumentException('Season not found');
}
$user = $this->userRepository->findOneBy(['email' => $email]);
if (null === $user) {
throw new \InvalidArgumentException('User not found');
}
$season->addOwner($user);
$this->entityManager->flush();
} catch (\InvalidArgumentException $invalidArgumentException) {
$io->error($invalidArgumentException->getMessage());
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\UserRepository;
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;
#[AsCommand(
name: 'app:make-admin',
description: 'Give a user the role admin',
)]
class MakeAdminCommand extends Command
{
public function __construct(private readonly UserRepository $userRepository)
{
parent::__construct();
}
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
{
$io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email');
try {
$this->userRepository->makeAdmin($email);
} catch (\InvalidArgumentException) {
$io->error('User not found');
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Enum\FlashType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBaseController;
abstract class AbstractController extends AbstractBaseController
{
#[\Override]
protected function addFlash(FlashType|string $type, mixed $message): void
{
if ($type instanceof FlashType) {
$type = $type->value;
}
parent::addFlash($type, $message);
}
}

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Answer; use App\Entity\Answer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class AnswerCrudController extends AbstractCrudController class AnswerCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class AnswerCrudController extends AbstractCrudController
{ {
return Answer::class; return Answer::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Candidate; use App\Entity\Candidate;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class CandidateCrudController extends AbstractCrudController class CandidateCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class CandidateCrudController extends AbstractCrudController
{ {
return Candidate::class; return Candidate::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Correction; use App\Entity\Correction;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class CorrectionCrudController extends AbstractCrudController class CorrectionCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class CorrectionCrudController extends AbstractCrudController
{ {
return Correction::class; return Correction::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -12,20 +12,19 @@ use App\Entity\Question;
use App\Entity\Quiz; use App\Entity\Quiz;
use App\Entity\Season; use App\Entity\Season;
use App\Entity\User; use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard; use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem; use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
class DashboardController extends AbstractDashboardController class DashboardController extends AbstractDashboardController
{ {
#[Route('/admin', name: 'admin')] #[\Override]
public function index(): Response public function index(): Response
{ {
// return parent::index();
// Option 1. You can make your dashboard redirect to some common page of your backend // Option 1. You can make your dashboard redirect to some common page of your backend
// //
$adminUrlGenerator = $this->container->get(AdminUrlGenerator::class); $adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);
@@ -44,12 +43,14 @@ class DashboardController extends AbstractDashboardController
// return $this->render('some/path/my-dashboard.html.twig'); // return $this->render('some/path/my-dashboard.html.twig');
} }
#[\Override]
public function configureDashboard(): Dashboard public function configureDashboard(): Dashboard
{ {
return Dashboard::new() return Dashboard::new()
->setTitle('TijdVoorDeTest'); ->setTitle('TijdVoorDeTest');
} }
#[\Override]
public function configureMenuItems(): iterable public function configureMenuItems(): iterable
{ {
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home'); yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
@@ -61,6 +62,6 @@ class DashboardController extends AbstractDashboardController
yield MenuItem::linkToCrud('User', 'fas fa-list', User::class); yield MenuItem::linkToCrud('User', 'fas fa-list', User::class);
yield MenuItem::linkToCrud('Given Answer', 'fas fa-list', GivenAnswer::class); yield MenuItem::linkToCrud('Given Answer', 'fas fa-list', GivenAnswer::class);
yield MenuItem::linkToCrud('Answer', 'fas fa-list', Answer::class); yield MenuItem::linkToCrud('Answer', 'fas fa-list', Answer::class);
yield MenuItem::linkToLogout('Logout', 'fa fa-exit'); yield MenuItem::linkToLogout('Logout', 'fas fa-sign-out');
} }
} }

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\GivenAnswer; use App\Entity\GivenAnswer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class GivenAnswerCrudController extends AbstractCrudController class GivenAnswerCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class GivenAnswerCrudController extends AbstractCrudController
{ {
return GivenAnswer::class; return GivenAnswer::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Question; use App\Entity\Question;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class QuestionCrudController extends AbstractCrudController class QuestionCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class QuestionCrudController extends AbstractCrudController
{ {
return Question::class; return Question::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Quiz; use App\Entity\Quiz;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class QuizCrudController extends AbstractCrudController class QuizCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class QuizCrudController extends AbstractCrudController
{ {
return Quiz::class; return Quiz::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Season; use App\Entity\Season;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class SeasonCrudController extends AbstractCrudController class SeasonCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class SeasonCrudController extends AbstractCrudController
{ {
return Season::class; return Season::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\User; use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class UserCrudController extends AbstractCrudController class UserCrudController extends AbstractCrudController
{ {
@@ -14,15 +13,4 @@ class UserCrudController extends AbstractCrudController
{ {
return User::class; return User::class;
} }
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
} }

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Controller\Backoffice;
use App\Controller\AbstractController;
use App\Entity\Season;
use App\Entity\User;
use App\Form\CreateSeasonFormType;
use App\Repository\SeasonRepository;
use App\Service\QuizSpreadsheetService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[AsController]
#[IsGranted('ROLE_USER')]
final class BackofficeController extends AbstractController
{
public function __construct(
private readonly SeasonRepository $seasonRepository,
private readonly Security $security,
) {}
#[Route('/backoffice/', name: 'app_backoffice_index')]
public function index(): Response
{
$user = $this->getUser();
\assert($user instanceof User);
$seasons = $this->security->isGranted('ROLE_ADMIN')
? $this->seasonRepository->findAll()
: $this->seasonRepository->getSeasonsForUser($user);
return $this->render('backoffice/index.html.twig', [
'seasons' => $seasons,
]);
}
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
public function addSeason(Request $request, EntityManagerInterface $em): Response
{
$season = new Season();
$form = $this->createForm(CreateSeasonFormType::class, $season);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $this->getUser();
\assert($user instanceof User);
$season->addOwner($user);
$season->generateSeasonCode();
$em->persist($season);
$em->flush();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
}
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
public function getTemplate(QuizSpreadsheetService $excel): Response
{
$response = new StreamedResponse($excel->generateTemplate());
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
return $response;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Controller\Backoffice;
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;
final class PrepareEliminationController extends AbstractController
{
#[Route('/backoffice/elimination/{seasonCode}/{quiz}/prepare', name: 'app_prepare_elimination')]
public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
{
$elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
return $this->redirectToRoute('app_prepare_elimination_view', ['elimination' => $elimination->getId()]);
}
#[Route('/backoffice/elimination/{elimination}', name: 'app_prepare_elimination_view')]
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')) {
return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]);
}
$this->addFlash('success', 'Elimination updated');
}
return $this->render('backoffice/prepare_elimination/index.html.twig', [
'controller_name' => 'PrepareEliminationController',
'elimination' => $elimination,
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Controller\Backoffice;
use App\Controller\AbstractController;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Repository\CandidateRepository;
use App\Security\Voter\SeasonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[AsController]
#[IsGranted('ROLE_USER')]
class QuizController extends AbstractController
{
public function __construct(
private readonly CandidateRepository $candidateRepository,
) {}
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season, Quiz $quiz): Response
{
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $quiz,
'result' => $this->candidateRepository->getScores($quiz),
]);
}
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
{
$season->setActiveQuiz($quiz);
$em->flush();
if ($quiz instanceof Quiz) {
return $this->redirectToRoute('app_backoffice_quiz', ['seasonCode' => $season->getSeasonCode(), 'quiz' => $quiz->getId()]);
}
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
}

View File

@@ -0,0 +1,88 @@
<?php
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\Enum\FlashType;
use App\Form\AddCandidatesFormType;
use App\Form\UploadQuizFormType;
use App\Security\Voter\SeasonVoter;
use App\Service\QuizSpreadsheetService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController]
#[IsGranted('ROLE_USER')]
class SeasonController extends AbstractController
{
public function __construct(private readonly TranslatorInterface $translator, private EntityManagerInterface $em,
) {}
#[Route('/backoffice/season/{seasonCode}', name: 'app_backoffice_season')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season): Response
{
return $this->render('backoffice/season.html.twig', [
'season' => $season,
]);
}
#[Route('/backoffice/season/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addCandidates(Season $season, Request $request): Response
{
$form = $this->createForm(AddCandidatesFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$candidates = $form->get('candidates')->getData();
foreach (explode("\r\n", (string) $candidates) as $candidate) {
$season->addCandidate(new Candidate($candidate));
}
$this->em->flush();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
}
#[Route('/backoffice/season/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
{
$quiz = new Quiz();
$form = $this->createForm(UploadQuizFormType::class, $quiz);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/* @var UploadedFile $sheet */
$sheet = $form->get('sheet')->getData();
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
$quiz->setSeason($season);
$this->em->persist($quiz);
$this->em->flush();
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Candidate;
use App\Entity\Elimination;
use App\Enum\FlashType;
use App\Form\EliminationEnterNameType;
use App\Helpers\Base64;
use App\Repository\CandidateRepository;
use App\Security\Voter\SeasonVoter;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\Translation\t;
#[AsController]
#[IsGranted('ROLE_USER')]
final class EliminationController extends AbstractController
{
public function __construct(private readonly TranslatorInterface $translator) {}
#[Route('/elimination/{elimination}', name: 'app_elimination')]
#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]
public function index(#[MapEntity] Elimination $elimination, Request $request): Response
{
$form = $this->createForm(EliminationEnterNameType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$name = $form->get('name')->getData();
return $this->redirectToRoute('app_elimination_candidate', ['elimination' => $elimination->getId(), 'candidateHash' => Base64::base64UrlEncode($name)]);
}
return $this->render('quiz/elimination/index.html.twig', [
'form' => $form,
'controller_name' => 'EliminationController',
]);
}
#[Route('/elimination/{elimination}/{candidateHash}', name: 'app_elimination_candidate')]
#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]
public function candidateScreen(Elimination $elimination, string $candidateHash, CandidateRepository $candidateRepository): Response
{
$candidate = $candidateRepository->getCandidateByHash($elimination->getQuiz()->getSeason(), $candidateHash);
if (!$candidate instanceof Candidate) {
$this->addFlash(FlashType::Warning,
t('Cound not find candidate with name %name%', ['%name%' => Base64::base64UrlDecode($candidateHash)])->trans($this->translator)
);
return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]);
}
$screenColour = $elimination->getScreenColour($candidate->getName());
if (null === $screenColour) {
$this->addFlash(FlashType::Warning, $this->translator->trans('Cound not find candidate with name %name% in elimination.', ['%name%' => $candidate->getName()]));
return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]);
}
return $this->render('quiz/elimination/candidate.html.twig', [
'candidate' => $candidate,
'colour' => $screenColour,
]);
}
}

View File

@@ -4,15 +4,19 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use App\Enum\FlashType;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Contracts\Translation\TranslatorInterface;
class LoginController extends AbstractController #[AsController]
final class LoginController extends AbstractController
{ {
#[Route(path: '/login', name: 'app_login')] #[Route(path: '/login', name: 'app_login_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils, TranslatorInterface $translator): Response
{ {
// get the login error if there is one // get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError(); $error = $authenticationUtils->getLastAuthenticationError();
@@ -20,14 +24,18 @@ class LoginController extends AbstractController
// last username entered by the user // last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername(); $lastUsername = $authenticationUtils->getLastUsername();
return $this->render('login/login.html.twig', [ if ($error instanceof AuthenticationException) {
$this->addFlash(FlashType::Danger, $translator->trans($error->getMessageKey(), $error->getMessageData(), 'security'));
}
return $this->render('backoffice/login/login.html.twig', [
'last_username' => $lastUsername, 'last_username' => $lastUsername,
'error' => $error, 'error' => $error,
]); ]);
} }
#[Route(path: '/logout', name: 'app_logout')] #[Route(path: '/logout', name: 'app_login_logout')]
public function logout(): void public function logout(): never
{ {
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
} }

View File

@@ -4,44 +4,62 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\Answer;
use App\Entity\Candidate; use App\Entity\Candidate;
use App\Entity\GivenAnswer;
use App\Entity\Question;
use App\Entity\Season; use App\Entity\Season;
use App\Enum\FlashType; use App\Enum\FlashType;
use App\Form\EnterNameType; use App\Form\EnterNameType;
use App\Form\SelectSeasonType; use App\Form\SelectSeasonType;
use App\Helpers\Base64; use App\Helpers\Base64;
use App\Repository\AnswerRepository;
use App\Repository\CandidateRepository; use App\Repository\CandidateRepository;
use App\Repository\GivenAnswerRepository;
use App\Repository\QuestionRepository; use App\Repository\QuestionRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use App\Repository\SeasonRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController] #[AsController]
class QuizController extends AbstractController final class QuizController extends AbstractController
{ {
public const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}'; public const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
private const string CANDIDATE_HASH_REGEX = '[\w\-=]+'; private const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
#[Route(path: '/', name: 'select_season', methods: ['GET', 'POST'])] public function __construct(private readonly TranslatorInterface $translator) {}
public function selectSeason(Request $request): Response
#[Route(path: '/', name: 'app_quiz_selectseason', methods: ['GET', 'POST'])]
public function selectSeason(Request $request, SeasonRepository $seasonRepository): Response
{ {
$form = $this->createForm(SelectSeasonType::class); $form = $this->createForm(SelectSeasonType::class);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData(); $seasonCode = $form->get('season_code')->getData();
return $this->redirectToRoute('enter_name', ['seasonCode' => $data['season_code']]); 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_entername', ['seasonCode' => $seasonCode]);
} }
return $this->render('quiz/select_season.html.twig', ['form' => $form]); return $this->render('quiz/select_season.html.twig', ['form' => $form]);
} }
#[Route(path: '/{seasonCode}', name: 'enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])] #[Route(path: '/{seasonCode}', name: 'app_quiz_entername', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
public function enterName( public function enterName(
Request $request, Request $request,
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
Season $season, Season $season,
): Response { ): Response {
$form = $this->createForm(EnterNameType::class); $form = $this->createForm(EnterNameType::class);
@@ -49,10 +67,9 @@ class QuizController extends AbstractController
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData(); $name = $form->get('name')->getData();
$name = $data['name'];
return $this->redirectToRoute('quiz_page', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64_url_encode($name)]); return $this->redirectToRoute('app_quiz_quizpage', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64UrlEncode($name)]);
} }
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]); return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
@@ -60,26 +77,50 @@ class QuizController extends AbstractController
#[Route( #[Route(
path: '/{seasonCode}/{nameHash}', path: '/{seasonCode}/{nameHash}',
name: 'quiz_page', name: 'app_quiz_quizpage',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX], requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
)] )]
public function quizPage( public function quizPage(
#[MapEntity(mapping: ['seasonCode' => 'seasonCode'])]
Season $season, Season $season,
string $nameHash, string $nameHash,
CandidateRepository $candidateRepository, CandidateRepository $candidateRepository,
QuestionRepository $questionRepository, QuestionRepository $questionRepository,
AnswerRepository $answerRepository,
GivenAnswerRepository $givenAnswerRepository,
Request $request,
): Response { ): Response {
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash); $candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
if (!$candidate instanceof Candidate) { if (!$candidate instanceof Candidate) {
// Add option to add new candidate when preregister is disabled $this->addFlash(FlashType::Danger, $this->translator->trans('Candidate not found'));
$this->addFlash(FlashType::Danger->value, 'Candidate not found');
return $this->redirectToRoute('enter_name', ['seasonCode' => $season->getSeasonCode()]); return $this->redirectToRoute('app_quiz_entername', ['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');
}
$givenAnswer = (new GivenAnswer())
->setCandidate($candidate)
->setAnswer($answer)
->setQuiz($answer->getQuestion()->getQuiz());
$givenAnswerRepository->save($givenAnswer);
} }
$question = $questionRepository->findNextQuestionForCandidate($candidate); $question = $questionRepository->findNextQuestionForCandidate($candidate);
if (!$question instanceof Question) {
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz completed'));
return $this->redirectToRoute('app_quiz_entername', ['seasonCode' => $season->getSeasonCode()]);
}
// TODO One first question record time
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]); return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]);
} }
} }

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Repository\UserRepository;
use App\Security\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface;
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\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
final class RegistrationController extends AbstractController
{
public function __construct(private readonly EmailVerifier $emailVerifier, private readonly TranslatorInterface $translator) {}
#[Route('/register', name: 'app_register')]
public function register(
Request $request,
UserPasswordHasherInterface $userPasswordHasher,
Security $security,
EntityManagerInterface $entityManager,
): Response {
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var string $plainPassword */
$plainPassword = $form->get('plainPassword')->getData();
$user->setPassword($userPasswordHasher->hashPassword($user, $plainPassword));
$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')
);
$response = $security->login($user, 'form_login', 'main');
\assert($response instanceof Response);
return $response;
}
return $this->render('backoffice/registration/register.html.twig', [
'registrationForm' => $form,
]);
}
#[Route('/verify/email', name: 'app_verify_email')]
public function verifyUserEmail(Request $request, TranslatorInterface $translator, UserRepository $userRepository): Response
{
$id = $request->query->get('id');
if (null === $id) {
return $this->redirectToRoute('app_register');
}
$user = $userRepository->find($id);
if (null === $user) {
return $this->redirectToRoute('app_register');
}
// validate email confirmation link, sets User::isVerified=true and persists
try {
$this->emailVerifier->handleEmailConfirmation($request, $user);
} catch (VerifyEmailExceptionInterface $verifyEmailException) {
$this->addFlash('verify_email_error', $translator->trans($verifyEmailException->getReason(), [], 'VerifyEmailBundle'));
return $this->redirectToRoute('app_register');
}
$this->addFlash('success', $this->translator->trans('Your email address has been verified.'));
return $this->redirectToRoute('app_backoffice_index');
}
}

View File

@@ -20,8 +20,7 @@ class KrtekFixtures extends Fixture
$manager->persist($season); $manager->persist($season);
$season->setName('Krtek Weekend') $season->setName('Krtek Weekend')
->setSeasonCode('12345') ->setSeasonCode('krtek')
->setPreregisterCandidates(true)
->addCandidate(new Candidate('Claudia')) ->addCandidate(new Candidate('Claudia'))
->addCandidate(new Candidate('Eelco')) ->addCandidate(new Candidate('Eelco'))
->addCandidate(new Candidate('Elise')) ->addCandidate(new Candidate('Elise'))
@@ -53,6 +52,7 @@ class KrtekFixtures extends Fixture
->setQuestion('Is de Krtek een man of een vrouw?') ->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Vrouw', true)) ->addAnswer(new Answer('Vrouw', true))
->addAnswer(new Answer('Man')) ->addAnswer(new Answer('Man'))
->setOrdering(1)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -60,6 +60,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Geen', true)) ->addAnswer(new Answer('Geen', true))
->addAnswer(new Answer('1')) ->addAnswer(new Answer('1'))
->addAnswer(new Answer('2')) ->addAnswer(new Answer('2'))
->setOrdering(2)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -69,15 +70,17 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Koningsdag')) ->addAnswer(new Answer('Koningsdag'))
->addAnswer(new Answer('Kerst', true)) ->addAnswer(new Answer('Kerst', true))
->addAnswer(new Answer('Oud en Nieuw')) ->addAnswer(new Answer('Oud en Nieuw'))
->setOrdering(3)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?') ->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?')
->addAnswer(new Answer('Met het OV', true)) ->addAnswer(new Answer('Met het OV', true))
->addAnswer(new Answer('Met de auto')) ->addAnswer(new Answer('Met de auto'))
->setOrdering(4)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
->setQuestion('Met wie keek de Kretek video bij binnenkomst?') ->setQuestion('Met wie keek de Krtek video bij binnenkomst?')
->addAnswer(new Answer('Claudia')) ->addAnswer(new Answer('Claudia'))
->addAnswer(new Answer('Eelco')) ->addAnswer(new Answer('Eelco'))
->addAnswer(new Answer('Elise')) ->addAnswer(new Answer('Elise'))
@@ -91,6 +94,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Remy')) ->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert')) ->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom', true)) ->addAnswer(new Answer('Tom', true))
->setOrdering(5)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -103,6 +107,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Probeer ook eens buiten de lijntjes te kleuren', true)) ->addAnswer(new Answer('Probeer ook eens buiten de lijntjes te kleuren', true))
->addAnswer(new Answer('Ga als je groot bent op groepsreis! ')) ->addAnswer(new Answer('Ga als je groot bent op groepsreis! '))
->addAnswer(new Answer('Trek minder aan van de mening van anderen, het is oké om anders te zijn.')) ->addAnswer(new Answer('Trek minder aan van de mening van anderen, het is oké om anders te zijn.'))
->setOrdering(6)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -113,6 +118,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Pantoffels')) ->addAnswer(new Answer('Pantoffels'))
->addAnswer(new Answer('Hakken')) ->addAnswer(new Answer('Hakken'))
->addAnswer(new Answer('Geen schoenen, alleen sokken')) ->addAnswer(new Answer('Geen schoenen, alleen sokken'))
->setOrdering(7)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -120,12 +126,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Fiets', true)) ->addAnswer(new Answer('Fiets', true))
->addAnswer(new Answer('Auto')) ->addAnswer(new Answer('Auto'))
->addAnswer(new Answer('Trein')) ->addAnswer(new Answer('Trein'))
->setOrdering(8)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
->setQuestion('Heeft de Krtek een eigen auto?') ->setQuestion('Heeft de Krtek een eigen auto?')
->addAnswer(new Answer('Ja')) ->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true)) ->addAnswer(new Answer('Nee', true))
->setOrdering(9)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -145,12 +153,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Pieter')) ->addAnswer(new Answer('Pieter'))
->addAnswer(new Answer('Renée Fokker')) ->addAnswer(new Answer('Renée Fokker'))
->addAnswer(new Answer('Sam, Davy', true)) ->addAnswer(new Answer('Sam, Davy', true))
->setOrdering(10)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topitos uit iemands rugzak stelen om te kunnen winnen?') ->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topitos uit iemands rugzak stelen om te kunnen winnen?')
->addAnswer(new Answer('Ja')) ->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true)) ->addAnswer(new Answer('Nee', true))
->setOrdering(11)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -158,6 +168,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Éénpersoons, losstaand bed')) ->addAnswer(new Answer('Éénpersoons, losstaand bed'))
->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true)) ->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true))
->addAnswer(new Answer('Tweepersoons bed')) ->addAnswer(new Answer('Tweepersoons bed'))
->setOrdering(12)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -166,12 +177,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('6', true)) ->addAnswer(new Answer('6', true))
->addAnswer(new Answer('7')) ->addAnswer(new Answer('7'))
->addAnswer(new Answer('8')) ->addAnswer(new Answer('8'))
->setOrdering(13)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
->setQuestion('Waar zat de Krtek aan tafel bij het diner?') ->setQuestion('Waar zat de Krtek aan tafel bij het diner?')
->addAnswer(new Answer('Met de rug naar de accommodatie')) ->addAnswer(new Answer('Met de rug naar de accommodatie'))
->addAnswer(new Answer('Met de rug naar de buitenmuur', true)) ->addAnswer(new Answer('Met de rug naar de buitenmuur', true))
->setOrdering(14)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -189,6 +202,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Remy')) ->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert')) ->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom')) ->addAnswer(new Answer('Tom'))
->setOrdering(15)
) )
; ;
} }
@@ -203,6 +217,7 @@ class KrtekFixtures extends Fixture
->setQuestion('Is de Krtek een man of een vrouw?') ->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Man')) ->addAnswer(new Answer('Man'))
->addAnswer(new Answer('Vrouw', true)) ->addAnswer(new Answer('Vrouw', true))
->setOrdering(1)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -214,6 +229,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('De Krtek heeft een intolerantie')) ->addAnswer(new Answer('De Krtek heeft een intolerantie'))
->addAnswer(new Answer('De Krtek eet geen rundvlees')) ->addAnswer(new Answer('De Krtek eet geen rundvlees'))
->addAnswer(new Answer('De Krtek eet geen waterdieren')) ->addAnswer(new Answer('De Krtek eet geen waterdieren'))
->setOrdering(2)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -225,6 +241,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Tom')) ->addAnswer(new Answer('Tom'))
->addAnswer(new Answer('De huisdieren van de Krtek hebben geen naam')) ->addAnswer(new Answer('De huisdieren van de Krtek hebben geen naam'))
->addAnswer(new Answer('De Krtek heeft geen huisdieren', true)) ->addAnswer(new Answer('De Krtek heeft geen huisdieren', true))
->setOrdering(3)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -235,6 +252,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Melk')) ->addAnswer(new Answer('Melk'))
->addAnswer(new Answer('Sap')) ->addAnswer(new Answer('Sap'))
->addAnswer(new Answer('Niks')) ->addAnswer(new Answer('Niks'))
->setOrdering(4)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -246,6 +264,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Oostenrijk')) ->addAnswer(new Answer('Oostenrijk'))
->addAnswer(new Answer('Turkije')) ->addAnswer(new Answer('Turkije'))
->addAnswer(new Answer('Zweden', true)) ->addAnswer(new Answer('Zweden', true))
->setOrdering(5)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -255,6 +274,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Het derde groepje')) ->addAnswer(new Answer('Het derde groepje'))
->addAnswer(new Answer('Het vierde groepje')) ->addAnswer(new Answer('Het vierde groepje'))
->addAnswer(new Answer('Het vijfde groepje')) ->addAnswer(new Answer('Het vijfde groepje'))
->setOrdering(6)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -263,12 +283,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Het universum', true)) ->addAnswer(new Answer('Het universum', true))
->addAnswer(new Answer('Toeval')) ->addAnswer(new Answer('Toeval'))
->addAnswer(new Answer('De Krtek is hindoeïstisch')) ->addAnswer(new Answer('De Krtek is hindoeïstisch'))
->setOrdering(7)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?') ->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?')
->addAnswer(new Answer('Ja', true)) ->addAnswer(new Answer('Ja', true))
->addAnswer(new Answer('Nee')) ->addAnswer(new Answer('Nee'))
->setOrdering(8)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -277,6 +299,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true)) ->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true))
->addAnswer(new Answer('Tussen 2:00 en 2:59 uur')) ->addAnswer(new Answer('Tussen 2:00 en 2:59 uur'))
->addAnswer(new Answer('Na 3:00')) ->addAnswer(new Answer('Na 3:00'))
->setOrdering(9)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -285,6 +308,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('2')) ->addAnswer(new Answer('2'))
->addAnswer(new Answer('3')) ->addAnswer(new Answer('3'))
->addAnswer(new Answer('geen', true)) ->addAnswer(new Answer('geen', true))
->setOrdering(10)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -295,6 +319,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Sesamstraat')) ->addAnswer(new Answer('Sesamstraat'))
->addAnswer(new Answer('Spongebob Squarepants')) ->addAnswer(new Answer('Spongebob Squarepants'))
->addAnswer(new Answer('Teletubbies')) ->addAnswer(new Answer('Teletubbies'))
->setOrdering(11)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -302,6 +327,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('In koffer(s)', true)) ->addAnswer(new Answer('In koffer(s)', true))
->addAnswer(new Answer('In losse tas(sen)')) ->addAnswer(new Answer('In losse tas(sen)'))
->addAnswer(new Answer('In een rugzak')) ->addAnswer(new Answer('In een rugzak'))
->setOrdering(12)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -314,12 +340,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Servies dat tegen elkaar klettert')) ->addAnswer(new Answer('Servies dat tegen elkaar klettert'))
->addAnswer(new Answer('Het geroekoe van een duif', true)) ->addAnswer(new Answer('Het geroekoe van een duif', true))
->addAnswer(new Answer('Piepschuim')) ->addAnswer(new Answer('Piepschuim'))
->setOrdering(13)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
->setQuestion('Wilde de Krtek penningmeester worden?') ->setQuestion('Wilde de Krtek penningmeester worden?')
->addAnswer(new Answer('Ja')) ->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true)) ->addAnswer(new Answer('Nee', true))
->setOrdering(14)
) )
->addQuestion((new Question()) ->addQuestion((new Question())
@@ -337,6 +365,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Remy')) ->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert')) ->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom')) ->addAnswer(new Answer('Tom'))
->setOrdering(15)
) )
; ;
} }

View File

@@ -7,6 +7,7 @@ namespace App\Entity;
use App\Repository\AnswerRepository; use App\Repository\AnswerRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Bridge\Doctrine\Types\UuidType;
@@ -21,6 +22,9 @@ class Answer
#[ORM\CustomIdGenerator(class: UuidGenerator::class)] #[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id; private Uuid $id;
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
private int $ordering = 0;
#[ORM\ManyToOne(inversedBy: 'answers')] #[ORM\ManyToOne(inversedBy: 'answers')]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private Question $question; private Question $question;
@@ -72,7 +76,7 @@ class Answer
return $this; return $this;
} }
public function isRightAnswer(): ?bool public function isRightAnswer(): bool
{ {
return $this->isRightAnswer; return $this->isRightAnswer;
} }
@@ -121,4 +125,16 @@ class Answer
return $this; return $this;
} }
public function getOrdering(): int
{
return $this->ordering;
}
public function setOrdering(int $ordering): self
{
$this->ordering = $ordering;
return $this;
}
} }

View File

@@ -14,6 +14,7 @@ use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: CandidateRepository::class)] #[ORM\Entity(repositoryClass: CandidateRepository::class)]
#[ORM\UniqueConstraint(fields: ['name', 'season'])]
class Candidate class Candidate
{ {
#[ORM\Id] #[ORM\Id]
@@ -135,6 +136,6 @@ class Candidate
public function getNameHash(): string public function getNameHash(): string
{ {
return Base64::base64_url_encode($this->name); return Base64::base64UrlEncode($this->name);
} }
} }

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\EliminationRepository;
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\HttpFoundation\InputBag;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: EliminationRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Elimination
{
public const string SCREEN_GREEN = 'green';
public const string SCREEN_RED = 'red';
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id;
/** @var array<string, mixed> */
#[ORM\Column(type: Types::JSON)]
private array $data = [];
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $created;
public function __construct(
#[ORM\ManyToOne(inversedBy: 'eliminations')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz,
) {}
public function getId(): Uuid
{
return $this->id;
}
/** @return array<string, mixed> */
public function getData(): array
{
return $this->data;
}
/** @param array<string, mixed> $data */
public function setData(array $data): self
{
$this->data = $data;
return $this;
}
public function getQuiz(): Quiz
{
return $this->quiz;
}
/** @param InputBag<bool|float|int|string> $inputBag */
public function updateFromInputBag(InputBag $inputBag): self
{
foreach ($this->data as $name => $screenColour) {
$newColour = $inputBag->get('colour-'.mb_strtolower($name));
if (\is_string($newColour)) {
$this->data[$name] = $inputBag->get('colour-'.mb_strtolower($name));
}
}
return $this;
}
public function getScreenColour(?string $name): ?string
{
return $this->data[$name] ?? null;
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$this->created = new DateTimeImmutable();
}
public function getCreated(): \DateTimeInterface
{
return $this->created;
}
}

View File

@@ -20,7 +20,7 @@ class GivenAnswer
#[ORM\Column(type: UuidType::NAME, unique: true)] #[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)] #[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null; private Uuid $id;
#[ORM\ManyToOne(inversedBy: 'givenAnswers')] #[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
@@ -34,8 +34,8 @@ class GivenAnswer
#[ORM\JoinColumn(nullable: true)] #[ORM\JoinColumn(nullable: true)]
private ?Answer $answer = null; private ?Answer $answer = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: false)] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private \DateTimeInterface $created; private \DateTimeImmutable $created;
public function getId(): ?Uuid public function getId(): ?Uuid
{ {
@@ -78,7 +78,7 @@ class GivenAnswer
return $this; return $this;
} }
public function getCreated(): ?\DateTimeInterface public function getCreated(): \DateTimeImmutable
{ {
return $this->created; return $this->created;
} }

View File

@@ -7,6 +7,7 @@ namespace App\Entity;
use App\Repository\QuestionRepository; use App\Repository\QuestionRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Bridge\Doctrine\Types\UuidType;
@@ -21,7 +22,10 @@ class Question
#[ORM\CustomIdGenerator(class: UuidGenerator::class)] #[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null; private ?Uuid $id = null;
#[ORM\Column(length: 255, nullable: false)] #[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
private int $ordering;
#[ORM\Column(type: Types::STRING, length: 255)]
private string $question; private string $question;
#[ORM\ManyToOne(inversedBy: 'questions')] #[ORM\ManyToOne(inversedBy: 'questions')]
@@ -33,6 +37,7 @@ class Question
/** @var Collection<int, Answer> */ /** @var Collection<int, Answer> */
#[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', cascade: ['persist'], orphanRemoval: true)] #[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', cascade: ['persist'], orphanRemoval: true)]
#[ORM\OrderBy(['ordering' => 'ASC'])]
private Collection $answers; private Collection $answers;
public function __construct() public function __construct()
@@ -96,4 +101,35 @@ class Question
return $this; return $this;
} }
public function getErrors(): ?string
{
if (0 === \count($this->answers)) {
return 'This question has no answers';
}
$correctAnswers = $this->answers->filter(static fn (Answer $answer): bool => $answer->isRightAnswer())->count();
if (0 === $correctAnswers) {
return 'This question has no correct answers';
}
if ($correctAnswers > 1) {
return 'This question has multiple correct answers';
}
return null;
}
public function getOrdering(): int
{
return $this->ordering;
}
public function setOrdering(int $ordering): static
{
$this->ordering = $ordering;
return $this;
}
} }

View File

@@ -13,6 +13,7 @@ use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: QuizRepository::class)] #[ORM\Entity(repositoryClass: QuizRepository::class)]
#[ORM\UniqueConstraint(fields: ['name', 'season'])]
class Quiz class Quiz
{ {
#[ORM\Id] #[ORM\Id]
@@ -30,16 +31,26 @@ class Quiz
/** @var Collection<int, Question> */ /** @var Collection<int, Question> */
#[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)] #[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
#[ORM\OrderBy(['ordering' => 'ASC'])]
private Collection $questions; private Collection $questions;
/** @var Collection<int, Correction> */ /** @var Collection<int, Correction> */
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)] #[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
private Collection $corrections; private Collection $corrections;
#[ORM\Column(nullable: false, options: ['default' => 1])]
private int $dropouts = 1;
/** @var Collection<int, Elimination> */
#[ORM\OneToMany(targetEntity: Elimination::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
#[ORM\OrderBy(['created' => 'DESC'])]
private Collection $eliminations;
public function __construct() public function __construct()
{ {
$this->questions = new ArrayCollection(); $this->questions = new ArrayCollection();
$this->corrections = new ArrayCollection(); $this->corrections = new ArrayCollection();
$this->eliminations = new ArrayCollection();
} }
public function getId(): ?Uuid public function getId(): ?Uuid
@@ -102,4 +113,29 @@ class Quiz
return $this; return $this;
} }
public function getDropouts(): int
{
return $this->dropouts;
}
public function setDropouts(int $dropouts): static
{
$this->dropouts = $dropouts;
return $this;
}
/** @return Collection<int, Elimination> */
public function getEliminations(): Collection
{
return $this->eliminations;
}
public function addElimination(Elimination $elimination): self
{
$this->eliminations->add($elimination);
return $this;
}
} }

View File

@@ -15,6 +15,8 @@ use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: SeasonRepository::class)] #[ORM\Entity(repositoryClass: SeasonRepository::class)]
class Season class Season
{ {
private const string SEASON_CODE_CHARACTERS = 'bcdfghjklmnpqrstvwxz';
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: UuidType::NAME)] #[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\GeneratedValue(strategy: 'CUSTOM')]
@@ -27,15 +29,13 @@ class Season
#[ORM\Column(length: 5)] #[ORM\Column(length: 5)]
private string $seasonCode; private string $seasonCode;
#[ORM\Column]
private bool $preregisterCandidates;
/** @var Collection<int, Quiz> */ /** @var Collection<int, Quiz> */
#[ORM\OneToMany(targetEntity: Quiz::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)] #[ORM\OneToMany(targetEntity: Quiz::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)]
private Collection $quizzes; private Collection $quizzes;
/** @var Collection<int, Candidate> */ /** @var Collection<int, Candidate> */
#[ORM\OneToMany(targetEntity: Candidate::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)] #[ORM\OneToMany(targetEntity: Candidate::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
private Collection $candidates; private Collection $candidates;
/** @var Collection<int, User> */ /** @var Collection<int, User> */
@@ -81,18 +81,6 @@ class Season
return $this; return $this;
} }
public function isPreregisterCandidates(): bool
{
return $this->preregisterCandidates;
}
public function setPreregisterCandidates(bool $preregisterCandidates): static
{
$this->preregisterCandidates = $preregisterCandidates;
return $this;
}
/** @return Collection<int, Quiz> */ /** @return Collection<int, Quiz> */
public function getQuizzes(): Collection public function getQuizzes(): Collection
{ {
@@ -158,4 +146,23 @@ class Season
return $this; return $this;
} }
public function isOwner(User $user): bool
{
return $this->owners->contains($user);
}
public function generateSeasonCode(): self
{
$code = '';
$len = mb_strlen(self::SEASON_CODE_CHARACTERS) - 1;
for ($i = 0; $i < 5; ++$i) {
$code .= self::SEASON_CODE_CHARACTERS[random_int(0, $len)];
}
$this->seasonCode = $code;
return $this;
}
} }

View File

@@ -7,9 +7,11 @@ namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
@@ -17,6 +19,7 @@ use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')] #[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
class User implements UserInterface, PasswordAuthenticatedUserInterface class User implements UserInterface, PasswordAuthenticatedUserInterface
{ {
#[ORM\Id] #[ORM\Id]
@@ -29,7 +32,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private string $email; private string $email;
/** @var list<string> The user roles */ /** @var list<string> The user roles */
#[ORM\Column] #[ORM\Column(type: Types::JSON)]
private array $roles = []; private array $roles = [];
/** @var string The hashed password */ /** @var string The hashed password */
@@ -40,6 +43,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\ManyToMany(targetEntity: Season::class, mappedBy: 'owners')] #[ORM\ManyToMany(targetEntity: Season::class, mappedBy: 'owners')]
private Collection $seasons; private Collection $seasons;
#[ORM\Column]
private bool $isVerified = false;
public function __construct() public function __construct()
{ {
$this->seasons = new ArrayCollection(); $this->seasons = new ArrayCollection();
@@ -71,13 +77,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
*/ */
public function getUserIdentifier(): string public function getUserIdentifier(): string
{ {
return $this->email; /** @var non-empty-string $identifier */
$identifier = $this->email;
return $identifier;
} }
/** /**
* @see UserInterface * @see UserInterface
* *
* @return non-empty-list<string> * @return non-empty-array<int<0, max>, string>
*/ */
public function getRoles(): array public function getRoles(): array
{ {
@@ -140,4 +149,21 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function isVerified(): bool
{
return $this->isVerified;
}
public function setIsVerified(bool $isVerified): static
{
$this->isVerified = $isVerified;
return $this;
}
public function isAdmin(): bool
{
return \in_array('ROLE_ADMIN', $this->getRoles(), true);
}
} }

View File

@@ -12,6 +12,6 @@ enum FlashType: string
case Danger = 'danger'; case Danger = 'danger';
case Warning = 'warning'; case Warning = 'warning';
case Info = 'info'; case Info = 'info';
case Ligt = 'light'; case Light = 'light';
case Dark = 'dark'; case Dark = 'dark';
} }

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Exception;
class SpreadsheetDataException extends SpreadsheetException
{
/** @param list<string> $errors */
public function __construct(
public readonly array $errors,
string $message = '',
int $code = 0,
?\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
}

View File

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

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Entity\Elimination;
use App\Entity\Quiz;
use App\Repository\CandidateRepository;
use Doctrine\ORM\EntityManagerInterface;
final readonly class EliminationFactory
{
public function __construct(
private CandidateRepository $candidateRepository,
private EntityManagerInterface $em,
) {}
public function createEliminationFromQuiz(Quiz $quiz): Elimination
{
$elimination = new Elimination($quiz);
$this->em->persist($elimination);
$scores = $this->candidateRepository->getScores($quiz);
$simpleScores = [];
foreach (array_reverse($scores) as $i => $score) {
$simpleScores[$score['name']] = $i < $quiz->getDropouts() ? Elimination::SCREEN_RED : Elimination::SCREEN_GREEN;
}
$elimination->setData($simpleScores);
$this->em->flush();
return $elimination;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<null> */
class AddCandidatesFormType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('candidates', TextareaType::class, [
'label' => $this->translator->trans('Candidates'), 'translation_domain' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\Season;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<Season> */
class CreateSeasonFormType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => $this->translator->trans('Season Name'),
'translation_domain' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Season::class,
]);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<null> */
class EliminationEnterNameType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class,
[
'required' => true,
'label' => $this->translator->trans('Enter name'),
'translation_domain' => false,
'attr' => ['autofocus' => true],
],
)
;
}
}

View File

@@ -7,29 +7,24 @@ namespace App\Form;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<null> */
class EnterNameType extends AbstractType class EnterNameType extends AbstractType
{ {
public function __construct(private TranslatorInterface $translator) public function __construct(private readonly TranslatorInterface $translator) {}
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $builder
->add('name', TextType::class, ->add('name', TextType::class,
['required' => true, 'label' => $this->translator->trans('Enter your name')], [
'required' => true,
'label' => $this->translator->trans('Enter your name'),
'translation_domain' => false,
'attr' => ['autofocus' => true],
],
) )
// ->add('submit', SubmitType::class, ['label' => 'Start quiz'])
; ;
} }
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
} }

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<User>
*/
class RegistrationFormType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class, [
'label' => $this->translator->trans('Email'),
'attr' => ['autocomplete' => 'email'],
'translation_domain' => false,
])
->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
'invalid_message' => $this->translator->trans('The password fields must match.'),
'options' => ['attr' => ['class' => 'password-field']],
'required' => true,
'first_options' => ['label' => $this->translator->trans('Password')],
'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,
]),
],
'translation_domain' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

View File

@@ -9,17 +9,23 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<null> */
class SelectSeasonType extends AbstractType class SelectSeasonType extends AbstractType
{ {
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $builder
->add('season_code', TextType::class, ->add('season_code', TextType::class, [
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/")], 'required' => true,
) 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"),
// ->add('submit', SubmitType::class, ['label' => 'Start quiz']) 'label' => $this->translator->trans('Season Code'),
; 'translation_domain' => false,
'attr' => ['autofocus' => true],
]);
} }
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\Quiz;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<Quiz> */
class UploadQuizFormType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => $this->translator->trans('Quiz name'),
'translation_domain' => false,
])
->add('sheet', FileType::class, [
'label' => $this->translator->trans('Quiz (xlsx)'),
'mapped' => false,
'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'),
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Quiz::class,
]);
}
}

View File

@@ -8,17 +8,13 @@ use Safe\Exceptions\UrlException;
class Base64 class Base64
{ {
private function __construct() public static function base64UrlEncode(string $input): string
{ {
} return rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
public static function base64_url_encode(string $input): string
{
return strtr(base64_encode($input), '+/', '-_');
} }
/** @throws UrlException */ /** @throws UrlException */
public static function base64_url_decode(string $input): string public static function base64UrlDecode(string $input): string
{ {
return \Safe\base64_decode(strtr($input, '-_', '+/'), true); return \Safe\base64_decode(strtr($input, '-_', '+/'), true);
} }

View File

@@ -5,14 +5,21 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Entity\Candidate; use App\Entity\Candidate;
use App\Entity\Correction;
use App\Entity\Quiz;
use App\Entity\Season; use App\Entity\Season;
use App\Helpers\Base64; use App\Helpers\Base64;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
use Safe\Exceptions\UrlException; use Safe\Exceptions\UrlException;
use Symfony\Component\Uid\Uuid;
/** /**
* @extends ServiceEntityRepository<Candidate> * @extends ServiceEntityRepository<Candidate>
*
* @phpstan-type Result array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections?: float, score: float}
* @phpstan-type ResultList list<Result>
*/ */
class CandidateRepository extends ServiceEntityRepository class CandidateRepository extends ServiceEntityRepository
{ {
@@ -24,7 +31,7 @@ class CandidateRepository extends ServiceEntityRepository
public function getCandidateByHash(Season $season, string $hash): ?Candidate public function getCandidateByHash(Season $season, string $hash): ?Candidate
{ {
try { try {
$name = Base64::base64_url_decode($hash); $name = Base64::base64UrlDecode($hash);
} catch (UrlException) { } catch (UrlException) {
return null; return null;
} }
@@ -36,4 +43,59 @@ class CandidateRepository extends ServiceEntityRepository
->setParameter('name', $name) ->setParameter('name', $name)
->getQuery()->getOneOrNullResult(); ->getQuery()->getOneOrNullResult();
} }
public function save(Candidate $candidate, bool $flush = true): void
{
$this->getEntityManager()->persist($candidate);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/** @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;
}
} }

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Elimination;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Elimination>
*/
class EliminationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
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()
// ;
// }
}

View File

@@ -17,4 +17,13 @@ class GivenAnswerRepository extends ServiceEntityRepository
{ {
parent::__construct($registry, GivenAnswer::class); parent::__construct($registry, GivenAnswer::class);
} }
public function save(GivenAnswer $givenAnswer, bool $flush = true): void
{
$this->getEntityManager()->persist($givenAnswer);
if ($flush) {
$this->getEntityManager()->flush();
}
}
} }

View File

@@ -20,13 +20,13 @@ class QuestionRepository extends ServiceEntityRepository
parent::__construct($registry, Question::class); parent::__construct($registry, Question::class);
} }
public function findNextQuestionForCandidate(Candidate $candidate): Question public function findNextQuestionForCandidate(Candidate $candidate): ?Question
{ {
$qb = $this->createQueryBuilder('q'); $qb = $this->createQueryBuilder('q');
return $qb->join('q.quiz', 'qz') return $qb->join('q.quiz', 'qz')
->andWhere($qb->expr()->notIn('q.id', $this->getEntityManager()->createQueryBuilder() ->andWhere($qb->expr()->notIn('q.id', $this->getEntityManager()->createQueryBuilder()
->select('ga.id') ->select('q1')
->from(GivenAnswer::class, 'ga') ->from(GivenAnswer::class, 'ga')
->join('ga.answer', 'a') ->join('ga.answer', 'a')
->join('a.question', 'q1') ->join('a.question', 'q1')
@@ -38,6 +38,6 @@ class QuestionRepository extends ServiceEntityRepository
->setMaxResults(1) ->setMaxResults(1)
->setParameter('candidate', $candidate) ->setParameter('candidate', $candidate)
->setParameter('quiz', $candidate->getSeason()->getActiveQuiz()) ->setParameter('quiz', $candidate->getSeason()->getActiveQuiz())
->getQuery()->getSingleResult(); ->getQuery()->getOneOrNullResult();
} }
} }

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Entity\Season; use App\Entity\Season;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@@ -17,4 +18,15 @@ class SeasonRepository extends ServiceEntityRepository
{ {
parent::__construct($registry, Season::class); parent::__construct($registry, Season::class);
} }
/** @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();
}
} }

View File

@@ -10,11 +10,7 @@ use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/** /** @extends ServiceEntityRepository<User> */
* @extends ServiceEntityRepository<User>
*
* @implements PasswordUpgraderInterface<User>
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{ {
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry)
@@ -22,11 +18,24 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
parent::__construct($registry, User::class); parent::__construct($registry, User::class);
} }
/** Used to upgrade (rehash) the user's password automatically over time. */ /** Used to upgrade (rehash) the user's password automatically over time.
* @param User $user
* */
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{ {
$user->setPassword($newHashedPassword); $user->setPassword($newHashedPassword);
$this->getEntityManager()->persist($user); $this->getEntityManager()->persist($user);
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
} }
public function makeAdmin(string $email): void
{
$user = $this->findOneBy(['email' => $email]);
if (!$user instanceof User) {
throw new \InvalidArgumentException('User not found');
}
$user->setRoles(['ROLE_ADMIN']);
$this->getEntityManager()->flush();
}
} }

Some files were not shown because too many files have changed in this diff Show More