24 Commits

Author SHA1 Message Date
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
113 changed files with 5310 additions and 1729 deletions

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 ###

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,32 @@ 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 --wait --no-build run: docker compose up --wait --no-build
- - name: Coding Style
name: Check HTTP reachability run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none
- name: Check HTTP reachability
run: curl -v --fail-with-body http://localhost run: curl -v --fail-with-body http://localhost
- - name: Check HTTPS reachability
name: Check HTTPS reachability
if: false # Remove this line when the homepage will be configured, or change the path to check 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 run: curl -vk --fail-with-body https://localhost
- - name: Check Mercure reachability
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

15
.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,14 @@ 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 ###

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,36 @@
<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" />
</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,6 +1,74 @@
<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="PhpStanGlobal" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="PhpStanGlobal" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile> </profile>

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>

76
.idea/php.xml generated
View File

@@ -1,32 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="MessDetector">
<phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" timeout="30000" />
</phpmd_settings>
</component>
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</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" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</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">
@@ -38,7 +26,6 @@
<path value="$PROJECT_DIR$/vendor/symfony/string" /> <path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" /> <path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" /> <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/runtime/frankenphp-symfony" />
<path value="$PROJECT_DIR$/vendor/composer" /> <path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" /> <path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
@@ -46,7 +33,6 @@
<path value="$PROJECT_DIR$/vendor/symfony/finder" /> <path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" /> <path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" /> <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/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" /> <path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/psr/cache" /> <path value="$PROJECT_DIR$/vendor/psr/cache" />
@@ -71,7 +57,6 @@
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" /> <path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" /> <path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" /> <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/lexer" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" /> <path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" /> <path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
@@ -113,8 +98,6 @@
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" /> <path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" /> <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/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" /> <path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" /> <path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" /> <path value="$PROJECT_DIR$/vendor/phar-io/version" />
@@ -131,7 +114,6 @@
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" /> <path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/twig/twig" /> <path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" /> <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/psr/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" /> <path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" /> <path value="$PROJECT_DIR$/vendor/symfony/type-info" />
@@ -163,11 +145,35 @@
<path value="$PROJECT_DIR$/vendor/symfony/translation" /> <path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" /> <path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" /> <path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/vincentlanglet/twig-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/jean85/pretty-package-versions" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry-symfony" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry" />
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/symfony/phpunit-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
</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 +187,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,28 +247,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">
<option name="transferred" value="true" />
</component>
</project> </project>

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,11 @@ 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,
'phpdoc_line_span' => ['const' => 'single', 'method' => 'single', 'property' => 'single'],
]) ])
->setRiskyAllowed(true) ->setRiskyAllowed(true)
->setFinder($finder) ->setFinder($finder)

View File

@@ -31,6 +31,7 @@ RUN set -eux; \
intl \ intl \
opcache \ opcache \
zip \ zip \
uuid \
; ;
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser # https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
@@ -67,6 +68,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

34
Justfile Normal file
View File

@@ -0,0 +1,34 @@
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
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 --domain=messages --force --format=yaml --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 }}

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).

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,29 @@ 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:
#- /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
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
@@ -27,4 +39,15 @@ 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 ###

View File

@@ -8,3 +8,21 @@ 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}
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,7 +7,7 @@ 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}
@@ -18,25 +18,12 @@ services:
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 +32,20 @@ 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! ###< doctrine/doctrine-bundle ###
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< 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,53 @@
"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.1",
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3.2",
"easycorp/easyadmin-bundle": "^4.23", "easycorp/easyadmin-bundle": "^4.24.6",
"phpdocumentor/reflection-docblock": "^5.6",
"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/console": "7.2.*", "symfony/console": "7.2.*",
"symfony/dotenv": "7.2.*", "symfony/dotenv": "7.2.*",
"symfony/flex": "^2.4.7", "symfony/flex": "^2.5.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/verify-email-bundle": "^1.17.3",
"thecodingmachine/safe": "^3.1.0"
}, },
"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.12",
"phpstan/phpstan-doctrine": "^2.0", "phpstan/phpstan-doctrine": "^2.0.2",
"phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-phpunit": "^2.0.6",
"phpstan/phpstan-symfony": "^2.0", "phpstan/phpstan-symfony": "^2.0.4",
"phpunit/phpunit": "^11", "phpunit/phpunit": "^12.1.3",
"rector/rector": "^2.0", "rector/rector": "^2.0.12",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*",
"symfony/maker-bundle": "^1.62.1", "symfony/maker-bundle": "^1.62.1",
"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",
"vincentlanglet/twig-cs-fixer": "^3.5.1"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
@@ -66,12 +80,15 @@
"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": {

4148
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,33 @@
<?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 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],
]; ];

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

