mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-07 13:14:20 +01:00
Compare commits
4 Commits
58bda32f09
...
cd5946bda8
| Author | SHA1 | Date | |
|---|---|---|---|
|
cd5946bda8
|
|||
| 25aa8b8622 | |||
| d3e5cb0569 | |||
| e0350c8c31 |
1
.env.dev
1
.env.dev
@@ -2,3 +2,4 @@
|
|||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_SECRET=e26b9552d9e7f969b160373effaa7690
|
APP_SECRET=e26b9552d9e7f969b160373effaa7690
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
MAILER_SENDER=info@tijdvoordetest.nl
|
||||||
|
|||||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -33,15 +33,17 @@ jobs:
|
|||||||
*.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 php database --wait --no-build
|
||||||
|
- name: Lint Twig templates
|
||||||
|
run: docker compose exec -T php bin/console lint:twig --format=github templates
|
||||||
- name: Coding Style
|
- name: Coding Style
|
||||||
run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none
|
run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none
|
||||||
|
- name: Twig Coding Style
|
||||||
|
run: docker compose exec -T php vendor/bin/twig-cs-fixer check
|
||||||
- name: Check HTTP reachability
|
- name: Check HTTP reachability
|
||||||
run: curl -v --fail-with-body http://localhost
|
run: curl -v --fail-with-body http://localhost
|
||||||
- name: Check HTTPS reachability
|
|
||||||
if: false # Remove this line when the homepage will be configured, or change the path to check
|
|
||||||
run: curl -vk --fail-with-body https://localhost
|
|
||||||
- name: Check Mercure reachability
|
- name: Check Mercure reachability
|
||||||
|
if: false
|
||||||
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
|
||||||
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
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -114,3 +114,8 @@ phpstan.neon
|
|||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
/phpunit.xml
|
/phpunit.xml
|
||||||
###< symfony/phpunit-bridge ###
|
###< symfony/phpunit-bridge ###
|
||||||
|
|
||||||
|
###> symfony/asset-mapper ###
|
||||||
|
/public/assets/
|
||||||
|
/assets/vendor/
|
||||||
|
###< symfony/asset-mapper ###
|
||||||
|
|||||||
5
.idea/TijdVoorDeTest.iml
generated
5
.idea/TijdVoorDeTest.iml
generated
@@ -159,6 +159,11 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset-mapper" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/sass-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/intl-extra" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
293
.idea/php.xml
generated
293
.idea/php.xml
generated
@@ -1,5 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="LaravelPint">
|
||||||
|
<laravel_pint_settings>
|
||||||
|
<laravel_pint_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8">
|
||||||
|
<option name="timeout" value="30000" />
|
||||||
|
</laravel_pint_by_interpreter>
|
||||||
|
</laravel_pint_settings>
|
||||||
|
</component>
|
||||||
|
<component name="MessDetector">
|
||||||
|
<phpmd_settings>
|
||||||
|
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="30000" />
|
||||||
|
</phpmd_settings>
|
||||||
|
</component>
|
||||||
<component name="MessDetectorOptionsConfiguration">
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
@@ -29,159 +41,164 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="PhpIncludePathManager">
|
<component name="PhpIncludePathManager">
|
||||||
<include_path>
|
<include_path>
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/rector/rector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
||||||
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
||||||
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/child-process" />
|
<path value="$PROJECT_DIR$/vendor/react/child-process" />
|
||||||
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
<path value="$PROJECT_DIR$/vendor/jean85/pretty-package-versions" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sentry/sentry" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/rector/rector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sentry/sentry-symfony" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-symfony" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-doctrine" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/extension-installer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/form" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/asset-mapper" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/phpunit-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
||||||
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
|
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/ux-twig-component" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/easycorp/easyadmin-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpstan/extension-installer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-doctrine" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-symfony" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/phpstan-safe-rule" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/form" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/html-extra" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/easycorp/easyadmin-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/ux-twig-component" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
|
<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/guzzlehttp/psr7" />
|
||||||
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||||
<path value="$PROJECT_DIR$/vendor/jean85/pretty-package-versions" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
|
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
|
<path value="$PROJECT_DIR$/vendor/symfonycasts/sass-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sentry/sentry-symfony" />
|
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sentry/sentry" />
|
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
<path value="$PROJECT_DIR$/vendor/vincentlanglet/twig-cs-fixer" />
|
||||||
<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/reflection-common" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
<path value="$PROJECT_DIR$/vendor/thecodingmachine/phpstan-safe-rule" />
|
||||||
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
|
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
|
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
|
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
|
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
|
||||||
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
|
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/intl-extra" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/html-extra" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpInterpreters">
|
<component name="PhpInterpreters">
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ RUN set -eux; \
|
|||||||
zip \
|
zip \
|
||||||
uuid \
|
uuid \
|
||||||
gd \
|
gd \
|
||||||
excimer \
|
excimer-1.2.3 \
|
||||||
;
|
;
|
||||||
|
|
||||||
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
||||||
@@ -99,4 +99,7 @@ RUN set -eux; \
|
|||||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
composer dump-autoload --classmap-authoritative --no-dev; \
|
||||||
composer dump-env prod; \
|
composer dump-env prod; \
|
||||||
composer run-script --no-dev post-install-cmd; \
|
composer run-script --no-dev post-install-cmd; \
|
||||||
chmod +x bin/console; sync;
|
chmod +x bin/console; \
|
||||||
|
bin/console sass:build; \
|
||||||
|
bin/console asset-map:compile --no-debug --quiet --no-ansi; \
|
||||||
|
sync;
|
||||||
|
|||||||
5
Justfile
5
Justfile
@@ -34,3 +34,8 @@ rector *args:
|
|||||||
|
|
||||||
phpstan *args:
|
phpstan *args:
|
||||||
docker compose exec php vendor/bin/phpstan analyse {{ args }}
|
docker compose exec php vendor/bin/phpstan analyse {{ args }}
|
||||||
|
|
||||||
|
[confirm]
|
||||||
|
clean:
|
||||||
|
docker compose down -v --remove-orphans
|
||||||
|
rm -rf vendor var assets/vendor public/assets public/bundles .php-cs-fixer.cache .twig-cs-fixer.cache
|
||||||
|
|||||||
4
assets/backoffice.js
Normal file
4
assets/backoffice.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
|
import * as bootstrap from 'bootstrap'
|
||||||
|
|
||||||
|
import './styles/app.scss';
|
||||||
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
4
assets/quiz.js
Normal file
4
assets/quiz.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
|
import * as bootstrap from 'bootstrap'
|
||||||
|
|
||||||
|
import './styles/quiz.scss'
|
||||||
0
assets/styles/app.scss
Normal file
0
assets/styles/app.scss
Normal file
12
assets/styles/quiz.scss
Normal file
12
assets/styles/quiz.scss
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
background-image: url("../img/background.png");
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
@@ -31,6 +31,17 @@ services:
|
|||||||
- target: 443
|
- target: 443
|
||||||
published: ${HTTP3_PORT:-443}
|
published: ${HTTP3_PORT:-443}
|
||||||
protocol: udp
|
protocol: udp
|
||||||
|
sass:
|
||||||
|
image: ${IMAGES_PREFIX:-}app-php
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
entrypoint: ''
|
||||||
|
depends_on:
|
||||||
|
- php
|
||||||
|
command:
|
||||||
|
- bin/console
|
||||||
|
- sass:build
|
||||||
|
- --watch
|
||||||
|
|
||||||
###> symfony/mercure-bundle ###
|
###> symfony/mercure-bundle ###
|
||||||
###< symfony/mercure-bundle ###
|
###< symfony/mercure-bundle ###
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ services:
|
|||||||
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
|
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
|
||||||
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure}
|
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure}
|
||||||
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||||
# The two next lines can be removed after initial installation
|
|
||||||
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
|
|
||||||
STABILITY: ${STABILITY:-stable}
|
|
||||||
volumes:
|
volumes:
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
|
|||||||
@@ -11,18 +11,19 @@
|
|||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"doctrine/dbal": "^4.2.3",
|
"doctrine/dbal": "^4.2.3",
|
||||||
"doctrine/doctrine-bundle": "^2.14.0",
|
"doctrine/doctrine-bundle": "^2.14.0",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.4.1",
|
"doctrine/doctrine-migrations-bundle": "^3.4.2",
|
||||||
"doctrine/orm": "^3.3.2",
|
"doctrine/orm": "^3.3.3",
|
||||||
"easycorp/easyadmin-bundle": "^4.24.6",
|
"easycorp/easyadmin-bundle": "^4.24.7",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6.2",
|
||||||
"phpoffice/phpspreadsheet": "*",
|
"phpoffice/phpspreadsheet": "^4.2.0",
|
||||||
"phpstan/phpdoc-parser": "^2.1",
|
"phpstan/phpdoc-parser": "^2.1",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
"sentry/sentry-symfony": "^5.2",
|
"sentry/sentry-symfony": "^5.2",
|
||||||
"symfony/asset": "7.2.*",
|
"symfony/asset": "7.2.*",
|
||||||
|
"symfony/asset-mapper": "7.2.*",
|
||||||
"symfony/console": "7.2.*",
|
"symfony/console": "7.2.*",
|
||||||
"symfony/dotenv": "7.2.*",
|
"symfony/dotenv": "7.2.*",
|
||||||
"symfony/flex": "^2.5.0",
|
"symfony/flex": "^2.7.0",
|
||||||
"symfony/form": "7.2.*",
|
"symfony/form": "7.2.*",
|
||||||
"symfony/framework-bundle": "7.2.*",
|
"symfony/framework-bundle": "7.2.*",
|
||||||
"symfony/mailer": "7.2.*",
|
"symfony/mailer": "7.2.*",
|
||||||
@@ -35,28 +36,32 @@
|
|||||||
"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.*",
|
||||||
|
"symfonycasts/sass-bundle": "^0.8.2",
|
||||||
"symfonycasts/verify-email-bundle": "^1.17.3",
|
"symfonycasts/verify-email-bundle": "^1.17.3",
|
||||||
"thecodingmachine/safe": "^3.1.0"
|
"thecodingmachine/safe": "^3.3.0",
|
||||||
|
"twig/extra-bundle": "^3.21",
|
||||||
|
"twig/intl-extra": "^3.21",
|
||||||
|
"twig/twig": "^3.21.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/doctrine-fixtures-bundle": "^4.1",
|
"doctrine/doctrine-fixtures-bundle": "^4.1",
|
||||||
"friendsofphp/php-cs-fixer": "^3.75.0",
|
"friendsofphp/php-cs-fixer": "^3.75.0",
|
||||||
"phpstan/extension-installer": "^1.4.3",
|
"phpstan/extension-installer": "^1.4.3",
|
||||||
"phpstan/phpstan": "^2.1.12",
|
"phpstan/phpstan": "^2.1.17",
|
||||||
"phpstan/phpstan-doctrine": "^2.0.2",
|
"phpstan/phpstan-doctrine": "^2.0.3",
|
||||||
"phpstan/phpstan-phpunit": "^2.0.6",
|
"phpstan/phpstan-phpunit": "^2.0.6",
|
||||||
"phpstan/phpstan-symfony": "^2.0.4",
|
"phpstan/phpstan-symfony": "^2.0.6",
|
||||||
"phpunit/phpunit": "^12.1.3",
|
"phpunit/phpunit": "^12.1.6",
|
||||||
"rector/rector": "^2.0.12",
|
"rector/rector": "^2.0.16",
|
||||||
"roave/security-advisories": "dev-latest",
|
"roave/security-advisories": "dev-latest",
|
||||||
"symfony/browser-kit": "7.2.*",
|
"symfony/browser-kit": "7.2.*",
|
||||||
"symfony/css-selector": "7.2.*",
|
"symfony/css-selector": "7.2.*",
|
||||||
"symfony/maker-bundle": "^1.62.1",
|
"symfony/maker-bundle": "^1.63.0",
|
||||||
"symfony/phpunit-bridge": "^7.2",
|
"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.4",
|
"thecodingmachine/phpstan-safe-rule": "^1.4.1",
|
||||||
"vincentlanglet/twig-cs-fixer": "^3.5.1"
|
"vincentlanglet/twig-cs-fixer": "^3.7.1"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
@@ -94,7 +99,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"auto-scripts": {
|
"auto-scripts": {
|
||||||
"cache:clear": "symfony-cmd",
|
"cache:clear": "symfony-cmd",
|
||||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||||
|
"importmap:install": "symfony-cmd"
|
||||||
},
|
},
|
||||||
"post-install-cmd": [
|
"post-install-cmd": [
|
||||||
"@auto-scripts"
|
"@auto-scripts"
|
||||||
|
|||||||
1331
composer.lock
generated
1331
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ use Symfony\Bundle\TwigBundle\TwigBundle;
|
|||||||
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
|
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
|
||||||
use Symfony\UX\TwigComponent\TwigComponentBundle;
|
use Symfony\UX\TwigComponent\TwigComponentBundle;
|
||||||
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;
|
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;
|
||||||
|
use Symfonycasts\SassBundle\SymfonycastsSassBundle;
|
||||||
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
|
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -30,4 +31,5 @@ return [
|
|||||||
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
SymfonyCastsVerifyEmailBundle::class => ['all' => true],
|
SymfonyCastsVerifyEmailBundle::class => ['all' => true],
|
||||||
SentryBundle::class => ['prod' => true],
|
SentryBundle::class => ['prod' => true],
|
||||||
|
SymfonycastsSassBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
15
config/packages/asset_mapper.yaml
Normal file
15
config/packages/asset_mapper.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
framework:
|
||||||
|
asset_mapper:
|
||||||
|
# The paths to make available to the asset mapper.
|
||||||
|
paths:
|
||||||
|
- assets/
|
||||||
|
excluded_patterns:
|
||||||
|
- '*/assets/styles/_*.scss'
|
||||||
|
- '*/assets/styles/**/_*.scss'
|
||||||
|
missing_import_mode: strict
|
||||||
|
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
framework:
|
||||||
|
asset_mapper:
|
||||||
|
missing_import_mode: warn
|
||||||
35
importmap.php
Normal file
35
importmap.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the importmap for this application.
|
||||||
|
*
|
||||||
|
* - "path" is a path inside the asset mapper system. Use the
|
||||||
|
* "debug:asset-map" command to see the full list of paths.
|
||||||
|
*
|
||||||
|
* - "entrypoint" (JavaScript only) set to true for any module that will
|
||||||
|
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
||||||
|
*
|
||||||
|
* The "importmap:require" command can be used to add new entries to this file.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'quiz' => [
|
||||||
|
'path' => './assets/quiz.js',
|
||||||
|
'entrypoint' => true,
|
||||||
|
],
|
||||||
|
'backoffice' => [
|
||||||
|
'path' => './assets/backoffice.js',
|
||||||
|
'entrypoint' => true,
|
||||||
|
],
|
||||||
|
'bootstrap' => [
|
||||||
|
'version' => '5.3.6',
|
||||||
|
],
|
||||||
|
'@popperjs/core' => [
|
||||||
|
'version' => '2.11.8',
|
||||||
|
],
|
||||||
|
'bootstrap/dist/css/bootstrap.min.css' => [
|
||||||
|
'version' => '5.3.6',
|
||||||
|
'type' => 'css',
|
||||||
|
],
|
||||||
|
];
|
||||||
41
migrations/Version20250504101440.php
Normal file
41
migrations/Version20250504101440.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250504101440 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE UNIQUE INDEX UNIQ_C8B28E445E237E064EC001D1 ON candidate (name, season_id)
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE UNIQUE INDEX UNIQ_A412FA925E237E064EC001D1 ON quiz (name, season_id)
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP INDEX UNIQ_A412FA925E237E064EC001D1
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP INDEX UNIQ_C8B28E445E237E064EC001D1
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
migrations/Version20250521192752.php
Normal file
54
migrations/Version20250521192752.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250521192752 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination ADD quiz_id UUID NOT NULL
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination ADD CONSTRAINT FK_5947284F853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE INDEX IDX_5947284F853CD175 ON elimination (quiz_id)
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE quiz ALTER dropouts SET DEFAULT 1
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE quiz ALTER dropouts SET NOT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE quiz ALTER dropouts DROP DEFAULT
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE quiz ALTER dropouts DROP NOT NULL
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination DROP CONSTRAINT FK_5947284F853CD175
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP INDEX IDX_5947284F853CD175
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination DROP quiz_id
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
migrations/Version20250521194035.php
Normal file
38
migrations/Version20250521194035.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250521194035 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination ADD created TIMESTAMP(0) WITHOUT TIME ZONE
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
UPDATE elimination SET created = NOW()
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination ALTER created SET NOT NULL
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination DROP created
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,5 +30,4 @@ return RectorConfig::configure()
|
|||||||
)
|
)
|
||||||
->withAttributesSets(all: true)
|
->withAttributesSets(all: true)
|
||||||
->withComposerBased(twig: true, doctrine: true, phpunit: true, symfony: true)
|
->withComposerBased(twig: true, doctrine: true, phpunit: true, symfony: true)
|
||||||
->withAttributesSets()
|
|
||||||
;
|
;
|
||||||
|
|||||||
67
src/Command/ClaimSeasonCommand.php
Normal file
67
src/Command/ClaimSeasonCommand.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Repository\SeasonRepository;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:claim-season',
|
||||||
|
description: 'Give a user owner rights on a season',
|
||||||
|
)]
|
||||||
|
class ClaimSeasonCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly UserRepository $userRepository,
|
||||||
|
private readonly SeasonRepository $seasonRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addArgument('email', InputArgument::REQUIRED, 'The email of the user to make admin')
|
||||||
|
->addArgument('season', InputArgument::REQUIRED, 'The season to claim')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$email = $input->getArgument('email');
|
||||||
|
$seasonCode = $input->getArgument('season');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$season = $this->seasonRepository->findOneBy(['seasonCode' => $seasonCode]);
|
||||||
|
if (null === $season) {
|
||||||
|
throw new \InvalidArgumentException('Season not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->userRepository->findOneBy(['email' => $email]);
|
||||||
|
if (null === $user) {
|
||||||
|
throw new \InvalidArgumentException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$season->addOwner($user);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
} catch (\InvalidArgumentException $invalidArgumentException) {
|
||||||
|
$io->error($invalidArgumentException->getMessage());
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ class MakeAdminCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->addArgument('email', InputArgument::OPTIONAL, 'The email of the user to make admin')
|
->addArgument('email', InputArgument::REQUIRED, 'The email of the user to make admin')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
src/Controller/Backoffice/BackofficeController.php
Normal file
79
src/Controller/Backoffice/BackofficeController.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Backoffice;
|
||||||
|
|
||||||
|
use App\Controller\AbstractController;
|
||||||
|
use App\Entity\Season;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Form\CreateSeasonFormType;
|
||||||
|
use App\Repository\SeasonRepository;
|
||||||
|
use App\Service\QuizSpreadsheetService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[AsController]
|
||||||
|
#[IsGranted('ROLE_USER')]
|
||||||
|
final class BackofficeController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly SeasonRepository $seasonRepository,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/backoffice/', name: 'app_backoffice_index')]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
\assert($user instanceof User);
|
||||||
|
|
||||||
|
$seasons = $this->security->isGranted('ROLE_ADMIN')
|
||||||
|
? $this->seasonRepository->findAll()
|
||||||
|
: $this->seasonRepository->getSeasonsForUser($user);
|
||||||
|
|
||||||
|
return $this->render('backoffice/index.html.twig', [
|
||||||
|
'seasons' => $seasons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
|
||||||
|
public function addSeason(Request $request, EntityManagerInterface $em): Response
|
||||||
|
{
|
||||||
|
$season = new Season();
|
||||||
|
$form = $this->createForm(CreateSeasonFormType::class, $season);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$user = $this->getUser();
|
||||||
|
\assert($user instanceof User);
|
||||||
|
|
||||||
|
$season->addOwner($user);
|
||||||
|
$season->generateSeasonCode();
|
||||||
|
|
||||||
|
$em->persist($season);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
|
||||||
|
public function getTemplate(QuizSpreadsheetService $excel): Response
|
||||||
|
{
|
||||||
|
$response = new StreamedResponse($excel->generateTemplate());
|
||||||
|
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Controller/Backoffice/QuizController.php
Normal file
50
src/Controller/Backoffice/QuizController.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Backoffice;
|
||||||
|
|
||||||
|
use App\Controller\AbstractController;
|
||||||
|
use App\Entity\Quiz;
|
||||||
|
use App\Entity\Season;
|
||||||
|
use App\Repository\CandidateRepository;
|
||||||
|
use App\Security\Voter\SeasonVoter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[AsController]
|
||||||
|
#[IsGranted('ROLE_USER')]
|
||||||
|
class QuizController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CandidateRepository $candidateRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz')]
|
||||||
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
|
public function index(Season $season, Quiz $quiz): Response
|
||||||
|
{
|
||||||
|
return $this->render('backoffice/quiz.html.twig', [
|
||||||
|
'season' => $season,
|
||||||
|
'quiz' => $quiz,
|
||||||
|
'result' => $this->candidateRepository->getScores($quiz),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable')]
|
||||||
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
|
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
|
||||||
|
{
|
||||||
|
$season->setActiveQuiz($quiz);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
if ($quiz instanceof Quiz) {
|
||||||
|
return $this->redirectToRoute('app_backoffice_quiz', ['seasonCode' => $season->getSeasonCode(), 'quiz' => $quiz->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/Controller/Backoffice/SeasonController.php
Normal file
88
src/Controller/Backoffice/SeasonController.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Backoffice;
|
||||||
|
|
||||||
|
use App\Controller\AbstractController;
|
||||||
|
use App\Entity\Candidate;
|
||||||
|
use App\Entity\Quiz;
|
||||||
|
use App\Entity\Season;
|
||||||
|
use App\Enum\FlashType;
|
||||||
|
use App\Form\AddCandidatesFormType;
|
||||||
|
use App\Form\UploadQuizFormType;
|
||||||
|
use App\Security\Voter\SeasonVoter;
|
||||||
|
use App\Service\QuizSpreadsheetService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
#[AsController]
|
||||||
|
#[IsGranted('ROLE_USER')]
|
||||||
|
class SeasonController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly TranslatorInterface $translator, private EntityManagerInterface $em,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/backoffice/season/{seasonCode}', name: 'app_backoffice_season')]
|
||||||
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
|
public function index(Season $season): Response
|
||||||
|
{
|
||||||
|
return $this->render('backoffice/season.html.twig', [
|
||||||
|
'season' => $season,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/backoffice/season/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
|
||||||
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
|
public function addCandidates(Season $season, Request $request): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(AddCandidatesFormType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$candidates = $form->get('candidates')->getData();
|
||||||
|
foreach (explode("\r\n", (string) $candidates) as $candidate) {
|
||||||
|
$season->addCandidate(new Candidate($candidate));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/backoffice/season/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
|
||||||
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
|
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
|
||||||
|
{
|
||||||
|
$quiz = new Quiz();
|
||||||
|
$form = $this->createForm(UploadQuizFormType::class, $quiz);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
/* @var UploadedFile $sheet */
|
||||||
|
$sheet = $form->get('sheet')->getData();
|
||||||
|
|
||||||
|
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
|
||||||
|
|
||||||
|
$quiz->setSeason($season);
|
||||||
|
$this->em->persist($quiz);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Entity\Candidate;
|
|
||||||
use App\Entity\Quiz;
|
|
||||||
use App\Entity\Season;
|
|
||||||
use App\Entity\User;
|
|
||||||
use App\Enum\FlashType;
|
|
||||||
use App\Form\AddCandidatesFormType;
|
|
||||||
use App\Form\CreateSeasonFormType;
|
|
||||||
use App\Form\UploadQuizFormType;
|
|
||||||
use App\Repository\CandidateRepository;
|
|
||||||
use App\Repository\SeasonRepository;
|
|
||||||
use App\Security\Voter\SeasonVoter;
|
|
||||||
use App\Service\QuizSpreadsheetService;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
||||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
|
|
||||||
#[AsController]
|
|
||||||
#[IsGranted('ROLE_USER')]
|
|
||||||
final class BackofficeController extends AbstractController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly SeasonRepository $seasonRepository,
|
|
||||||
private readonly CandidateRepository $candidateRepository,
|
|
||||||
private readonly Security $security,
|
|
||||||
private readonly TranslatorInterface $translator,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route('/backoffice/', name: 'app_backoffice_index')]
|
|
||||||
public function index(): Response
|
|
||||||
{
|
|
||||||
$user = $this->getUser();
|
|
||||||
\assert($user instanceof User);
|
|
||||||
|
|
||||||
$seasons = $this->security->isGranted('ROLE_ADMIN')
|
|
||||||
? $this->seasonRepository->findAll()
|
|
||||||
: $this->seasonRepository->getSeasonsForUser($user);
|
|
||||||
|
|
||||||
return $this->render('backoffice/index.html.twig', [
|
|
||||||
'seasons' => $seasons,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
|
|
||||||
public function seasonAdd(Request $request, EntityManagerInterface $em): Response
|
|
||||||
{
|
|
||||||
$season = new Season();
|
|
||||||
$form = $this->createForm(CreateSeasonFormType::class, $season);
|
|
||||||
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
$user = $this->getUser();
|
|
||||||
\assert($user instanceof User);
|
|
||||||
|
|
||||||
$season->addOwner($user);
|
|
||||||
$season->generateSeasonCode();
|
|
||||||
|
|
||||||
$em->persist($season);
|
|
||||||
$em->flush();
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/backoffice/{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}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
|
|
||||||
public function addCandidates(Season $season, Request $request, EntityManagerInterface $em): Response
|
|
||||||
{
|
|
||||||
$form = $this->createForm(AddCandidatesFormType::class);
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
$candidates = $form->get('candidates')->getData();
|
|
||||||
foreach (explode("\r\n", $candidates) as $candidate) {
|
|
||||||
$season->addCandidate(new Candidate($candidate));
|
|
||||||
}
|
|
||||||
$em->flush();
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/backoffice/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
|
|
||||||
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet, EntityManagerInterface $em): Response
|
|
||||||
{
|
|
||||||
$quiz = new Quiz();
|
|
||||||
$form = $this->createForm(UploadQuizFormType::class, $quiz);
|
|
||||||
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
/* @var UploadedFile $sheet */
|
|
||||||
$sheet = $form->get('sheet')->getData();
|
|
||||||
|
|
||||||
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
|
|
||||||
|
|
||||||
$quiz->setSeason($season);
|
|
||||||
$em->persist($quiz);
|
|
||||||
$em->flush();
|
|
||||||
|
|
||||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
|
|
||||||
public function getTemplate(QuizSpreadsheetService $excel): Response
|
|
||||||
{
|
|
||||||
$response = new StreamedResponse($excel->generateTemplate());
|
|
||||||
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
||||||
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
src/Controller/EliminationController.php
Normal file
54
src/Controller/EliminationController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Candidate;
|
||||||
|
use App\Entity\Season;
|
||||||
|
use App\Enum\FlashType;
|
||||||
|
use App\Helpers\Base64;
|
||||||
|
use App\Repository\CandidateRepository;
|
||||||
|
use App\Security\Voter\SeasonVoter;
|
||||||
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
use function Symfony\Component\Translation\t;
|
||||||
|
|
||||||
|
#[AsController]
|
||||||
|
#[IsGranted('ROLE_USER')]
|
||||||
|
final class EliminationController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||||
|
|
||||||
|
#[Route('/elimination/{seasonCode}', name: 'app_elimination')]
|
||||||
|
#[IsGranted(SeasonVoter::ELIMINATION, 'season')]
|
||||||
|
public function index(#[MapEntity] Season $season): Response
|
||||||
|
{
|
||||||
|
return $this->render('elimination/index.html.twig', [
|
||||||
|
'controller_name' => 'EliminationController',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/elimination/{seasonCode}/{candidateHash}', name: 'app_elimination_cadidate')]
|
||||||
|
#[IsGranted(SeasonVoter::ELIMINATION, 'season')]
|
||||||
|
public function candidateScreen(Season $season, string $candidateHash, CandidateRepository $candidateRepository): Response
|
||||||
|
{
|
||||||
|
$candidate = $candidateRepository->getCandidateByHash($season, $candidateHash);
|
||||||
|
if (!$candidate instanceof Candidate) {
|
||||||
|
$this->addFlash(FlashType::Warning,
|
||||||
|
t('Cound not find candidate with name %name%', ['%name%' => Base64::base64UrlDecode($candidateHash)])->trans($this->translator)
|
||||||
|
);
|
||||||
|
throw new \InvalidArgumentException('Candidate not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('elimination/candidate.html.twig', [
|
||||||
|
'season' => $season,
|
||||||
|
'candidate' => $candidate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,17 +4,19 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use App\Enum\FlashType;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
#[AsController]
|
#[AsController]
|
||||||
final class LoginController extends AbstractController
|
final class LoginController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route(path: '/login', name: 'app_login_login')]
|
#[Route(path: '/login', name: 'app_login_login')]
|
||||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
public function login(AuthenticationUtils $authenticationUtils, TranslatorInterface $translator): Response
|
||||||
{
|
{
|
||||||
// get the login error if there is one
|
// get the login error if there is one
|
||||||
$error = $authenticationUtils->getLastAuthenticationError();
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
@@ -22,7 +24,11 @@ final class LoginController extends AbstractController
|
|||||||
// last username entered by the user
|
// last username entered by the user
|
||||||
$lastUsername = $authenticationUtils->getLastUsername();
|
$lastUsername = $authenticationUtils->getLastUsername();
|
||||||
|
|
||||||
return $this->render('login/login.html.twig', [
|
if ($error instanceof AuthenticationException) {
|
||||||
|
$this->addFlash(FlashType::Danger, $translator->trans($error->getMessageKey(), $error->getMessageData(), 'security'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('backoffice/login/login.html.twig', [
|
||||||
'last_username' => $lastUsername,
|
'last_username' => $lastUsername,
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -4,17 +4,39 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Elimination;
|
||||||
|
use App\Entity\Quiz;
|
||||||
|
use App\Entity\Season;
|
||||||
|
use App\Factory\EliminationFactory;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
final class PrepareEliminationController extends AbstractController
|
final class PrepareEliminationController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/backoffice/elimination/prepare', name: 'app_prepare_elimination')]
|
#[Route('/backoffice/elimination/{seasonCode}/{quiz}/prepare', name: 'app_prepare_elimination')]
|
||||||
public function index(): Response
|
public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
|
||||||
{
|
{
|
||||||
return $this->render('prepare_elimination/index.html.twig', [
|
$elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_prepare_elimination_view', ['elimination' => $elimination->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/backoffice/elimination/{elimination}', name: 'app_prepare_elimination_view')]
|
||||||
|
public function viewElimination(Elimination $elimination, Request $request, EntityManagerInterface $em): Response
|
||||||
|
{
|
||||||
|
if ('POST' === $request->getMethod()) {
|
||||||
|
$elimination->updateFromInputBag($request->request);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Elimination updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('backoffice/prepare_elimination/index.html.twig', [
|
||||||
'controller_name' => 'PrepareEliminationController',
|
'controller_name' => 'PrepareEliminationController',
|
||||||
|
'elimination' => $elimination,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ final class RegistrationController extends AbstractController
|
|||||||
(new TemplatedEmail())
|
(new TemplatedEmail())
|
||||||
->to((string) $user->getEmail())
|
->to((string) $user->getEmail())
|
||||||
->subject($this->translator->trans('Please Confirm your Email'))
|
->subject($this->translator->trans('Please Confirm your Email'))
|
||||||
->htmlTemplate('registration/confirmation_email.html.twig')
|
->htmlTemplate('backoffice/registration/confirmation_email.html.twig')
|
||||||
);
|
);
|
||||||
|
|
||||||
$response = $security->login($user, 'form_login', 'main');
|
$response = $security->login($user, 'form_login', 'main');
|
||||||
@@ -57,7 +57,7 @@ final class RegistrationController extends AbstractController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('registration/register.html.twig', [
|
return $this->render('backoffice/registration/register.html.twig', [
|
||||||
'registrationForm' => $form,
|
'registrationForm' => $form,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Answer
|
|||||||
private Uuid $id;
|
private Uuid $id;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
|
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
|
||||||
private int $ordering;
|
private int $ordering = 0;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'answers')]
|
#[ORM\ManyToOne(inversedBy: 'answers')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use Symfony\Bridge\Doctrine\Types\UuidType;
|
|||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: CandidateRepository::class)]
|
#[ORM\Entity(repositoryClass: CandidateRepository::class)]
|
||||||
|
#[ORM\UniqueConstraint(fields: ['name', 'season'])]
|
||||||
class Candidate
|
class Candidate
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ namespace App\Entity;
|
|||||||
use App\Repository\EliminationRepository;
|
use App\Repository\EliminationRepository;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Safe\DateTimeImmutable;
|
||||||
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\Component\HttpFoundation\InputBag;
|
||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: EliminationRepository::class)]
|
#[ORM\Entity(repositoryClass: EliminationRepository::class)]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
class Elimination
|
class Elimination
|
||||||
{
|
{
|
||||||
|
public const string SCREEN_GREEN = 'green';
|
||||||
|
public const string SCREEN_RED = 'red';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
@@ -24,6 +30,15 @@ class Elimination
|
|||||||
#[ORM\Column(type: Types::JSON)]
|
#[ORM\Column(type: Types::JSON)]
|
||||||
private array $data = [];
|
private array $data = [];
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||||
|
private \DateTimeImmutable $created;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'eliminations')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private Quiz $quiz,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function getId(): Uuid
|
public function getId(): Uuid
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -36,10 +51,39 @@ class Elimination
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $data */
|
/** @param array<string, mixed> $data */
|
||||||
public function setData(array $data): static
|
public function setData(array $data): self
|
||||||
{
|
{
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getQuiz(): Quiz
|
||||||
|
{
|
||||||
|
return $this->quiz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param InputBag<bool|float|int|string> $inputBag */
|
||||||
|
public function updateFromInputBag(InputBag $inputBag): self
|
||||||
|
{
|
||||||
|
foreach ($this->data as $name => $screenColour) {
|
||||||
|
$newColour = $inputBag->get('colour-'.mb_strtolower($name));
|
||||||
|
if (\is_string($newColour)) {
|
||||||
|
$this->data[$name] = $inputBag->get('colour-'.mb_strtolower($name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PrePersist]
|
||||||
|
public function setCreatedAtValue(): void
|
||||||
|
{
|
||||||
|
$this->created = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreated(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->created;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use Symfony\Bridge\Doctrine\Types\UuidType;
|
|||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: QuizRepository::class)]
|
#[ORM\Entity(repositoryClass: QuizRepository::class)]
|
||||||
|
#[ORM\UniqueConstraint(fields: ['name', 'season'])]
|
||||||
class Quiz
|
class Quiz
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -37,13 +38,19 @@ 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)]
|
#[ORM\Column(nullable: false, options: ['default' => 1])]
|
||||||
private ?int $dropouts = null;
|
private int $dropouts = 1;
|
||||||
|
|
||||||
|
/** @var Collection<int, Elimination> */
|
||||||
|
#[ORM\OneToMany(targetEntity: Elimination::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
|
||||||
|
#[ORM\OrderBy(['created' => 'DESC'])]
|
||||||
|
private Collection $eliminations;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->questions = new ArrayCollection();
|
$this->questions = new ArrayCollection();
|
||||||
$this->corrections = new ArrayCollection();
|
$this->corrections = new ArrayCollection();
|
||||||
|
$this->eliminations = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?Uuid
|
public function getId(): ?Uuid
|
||||||
@@ -107,15 +114,28 @@ class Quiz
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDropouts(): ?int
|
public function getDropouts(): int
|
||||||
{
|
{
|
||||||
return $this->dropouts;
|
return $this->dropouts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDropouts(?int $dropouts): static
|
public function setDropouts(int $dropouts): static
|
||||||
{
|
{
|
||||||
$this->dropouts = $dropouts;
|
$this->dropouts = $dropouts;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, Elimination> */
|
||||||
|
public function getEliminations(): Collection
|
||||||
|
{
|
||||||
|
return $this->eliminations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addElimination(Elimination $elimination): self
|
||||||
|
{
|
||||||
|
$this->eliminations->add($elimination);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class Season
|
|||||||
|
|
||||||
/** @var Collection<int, Candidate> */
|
/** @var Collection<int, Candidate> */
|
||||||
#[ORM\OneToMany(targetEntity: Candidate::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: Candidate::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)]
|
||||||
|
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||||
private Collection $candidates;
|
private Collection $candidates;
|
||||||
|
|
||||||
/** @var Collection<int, User> */
|
/** @var Collection<int, User> */
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace App\Entity;
|
|||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
|
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
|
||||||
use Symfony\Bridge\Doctrine\Types\UuidType;
|
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||||
@@ -31,7 +32,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
private string $email;
|
private string $email;
|
||||||
|
|
||||||
/** @var list<string> The user roles */
|
/** @var list<string> The user roles */
|
||||||
#[ORM\Column]
|
#[ORM\Column(type: Types::JSON)]
|
||||||
private array $roles = [];
|
private array $roles = [];
|
||||||
|
|
||||||
/** @var string The hashed password */
|
/** @var string The hashed password */
|
||||||
|
|||||||
38
src/Factory/EliminationFactory.php
Normal file
38
src/Factory/EliminationFactory.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Factory;
|
||||||
|
|
||||||
|
use App\Entity\Elimination;
|
||||||
|
use App\Entity\Quiz;
|
||||||
|
use App\Repository\CandidateRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
final readonly class EliminationFactory
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private CandidateRepository $candidateRepository,
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function createEliminationFromQuiz(Quiz $quiz): Elimination
|
||||||
|
{
|
||||||
|
$elimination = new Elimination($quiz);
|
||||||
|
$this->em->persist($elimination);
|
||||||
|
|
||||||
|
$scores = $this->candidateRepository->getScores($quiz);
|
||||||
|
|
||||||
|
$simpleScores = [];
|
||||||
|
|
||||||
|
foreach (array_reverse($scores) as $i => $score) {
|
||||||
|
$simpleScores[$score['name']] = $i < $quiz->getDropouts() ? Elimination::SCREEN_RED : Elimination::SCREEN_GREEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
$elimination->setData($simpleScores);
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $elimination;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ class AddCandidatesFormType extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('candidates', TextareaType::class, [
|
->add('candidates', TextareaType::class, [
|
||||||
'label' => $this->translator->trans('Candidates'),
|
'label' => $this->translator->trans('Candidates'), 'translation_domain' => false,
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class CreateSeasonFormType extends AbstractType
|
|||||||
$builder
|
$builder
|
||||||
->add('name', TextType::class, [
|
->add('name', TextType::class, [
|
||||||
'label' => $this->translator->trans('Season Name'),
|
'label' => $this->translator->trans('Season Name'),
|
||||||
|
'translation_domain' => false,
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ class EnterNameType extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('name', TextType::class,
|
->add('name', TextType::class,
|
||||||
['required' => true, 'label' => $this->translator->trans('Enter your name')],
|
[
|
||||||
|
'required' => true,
|
||||||
|
'label' => $this->translator->trans('Enter your name'),
|
||||||
|
'translation_domain' => false,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Entity\User;
|
|||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||||
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\Length;
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
@@ -27,11 +28,16 @@ class RegistrationFormType extends AbstractType
|
|||||||
->add('email', EmailType::class, [
|
->add('email', EmailType::class, [
|
||||||
'label' => $this->translator->trans('Email'),
|
'label' => $this->translator->trans('Email'),
|
||||||
'attr' => ['autocomplete' => 'email'],
|
'attr' => ['autocomplete' => 'email'],
|
||||||
|
'translation_domain' => false,
|
||||||
])
|
])
|
||||||
->add('plainPassword', PasswordType::class, [
|
->add('plainPassword', RepeatedType::class, [
|
||||||
'label' => $this->translator->trans('Password'),
|
'type' => PasswordType::class,
|
||||||
|
'invalid_message' => $this->translator->trans('The password fields must match.'),
|
||||||
|
'options' => ['attr' => ['class' => 'password-field']],
|
||||||
|
'required' => true,
|
||||||
|
'first_options' => ['label' => $this->translator->trans('Password')],
|
||||||
|
'second_options' => ['label' => $this->translator->trans('Repeat Password')],
|
||||||
'mapped' => false,
|
'mapped' => false,
|
||||||
'attr' => ['autocomplete' => 'new-password'],
|
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new NotBlank([
|
new NotBlank([
|
||||||
'message' => 'Please enter a password',
|
'message' => 'Please enter a password',
|
||||||
@@ -43,6 +49,7 @@ class RegistrationFormType extends AbstractType
|
|||||||
'max' => 4096,
|
'max' => 4096,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
'translation_domain' => false,
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class SelectSeasonType extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('season_code', TextType::class,
|
->add('season_code', TextType::class,
|
||||||
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), 'label' => $this->translator->trans('Season Code')]
|
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), 'label' => $this->translator->trans('Season Code'), 'translation_domain' => false]
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ class UploadQuizFormType extends AbstractType
|
|||||||
$builder
|
$builder
|
||||||
->add('name', TextType::class, [
|
->add('name', TextType::class, [
|
||||||
'label' => $this->translator->trans('Quiz name'),
|
'label' => $this->translator->trans('Quiz name'),
|
||||||
|
'translation_domain' => false,
|
||||||
])
|
])
|
||||||
->add('sheet', FileType::class, [
|
->add('sheet', FileType::class, [
|
||||||
'label' => $this->translator->trans('Quiz (xlsx)'),
|
'label' => $this->translator->trans('Quiz (xlsx)'),
|
||||||
'mapped' => false,
|
'mapped' => false,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
'translation_domain' => false,
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new File([
|
new File([
|
||||||
'maxSize' => '1024k',
|
'maxSize' => '1024k',
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Safe\Exceptions\UrlException;
|
use Safe\Exceptions\UrlException;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends ServiceEntityRepository<Candidate>
|
* @extends ServiceEntityRepository<Candidate>
|
||||||
*
|
*
|
||||||
* @phpstan-type Result array{0: Candidate, correct: int, time: \DateInterval, corrections?: float, score: float}
|
* @phpstan-type Result array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections?: float, score: float}
|
||||||
* @phpstan-type ResultList list<Result>
|
* @phpstan-type ResultList list<Result>
|
||||||
*/
|
*/
|
||||||
class CandidateRepository extends ServiceEntityRepository
|
class CandidateRepository extends ServiceEntityRepository
|
||||||
@@ -56,7 +57,7 @@ class CandidateRepository extends ServiceEntityRepository
|
|||||||
public function getScores(Quiz $quiz): array
|
public function getScores(Quiz $quiz): array
|
||||||
{
|
{
|
||||||
$scoreTimeQb = $this->createQueryBuilder('c', 'c.id')
|
$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')
|
->select('c.id', 'c.name', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'max(ga.created) - min(ga.created) as time')
|
||||||
->join('c.givenAnswers', 'ga')
|
->join('c.givenAnswers', 'ga')
|
||||||
->join('ga.answer', 'a')
|
->join('ga.answer', 'a')
|
||||||
->where('ga.quiz = :quiz')
|
->where('ga.quiz = :quiz')
|
||||||
@@ -64,7 +65,7 @@ class CandidateRepository extends ServiceEntityRepository
|
|||||||
->setParameter('quiz', $quiz);
|
->setParameter('quiz', $quiz);
|
||||||
|
|
||||||
$correctionsQb = $this->createQueryBuilder('c', 'c.id')
|
$correctionsQb = $this->createQueryBuilder('c', 'c.id')
|
||||||
->select('c', 'cor.amount as corrections')
|
->select('c.id', 'cor.amount as corrections')
|
||||||
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
|
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
|
||||||
->setParameter('quiz', $quiz);
|
->setParameter('quiz', $quiz);
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ class CandidateRepository extends ServiceEntityRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array{0: Candidate, correct: int, time: \DateInterval, corrections?: float}> $in
|
* @param array<string, array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections?: float}> $in
|
||||||
*
|
*
|
||||||
* @return array<string, Result>
|
* @return array<string, Result>
|
||||||
* */
|
* */
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ final class SeasonVoter extends Voter
|
|||||||
{
|
{
|
||||||
public const string EDIT = 'SEASON_EDIT';
|
public const string EDIT = 'SEASON_EDIT';
|
||||||
|
|
||||||
|
public const string ELIMINATION = 'SEASON_ELIMINATION';
|
||||||
|
|
||||||
public const string DELETE = 'SEASON_DELETE';
|
public const string DELETE = 'SEASON_DELETE';
|
||||||
|
|
||||||
protected function supports(string $attribute, mixed $subject): bool
|
protected function supports(string $attribute, mixed $subject): bool
|
||||||
{
|
{
|
||||||
return \in_array($attribute, [self::EDIT, self::DELETE], true)
|
return \in_array($attribute, [self::EDIT, self::DELETE, self::ELIMINATION], true)
|
||||||
&& $subject instanceof Season;
|
&& $subject instanceof Season;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,16 +36,9 @@ final class SeasonVoter extends Voter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($attribute) {
|
return match ($attribute) {
|
||||||
case self::EDIT:
|
self::EDIT, self::DELETE, self::ELIMINATION => $subject->isOwner($user),
|
||||||
case self::DELETE:
|
default => false,
|
||||||
if ($subject->isOwner($user)) {
|
};
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,14 +52,14 @@ class QuizSpreadsheetService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @throws SpreadsheetDataException */
|
/** @throws SpreadsheetDataException */
|
||||||
public function xlsxToQuiz(Quiz $quiz, File $file): Quiz
|
public function xlsxToQuiz(Quiz $quiz, File $file): void
|
||||||
{
|
{
|
||||||
$spreadsheet = $this->readSheet($file);
|
$spreadsheet = $this->readSheet($file);
|
||||||
$sheet = $spreadsheet->getSheet($spreadsheet->getFirstSheetIndex());
|
$sheet = $spreadsheet->getSheet($spreadsheet->getFirstSheetIndex());
|
||||||
|
|
||||||
$answerLines = \array_slice($sheet->toArray(formatData: false), 1);
|
$answerLines = \array_slice($sheet->toArray(formatData: false), 1);
|
||||||
|
|
||||||
return $this->fillQuizFromArray($quiz, $answerLines);
|
$this->fillQuizFromArray($quiz, $answerLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function readSheet(File $file): Spreadsheet
|
private function readSheet(File $file): Spreadsheet
|
||||||
@@ -94,6 +94,7 @@ class QuizSpreadsheetService
|
|||||||
if (1 === $answerCounter) {
|
if (1 === $answerCounter) {
|
||||||
$errors[] = \sprintf('Question %d has no answers', $answerCounter);
|
$errors[] = \sprintf('Question %d has no answers', $answerCounter);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
symfony.lock
18
symfony.lock
@@ -97,6 +97,21 @@
|
|||||||
"config/packages/sentry.yaml"
|
"config/packages/sentry.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/asset-mapper": {
|
||||||
|
"version": "7.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.4",
|
||||||
|
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets/quiz.js",
|
||||||
|
"assets/styles/app.css",
|
||||||
|
"config/packages/asset_mapper.yaml",
|
||||||
|
"importmap.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/console": {
|
"symfony/console": {
|
||||||
"version": "7.2",
|
"version": "7.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@@ -287,6 +302,9 @@
|
|||||||
"config/routes/web_profiler.yaml"
|
"config/routes/web_profiler.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfonycasts/sass-bundle": {
|
||||||
|
"version": "v0.8.2"
|
||||||
|
},
|
||||||
"symfonycasts/verify-email-bundle": {
|
"symfonycasts/verify-email-bundle": {
|
||||||
"version": "v1.17.3"
|
"version": "v1.17.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,49 +1,3 @@
|
|||||||
<!DOCTYPE html>
|
{% extends 'base.html.twig' %}
|
||||||
<html lang="nl" data-bs-theme="dark">
|
{% block importmap %}{{ importmap('backoffice') }}{% endblock %}
|
||||||
<head>
|
{% block nav %}{{ include('backoffice/nav.html.twig') }}{% endblock %}
|
||||||
<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>
|
|
||||||
<script
|
|
||||||
src="https://js-de.sentry-cdn.com/30cf438bc708c97e6f45c127bed9af96.min.js"
|
|
||||||
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>
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">Tijd voor de test</a>
|
<a class="navbar-brand" href="{{ path('app_backoffice_index') }}">Tijd voor de test</a>
|
||||||
<button class="navbar-toggler"
|
<button class="navbar-toggler"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="collapse"
|
data-bs-toggle="collapse"
|
||||||
|
|||||||
@@ -1,3 +1,46 @@
|
|||||||
{% extends 'backoffice/base.html.twig' %}
|
{% extends 'backoffice/base.html.twig' %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
<div class="row">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ path('app_backoffice_index') }}">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a
|
||||||
|
href="{{ path('app_backoffice_season', {seasonCode: elimination.quiz.season.seasonCode}) }}">{{ elimination.quiz.season.name }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item"><a
|
||||||
|
href="{{ path('app_backoffice_quiz', {seasonCode: elimination.quiz.season.seasonCode, quiz: elimination.quiz.id}) }}">{{ elimination.quiz.name }}</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Prepare Elimination</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<form method="post">
|
||||||
|
{%~ for candidate, colour in elimination.data %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for="colour-{{ candidate|lower }}" class="col-4 col-form-label">{{ candidate }}</label>
|
||||||
|
<div class="col-4">
|
||||||
|
<select id="colour-{{ candidate|lower }}" class="form-select"
|
||||||
|
name="colour-{{ candidate|lower }}">
|
||||||
|
<option value="green"{% if colour == 'green' %} selected{% endif %}>Green</option>
|
||||||
|
<option value="red"{% if colour == 'red' %} selected{% endif %}>Red</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="btn-group py-2">
|
||||||
|
<button type="submit" class="btn btn-primary" name="start" value="1">{{ 'Save'|trans }}</button>
|
||||||
|
<button type="submit" class="btn btn-success" name="start"
|
||||||
|
value="0">{{ 'Save and start elimination'|trans }}</button>
|
||||||
|
<a href="{{ path('app_backoffice_quiz', {seasonCode: elimination.quiz.season.seasonCode, quiz: elimination.quiz.id}) }}"
|
||||||
|
class="btn btn-secondary">{{ 'Back'|trans }}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Hier kan dus weer wat uitleg komen</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
|
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
|
||||||
<a class="py-2 btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}"
|
<div class="py-2 btn-group">
|
||||||
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
|
<a class="btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}"
|
||||||
|
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
|
||||||
|
{% if quiz is same as (season.activeQuiz) %}
|
||||||
|
<a class="btn btn-secondary"
|
||||||
|
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="questions">
|
<div id="questions">
|
||||||
<h4 class="py-2">{{ 'Questions'|trans }}</h4>
|
<h4 class="py-2">{{ 'Questions'|trans }}</h4>
|
||||||
<div class="accordion">
|
<div class="accordion">
|
||||||
{% for question in quiz.questions %}
|
{%~ for question in quiz.questions ~%}
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header">
|
<h2 class="accordion-header">
|
||||||
<button class="accordion-button collapsed"
|
<button class="accordion-button collapsed"
|
||||||
@@ -16,23 +23,23 @@
|
|||||||
data-bs-target="#question-{{ loop.index0 }}"
|
data-bs-target="#question-{{ loop.index0 }}"
|
||||||
aria-controls="question-{{ loop.index0 }}">
|
aria-controls="question-{{ loop.index0 }}">
|
||||||
{% set questionErrors = question.getErrors %}
|
{% set questionErrors = question.getErrors %}
|
||||||
{% if questionErrors %}
|
{%~ if questionErrors -%}
|
||||||
<span data-bs-toggle="tooltip"
|
<span data-bs-toggle="tooltip"
|
||||||
title="{{ questionErrors }}"
|
title="{{ questionErrors }}"
|
||||||
class="badge text-bg-danger rounded-pill me-2">!</span>
|
class="badge text-bg-danger rounded-pill me-2">!</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ loop.index }}. {{ question.question }}
|
{{~ loop.index -}}. {{ question.question -}}
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="question-{{ loop.index0 }}"
|
<div id="question-{{ loop.index0 }}"
|
||||||
class="accordion-collapse collapse">
|
class="accordion-collapse collapse">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<ul>
|
<ul>
|
||||||
{% for answer in question.answers %}
|
{%~ for answer in question.answers %}
|
||||||
<li {% if answer.isRightAnswer %}class="text-decoration-underline"{% endif %}>{{ answer.text }}</li>
|
<li{% if answer.isRightAnswer %} class="text-decoration-underline"{% endif %}>{{ answer.text -}}</li>
|
||||||
{% else %}
|
{%~ else %}
|
||||||
{{ 'There are no answers for this question'|trans }}
|
{{ 'There are no answers for this question'|trans -}}
|
||||||
{% endfor %}
|
{%~ endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,16 +50,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scores">
|
<div class="scores">
|
||||||
<p>
|
<h4 class="py-2">{{ 'Score'|trans }}</h4>
|
||||||
<h4>{{ 'Score'|trans }}</h4>
|
|
||||||
</p>
|
|
||||||
<div class="btn-toolbar" role="toolbar">
|
<div class="btn-toolbar" role="toolbar">
|
||||||
<div class="btn-group btn-group-lg me-2">
|
<div class="btn-group btn-group-lg me-2">
|
||||||
<a class="btn btn-primary">{{ 'Start Elimination'|trans }}</a>
|
<a class="btn btn-primary">{{ 'Start Elimination'|trans }}</a>
|
||||||
</div>
|
<a href="{{ path('app_prepare_elimination', {seasonCode: season.seasonCode, quiz: quiz.id}) }}"
|
||||||
<div class="btn-group btn-group-lg">
|
class="btn btn-secondary">{{ 'Prepare Custom Elimination'|trans }}</a>
|
||||||
<a class="btn btn-secondary">{{ 'Prepare Custom Elimination'|trans }}</a>
|
{%~ if not quiz.eliminations.empty %}
|
||||||
<a class="btn btn-secondary">{{ 'Load Prepared Elimination'|trans }}</a>
|
<button class="btn btn-secondary dropdown-toggle"
|
||||||
|
data-bs-toggle="dropdown">{{ 'Load Prepared Elimination'|trans }}</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{%~ for elimination in quiz.eliminations %}
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
href="{{ path('app_prepare_elimination_view', {elimination: elimination.id}) }}">{{ elimination.created|format_datetime() }}</a>
|
||||||
|
</li>
|
||||||
|
{%~ endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }} </p>
|
<p>{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }} </p>
|
||||||
@@ -67,9 +81,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for candidate in result %}
|
{%~ for candidate in result ~%}
|
||||||
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
|
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
|
||||||
<td>{{ candidate.0.name }}</td>
|
<td>{{ candidate.name }}</td>
|
||||||
<td>{{ candidate.correct|default('0') }}</td>
|
<td>{{ candidate.correct|default('0') }}</td>
|
||||||
<td>{{ candidate.corrections|default('0') }}</td>
|
<td>{{ candidate.corrections|default('0') }}</td>
|
||||||
<td>{{ candidate.score|default('x') }}</td>
|
<td>{{ candidate.score|default('x') }}</td>
|
||||||
@@ -85,6 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
|
{{ parent() }}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-12">
|
<div class="col-md-6 col-12">
|
||||||
<h2 class="py-2">{{ 'Add a quiz to '|trans }} {{ season.name }}</h2>
|
<h2 class="py-2">{{ t('Add a quiz to %name%', {'%name%': season.name})|trans }} </h2>
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
{{ form_row(form.name) }}
|
{{ form_row(form.name) }}
|
||||||
{{ form_row(form.sheet) }}
|
{{ form_row(form.sheet) }}
|
||||||
|
|||||||
29
templates/base.html.twig
Normal file
29
templates/base.html.twig
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script
|
||||||
|
src="https://js-de.sentry-cdn.com/30cf438bc708c97e6f45c127bed9af96.min.js"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
<title>
|
||||||
|
{% block title %}Tijd voor de test{% endblock title %}
|
||||||
|
</title>
|
||||||
|
{% block stylesheets %}{% endblock %}
|
||||||
|
{% block javascripts %}{% block importmap %}{% endblock %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock nav %}
|
||||||
|
<main>
|
||||||
|
{% block main %}
|
||||||
|
<div class="container">
|
||||||
|
{{ include('flashes.html.twig') }}
|
||||||
|
{% block body %}
|
||||||
|
{% endblock body %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
templates/flashes.html.twig
Normal file
13
templates/flashes.html.twig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% set flashes = app.flashes() %}
|
||||||
|
{% if flashes is not empty %}
|
||||||
|
<div class="py-2">
|
||||||
|
{% for label, messages in 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 %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -1,55 +1,2 @@
|
|||||||
<!DOCTYPE html>
|
{% extends 'base.html.twig' %}
|
||||||
<html lang="nl" data-bs-theme="dark">
|
{% block importmap %}{{ importmap('quiz') }}{% endblock %}
|
||||||
<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>
|
|
||||||
<script
|
|
||||||
src="https://js-de.sentry-cdn.com/30cf438bc708c97e6f45c127bed9af96.min.js"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
></script>
|
|
||||||
<title>
|
|
||||||
{% block title %}
|
|
||||||
Tijd voor de test
|
|
||||||
{% endblock title %}
|
|
||||||
</title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
background-image: url("{{ asset('/img/background.png') }}");
|
|
||||||
background-position: center center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<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>
|
|
||||||
{% block script %}
|
|
||||||
{% endblock script %}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
3
templates/quiz/elimination/candidate.html.twig
Normal file
3
templates/quiz/elimination/candidate.html.twig
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{% block body %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
20
templates/quiz/elimination/index.html.twig
Normal file
20
templates/quiz/elimination/index.html.twig
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<title>Hello EliminationController!</title>
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<style>
|
||||||
|
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
|
||||||
|
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="example-wrapper">
|
||||||
|
<h1>Hello {{ controller_name }}! ✅</h1>
|
||||||
|
|
||||||
|
This friendly message is coming from:
|
||||||
|
<ul>
|
||||||
|
<li>Your controller at <code>/app/src/Controller/EliminationController.php</code></li>
|
||||||
|
<li>Your template at <code>/app/templates/elimination/index.html.twig</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
'Active Quiz': 'Actieve test'
|
'Active Quiz': 'Actieve test'
|
||||||
Add: Toevoegen
|
Add: Toevoegen
|
||||||
'Add Candidate': 'Voeg kandidaat toe'
|
'Add Candidate': 'Voeg kandidaat toe'
|
||||||
'Add a quiz to': 'Voeg een test toe aan'
|
'Add Candidates': 'Voeg kandidaten toe'
|
||||||
|
'Add a quiz to %name%': 'Voeg een test toe aan %name%'
|
||||||
'All Seasons': 'Alle seizoenen'
|
'All Seasons': 'Alle seizoenen'
|
||||||
'Already have an account? Log in': 'Heb je al een account? Log in'
|
'Already have an account? Log in': 'Heb je al een account? Log in'
|
||||||
|
Back: Terug
|
||||||
Candidate: Kandidaat
|
Candidate: Kandidaat
|
||||||
'Candidate not found': 'Kandidaat niet gevonden'
|
'Candidate not found': 'Kandidaat niet gevonden'
|
||||||
Candidates: Kandidaten
|
Candidates: Kandidaten
|
||||||
'Correct Answers': 'Goede antwoorden'
|
'Correct Answers': 'Goede antwoorden'
|
||||||
Corrections: Jokers
|
Corrections: Jokers
|
||||||
|
'Cound not find candidate with name %name%': 'Kon kandidaat met naam %name% niet vinden'
|
||||||
'Create a season': 'Maak een seizoen aan'
|
'Create a season': 'Maak een seizoen aan'
|
||||||
'Create an account': 'Maak een account aan'
|
'Create an account': 'Maak een account aan'
|
||||||
|
'Deactivate Quiz': 'Deactiveer test'
|
||||||
'Download Template': 'Download sjabloon'
|
'Download Template': 'Download sjabloon'
|
||||||
Email: E-mail
|
Email: E-mail
|
||||||
'Enter your name': 'Voor je naam in'
|
'Enter your name': 'Voor je naam in'
|
||||||
'Invalid season code': 'Ongeldige seizoenscode'
|
'Invalid season code': 'Ongeldige seizoencode'
|
||||||
'Load Prepared Elimination': 'Laad voorbereide eliminatie'
|
'Load Prepared Elimination': 'Laad voorbereide eliminatie'
|
||||||
Logout: Uitloggen
|
Logout: Uitloggen
|
||||||
'Make active': 'Maak actief'
|
'Make active': 'Maak actief'
|
||||||
@@ -39,14 +43,18 @@ Quiz: Test
|
|||||||
Quizzes: Tests
|
Quizzes: Tests
|
||||||
Register: Registreren
|
Register: Registreren
|
||||||
'Remember me': 'Onthoud mij'
|
'Remember me': 'Onthoud mij'
|
||||||
|
'Repeat Password': 'Herhaal wachtwoord'
|
||||||
|
Save: Opslaan
|
||||||
|
'Save and start elimination': 'Opslaan en start eliminatie'
|
||||||
Score: Score
|
Score: Score
|
||||||
Season: Seizoen
|
Season: Seizoen
|
||||||
'Season Code': Seizoenscode
|
'Season Code': Seizoencode
|
||||||
'Season Name': 'Seizoensnaam'
|
'Season Name': Seizoennaam
|
||||||
Seasons: Seizoenen
|
Seasons: Seizoenen
|
||||||
'Sign in': 'Log in'
|
'Sign in': 'Log in'
|
||||||
'Start Elimination': 'Start eliminatie'
|
'Start Elimination': 'Start eliminatie'
|
||||||
Submit: Verstuur
|
Submit: Verstuur
|
||||||
|
'The password fields must match.': 'De wachtwoorden moeten overeen komen.'
|
||||||
'There are no answers for this question': 'Er zijn geen antwoorden voor deze vraag'
|
'There are no answers for this question': 'Er zijn geen antwoorden voor deze vraag'
|
||||||
Time: Tijd
|
Time: Tijd
|
||||||
'Your Seasons': 'Jouw seizoenen'
|
'Your Seasons': 'Jouw seizoenen'
|
||||||
|
|||||||
Reference in New Issue
Block a user