@@ -1,52 +1,52 @@
doctrine: doctrine:
dbal: dbal:
url: '%env(resolve:DATABASE_URL)%' url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version, # IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file) # either here or in the DATABASE_URL env var (see .env file)
#server_version: '16' #server_version: '16'
profiling_collect_backtrace: '%kernel.debug%' profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true use_savepoints: true
orm: orm:
auto_generate_proxy_classes: true auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true enable_lazy_ghost_objects: true
report_fields_where_declared: true report_fields_where_declared: true
validate_xml_mapping: true validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences: identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true auto_mapping: true
mappings: mappings:
App: App:
type: attribute type: attribute
is_bundle: false is_bundle: false
dir: '%kernel.project_dir%/src/Entity' dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity' prefix: 'App\Entity'
alias: App alias: App
when@test: when@test:
doctrine: doctrine:
dbal: dbal:
# "TEST_TOKEN" is typically set by ParaTest # "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%' dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod: when@prod:
doctrine: doctrine:
orm: orm:
auto_generate_proxy_classes: false auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver: query_cache_driver:
type: pool type: pool
pool: doctrine.system_cache_pool pool: doctrine.system_cache_pool
result_cache_driver: result_cache_driver:
type: pool type: pool
pool: doctrine.result_cache_pool pool: doctrine.result_cache_pool
framework: framework:
cache: cache:
pools: pools:
doctrine.result_cache_pool: doctrine.result_cache_pool:
adapter: cache.app adapter: cache.app
doctrine.system_cache_pool: doctrine.system_cache_pool:
adapter: cache.system adapter: cache.system

View File

@@ -1,6 +1,6 @@
doctrine_migrations: doctrine_migrations:
migrations_paths: migrations_paths:
# namespace is arbitrary but should be different from App\Migrations # namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded # as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations' 'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false enable_profiler: false

View File

@@ -1,15 +1,23 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html # see https://symfony.com/doc/current/reference/configuration/framework.html
framework: framework:
secret: '%env(APP_SECRET)%' secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
# Note that the session will be started ONLY if you read or write from it.
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:
test: true test: true
session: session:
storage_factory_id: session.storage.factory.mock_file storage_factory_id: session.storage.factory.mock_file

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

@@ -1,10 +1,10 @@
framework: framework:
router: router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. # Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost #default_uri: http://localhost
when@prod: when@prod:
framework: framework:
router: router:
strict_requirements: null strict_requirements: null

View File

@@ -1,52 +1,53 @@
security: security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers: password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers: providers:
# used to reload user from session & other features (e.g. switch_user) # used to reload user from session & other features (e.g. switch_user)
app_user_provider: app_user_provider:
entity: entity:
class: App\Entity\User class: App\Entity\User
property: email property: email
# used to reload user from session & other features (e.g. switch_user) # used to reload user from session & other features (e.g. switch_user)
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
main: main:
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
logout: default_target_path: app_backoffice_index
path: app_logout logout:
# where to redirect after logout path: app_login_logout
# target: app_any_route # where to redirect after logout
# target: app_any_route
# activate different ways to authenticate # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall # https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html # https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true # switch_user: true
# 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:
security: security:
password_hashers: password_hashers:
# By default, password hashers are resource intensive and take time. This is # By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes # important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following # are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values. # reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto algorithm: auto
cost: 4 # Lowest possible value for bcrypt cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon memory_cost: 10 # Lowest possible value for argon

View File

@@ -0,0 +1,28 @@
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:
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

@@ -1,7 +1,7 @@
framework: framework:
default_locale: nl default_locale: nl
translator: translator:
default_path: '%kernel.project_dir%/translations' default_path: '%kernel.project_dir%/translations'
fallbacks: fallbacks:
- en - en
providers: providers:

View File

@@ -1,7 +1,7 @@
twig: twig:
file_name_pattern: '*.twig' file_name_pattern: '*.twig'
form_themes: [ 'bootstrap_5_layout.html.twig' ] form_themes: [ 'bootstrap_5_layout.html.twig' ]
when@test: when@test:
twig: twig:
strict_variables: true strict_variables: true

View File

@@ -1,5 +1,5 @@
twig_component: twig_component:
anonymous_template_directory: 'components/' anonymous_template_directory: 'components/'
defaults: defaults:
# Namespace & directory for components # Namespace & directory for components
App\Twig\Components\: 'components/' App\Twig\Components\: 'components/'

View File

@@ -1,4 +1,4 @@
framework: framework:
uid: uid:
default_uuid_version: 7 default_uuid_version: 7
time_based_uuid_version: 7 time_based_uuid_version: 7

View File

@@ -1,11 +1,11 @@
framework: framework:
validation: validation:
# Enables validator auto-mapping support. # Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata. # For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping: #auto_mapping:
# App\Entity\: [] # App\Entity\: []
when@test: when@test:
framework: framework:
validation: validation:
not_compromised_password: false not_compromised_password: false

View File

@@ -1,17 +1,17 @@
when@dev: when@dev:
web_profiler: web_profiler:
toolbar: true toolbar: true
intercept_redirects: false intercept_redirects: false
framework: framework:
profiler: profiler:
only_exceptions: false only_exceptions: false
collect_serializer_data: true collect_serializer_data: true
when@test: when@test:
web_profiler: web_profiler:
toolbar: false toolbar: false
intercept_redirects: false intercept_redirects: false
framework: framework:
profiler: { collect: false } profiler: { collect: false }

View File

@@ -1,5 +1,5 @@
controllers: controllers:
resource: resource:
path: ../src/Controller/ path: ../src/Controller/
namespace: App\Controller namespace: App\Controller
type: attribute type: attribute

View File

@@ -1,9 +1,9 @@
controllers: controllers:
resource: resource:
path: ../../src/Controller/ path: ../../src/Controller/
namespace: App\Controller namespace: App\Controller
type: attribute type: attribute
kernel: kernel:
resource: App\Kernel resource: App\Kernel
type: attribute type: attribute

View File

@@ -1,3 +1,3 @@
easyadmin: easyadmin:
resource: . resource: .
type: easyadmin.routes type: easyadmin.routes

View File

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

View File

@@ -1,3 +1,3 @@
_security_logout: _security_logout:
resource: security.route_loader.logout resource: security.route_loader.logout
type: service type: service

View File

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

View File

@@ -6,19 +6,19 @@
parameters: parameters:
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
_defaults: _defaults:
autowire: true # Automatically injects dependencies in your services. autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services # makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name # this creates a service per class whose id is the fully-qualified class name
App\: App\:
resource: '../src/' resource: '../src/'
exclude: exclude:
- '../src/DependencyInjection/' - '../src/DependencyInjection/'
- '../src/Entity/' - '../src/Entity/'
- '../src/Kernel.php' - '../src/Kernel.php'
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones

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

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

@@ -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

@@ -26,6 +26,7 @@ return RectorConfig::configure()
doctrineCodeQuality: true, doctrineCodeQuality: true,
symfonyCodeQuality: true, symfonyCodeQuality: true,
) )
->withComposerBased(twig: true, doctrine: true, phpunit: true) ->withAttributesSets(all: true)
->withComposerBased(twig: true, doctrine: true, phpunit: true, symfony: true)
->withAttributesSets() ->withAttributesSets()
; ;

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::OPTIONAL, '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,80 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Entity\User;
use App\Repository\CandidateRepository;
use App\Repository\SeasonRepository;
use App\Security\Voter\SeasonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
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\Component\Serializer\SerializerInterface;
#[AsController]
#[IsGranted('ROLE_USER')]
final class BackofficeController extends AbstractController
{
public function __construct(
private readonly SeasonRepository $seasonRepository,
private readonly CandidateRepository $candidateRepository,
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/{seasonCode}', name: 'app_backoffice_season')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function season(Season $season): Response
{
return $this->render('backoffice/season.html.twig', [
'season' => $season,
]);
}
#[Route('/backoffice/{seasonCode}/{quiz}', name: 'app_backoffice_quiz')]
public function quiz(Season $season, Quiz $quiz): Response
{
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $quiz,
'result' => $this->candidateRepository->getScores($quiz),
]);
}
#[Route('/backoffice/{seasonCode}/{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();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
#[Route('/backoffice/{seasonCode}/{quiz}/yaml')]
public function testRoute(Season $season, Quiz $quiz, SerializerInterface $serializer): Response
{
return new Response($serializer->serialize(\App\Resource\Quiz::fromEntity($quiz)->questions, 'yaml', ['yaml_inline' => 100, 'yaml_flags' => 0]), headers: ['Content-Type' => 'text/yaml']);
}
}

View File

@@ -6,12 +6,14 @@ namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
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\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
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): Response
{ {
// get the login error if there is one // get the login error if there is one
@@ -26,8 +28,8 @@ class LoginController extends AbstractController
]); ]);
} }
#[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

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class PrepareEliminationController extends AbstractController
{
#[Route('/backoffice/elimination/prepare', name: 'app_prepare_elimination')]
public function index(): Response
{
return $this->render('prepare_elimination/index.html.twig', [
'controller_name' => 'PrepareEliminationController',
]);
}
}

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('registration/confirmation_email.html.twig')
);
$response = $security->login($user, 'form_login', 'main');
\assert($response instanceof Response);
return $response;
}
return $this->render('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'))
@@ -77,7 +76,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Met de auto')) ->addAnswer(new Answer('Met de auto'))
) )
->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'))

View File

@@ -72,7 +72,7 @@ class Answer
return $this; return $this;
} }
public function isRightAnswer(): ?bool public function isRightAnswer(): bool
{ {
return $this->isRightAnswer; return $this->isRightAnswer;
} }

View File

@@ -135,6 +135,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,45 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\EliminationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: EliminationRepository::class)]
class Elimination
{
#[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 = [];
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): static
{
$this->data = $data;
return $this;
}
}

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

@@ -96,4 +96,23 @@ 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;
}
} }

View File

@@ -36,6 +36,9 @@ class Quiz
#[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: true)]
private ?int $dropouts = null;
public function __construct() public function __construct()
{ {
$this->questions = new ArrayCollection(); $this->questions = new ArrayCollection();
@@ -102,4 +105,16 @@ class Quiz
return $this; return $this;
} }
public function getDropouts(): ?int
{
return $this->dropouts;
}
public function setDropouts(?int $dropouts): static
{
$this->dropouts = $dropouts;
return $this;
}
} }

View File

@@ -27,9 +27,6 @@ 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;
@@ -81,18 +78,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 +143,9 @@ class Season
return $this; return $this;
} }
public function isOwner(User $user): bool
{
return $this->owners->contains($user);
}
} }

View File

@@ -10,6 +10,7 @@ use Doctrine\Common\Collections\Collection;
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 +18,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]
@@ -40,6 +42,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 +76,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 +148,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

@@ -7,14 +7,12 @@ 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
{ {
@@ -22,14 +20,6 @@ class EnterNameType extends AbstractType
->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')],
) )
// ->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,56 @@
<?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\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'],
])
->add('plainPassword', PasswordType::class, [
'label' => $this->translator->trans('Password'),
'mapped' => false,
'attr' => ['autocomplete' => 'new-password'],
'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,
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

View File

@@ -9,16 +9,19 @@ 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}$/"), 'label' => $this->translator->trans('Season Code')]
) )
// ->add('submit', SubmitType::class, ['label' => 'Start quiz'])
; ;
} }

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,20 @@ 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;
/** /**
* @extends ServiceEntityRepository<Candidate> * @extends ServiceEntityRepository<Candidate>
*
* @phpstan-type Result array{0: Candidate, correct: int, time: \DateInterval, corrections?: float, score: float}
* @phpstan-type ResultList list<Result>
*/ */
class CandidateRepository extends ServiceEntityRepository class CandidateRepository extends ServiceEntityRepository
{ {
@@ -24,7 +30,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 +42,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', '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', '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{0: Candidate, 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();
}
} }

42
src/Resource/Quiz.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Resource;
use App\Entity\Answer;
use App\Entity\Quiz as QuizEntity;
use Doctrine\Common\Collections\Collection;
final class Quiz
{
/** @param array<string, array<string, bool>> $questions*/
public function __construct(
public array $questions,
) {}
public static function fromEntity(QuizEntity $quiz): self
{
$questions = [];
foreach ($quiz->getQuestions() as $question) {
$questions[$question->getQuestion()] = self::answerArray($question->getAnswers());
}
return new self($questions);
}
/** @param Collection<int, Answer> $answers
* @return array<string, bool>
**/
private static function answerArray(Collection $answers): array
{
$result = [];
foreach ($answers as $answer) {
$result[$answer->getText()] = $answer->isRightAnswer();
}
return $result;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
class EmailVerifier
{
public function __construct(
private readonly VerifyEmailHelperInterface $verifyEmailHelper,
private readonly MailerInterface $mailer,
private readonly EntityManagerInterface $entityManager,
) {}
public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $email): void
{
$signatureComponents = $this->verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
(string) $user->getId(),
(string) $user->getEmail(),
['id' => $user->getId()]
);
$context = $email->getContext();
$context['signedUrl'] = $signatureComponents->getSignedUrl();
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
$email->context($context);
$this->mailer->send($email);
}
/** @throws VerifyEmailExceptionInterface */
public function handleEmailConfirmation(Request $request, User $user): void
{
$this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), (string) $user->getEmail());
$user->setIsVerified(true);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\Season;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** @extends Voter<string, Season> */
final class SeasonVoter extends Voter
{
public const string EDIT = 'SEASON_EDIT';
public const string DELETE = 'SEASON_DELETE';
protected function supports(string $attribute, mixed $subject): bool
{
return \in_array($attribute, [self::EDIT, self::DELETE], true)
&& $subject instanceof Season;
}
/** @param Season $subject */
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($user->isAdmin()) {
return true;
}
switch ($attribute) {
case self::EDIT:
case self::DELETE:
if ($subject->isOwner($user)) {
return true;
}
break;
}
return false;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\CandidateRepository;
/**
* @phpstan-import-type ResultList from CandidateRepository
*/
class EliminationService
{
/** @phpstan-param ResultList $result */
public function createEliminationFromResult(array $result): void {}
}

View File

@@ -72,19 +72,31 @@
] ]
}, },
"phpunit/phpunit": { "phpunit/phpunit": {
"version": "11.5", "version": "12.1",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "9.6", "version": "10.0",
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326" "ref": "bb22cf8d8c554a623b427d5f3416b538f5525233"
}, },
"files": [ "files": [
".env.test", ".env.test",
"phpunit.xml.dist", "phpunit.dist.xml",
"tests/bootstrap.php" "tests/bootstrap.php"
] ]
}, },
"sentry/sentry-symfony": {
"version": "5.2",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "5.0",
"ref": "c3decefe8a11a5da43adaf827a6cd66695586113"
},
"files": [
"config/packages/sentry.yaml"
]
},
"symfony/console": { "symfony/console": {
"version": "7.2", "version": "7.2",
"recipe": { "recipe": {
@@ -141,6 +153,18 @@
"src/Kernel.php" "src/Kernel.php"
] ]
}, },
"symfony/mailer": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "4.3",
"ref": "09051cfde49476e3c12cd3a0e44289ace1c75a4f"
},
"files": [
"config/packages/mailer.yaml"
]
},
"symfony/maker-bundle": { "symfony/maker-bundle": {
"version": "1.61", "version": "1.61",
"recipe": { "recipe": {
@@ -150,6 +174,21 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
} }
}, },
"symfony/phpunit-bridge": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/routing": { "symfony/routing": {
"version": "7.2", "version": "7.2",
"recipe": { "recipe": {
@@ -248,7 +287,19 @@
"config/routes/web_profiler.yaml" "config/routes/web_profiler.yaml"
] ]
}, },
"symfonycasts/verify-email-bundle": {
"version": "v1.17.3"
},
"twig/extra-bundle": { "twig/extra-bundle": {
"version": "v3.18.0" "version": "v3.18.0"
},
"vincentlanglet/twig-cs-fixer": {
"version": "3.5",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "3.0",
"ref": "d42582ae1bce86fd43491d6264c738b0867f8ffe"
}
} }
} }

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
<title>
{% block title %}Tijd voor de test{% endblock title %}
</title>
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{% endblock %}
</head>
<body>
{% block nav %}
{{ include('backoffice/nav.html.twig') }}
{% endblock nav %}
<main>
<div class="container">
{% for label, messages in app.flashes() %}
{% for message in messages %}
<div class="alert alert-{{ label }} alert-dismissible " role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
{% block body %}
{% endblock body %}
</div>
</main>
</body>
</html>

View File

@@ -0,0 +1,48 @@
{% extends 'backoffice/base.html.twig' %}
{% block title %}Hello BackofficeController!{% endblock %}
{% block body %}
<h2 class="py-2">
{{ is_granted('ROLE_ADMIN') ? 'All Seasons'|trans : 'Your Seasons'|trans }}
</h2>
<table class="table table-hover">
<thead>
<tr>
{% if is_granted('ROLE_ADMIN') %}
<th scope="col">{{ 'Owner(s)'|trans }}</th>
{% endif %}
<th scope="col">{{ 'Name'|trans }}</th>
<th scope="col">{{ 'Active Quiz'|trans }}</th>
<th scope="col">{{ 'Season Code'|trans }}</th>
<th scope="col">{{ 'Manage'|trans }}</th>
</tr>
</thead>
<tbody>
{% for season in seasons %}
<tr class="align-middle">
{% if is_granted('ROLE_ADMIN') %}
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
{% endif %}
<td>{{ season.name }}</td>
<td>
{% if season.activeQuiz %}
{{ season.activeQuiz.name }}
{% else %}
{{ 'No active quiz'|trans }}
{% endif %}
</td>
<td>
<a {% if season.activeQuiz %}href="{{ path('app_quiz_entername', {seasonCode: season.seasonCode}) }}"
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
</td>
<td>
<a href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
</td>
</tr>
{% else %}
EMPTY
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,30 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Tijd voor de test</a>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
{% if is_granted('IS_AUTHENTICATED') %}
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link{% if 'app_backoffice_index' == app.current_route() %} active{% endif %}"
href="{{ path('app_backoffice_index') }}">{{ 'Seasons'|trans }}</a>
</li>
</ul>
<ul class="navbar-nav mb-auto me-2 me-lg-0">
<li class="nav-item">
<a class="nav-link"
href="{{ path('app_login_logout') }}">{{ 'Logout'|trans }}</a>
</li>
</ul>
</div>
{% endif %}
</div>
</nav>

View File

@@ -0,0 +1,3 @@
{% extends 'backoffice/base.html.twig' %}
{% block body %}
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends 'backoffice/base.html.twig' %}
{% block body %}
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
<a class="py-2 btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}"
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
<div id="questions">
<h4 class="py-2">{{ 'Questions'|trans }}</h4>
<div class="accordion">
{% for question in quiz.questions %}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#question-{{ loop.index0 }}"
aria-controls="question-{{ loop.index0 }}">
{% set questionErrors = question.getErrors %}
{% if questionErrors %}
<span data-bs-toggle="tooltip"
title="{{ questionErrors }}"
class="badge text-bg-danger rounded-pill me-2">!</span>
{% endif %}
{{ loop.index }}. {{ question.question }}
</button>
</h2>
<div id="question-{{ loop.index0 }}"
class="accordion-collapse collapse">
<div class="accordion-body">
<ul>
{% for answer in question.answers %}
<li {% if answer.isRightAnswer %}class="text-decoration-underline"{% endif %}>{{ answer.text }}</li>
{% else %}
{{ 'There are no answers for this question'|trans }}
{% endfor %}
</ul>
</div>
</div>
</div>
{% else %}
EMPTY
{% endfor %}
</div>
</div>
<div class="scores">
<p>
<h4>{{ 'Score'|trans }}</h4>
</p>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group btn-group-lg me-2">
<a class="btn btn-primary">{{ 'Start Elimination'|trans }}</a>
</div>
<div class="btn-group btn-group-lg">
<a class="btn btn-secondary">{{ 'Prepare Custom Elimination'|trans }}</a>
<a class="btn btn-secondary">{{ 'Load Prepared Elimination'|trans }}</a>
</div>
</div>
<p>{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }} </p>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{{ 'Candidate'|trans }}</th>
<th scope="col">{{ 'Correct Answers'|trans }}</th>
<th scope="col">{{ 'Corrections'|trans }}</th>
<th scope="col">{{ 'Score'|trans }}</th>
<th scope="col">{{ 'Time'|trans }}</th>
</tr>
</thead>
<tbody>
{% for candidate in result %}
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
<td>{{ candidate.0.name }}</td>
<td>{{ candidate.correct|default('0') }}</td>
<td>{{ candidate.corrections|default('0') }}</td>
<td>{{ candidate.score|default('x') }}</td>
<td>{{ candidate.time }}</td>
</tr>
{% else %}
<tr>
<td colspan="5">{{ 'No results'|trans }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block javascripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
});
</script>
{% endblock javascripts %}
{% block title %}
{% endblock %}

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