Add Sheet upload function

This commit is contained in:
2025-04-28 08:01:27 +02:00
parent 9bae324447
commit 49b7c0f5d5
29 changed files with 1106 additions and 135 deletions

View File

@@ -153,6 +153,12 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/phpunit-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/serializer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/maennchen/zipstream-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/complex" />
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/matrix" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -70,6 +70,64 @@
<option name="migratedIntoUserSpace" value="true" />
</inspection_tool>
<inspection_tool class="PhpCSFixerValidationInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpFullyQualifiedNameUsageInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="IGNORE_GLOBAL_NAMESPACE" value="true" />
</inspection_tool>
<inspection_tool class="PhpStanGlobal" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="SecurityAdvisoriesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="optionConfiguration">
<list>
<option value="barryvdh/laravel-debugbar" />
<option value="behat/behat" />
<option value="brianium/paratest" />
<option value="codeception/codeception" />
<option value="codedungeon/phpunit-result-printer" />
<option value="composer/composer" />
<option value="doctrine/coding-standard" />
<option value="filp/whoops" />
<option value="friendsofphp/php-cs-fixer" />
<option value="humbug/humbug" />
<option value="infection/infection" />
<option value="jakub-onderka/php-parallel-lint" />
<option value="johnkary/phpunit-speedtrap" />
<option value="kalessil/production-dependencies-guard" />
<option value="mikey179/vfsStream" />
<option value="mockery/mockery" />
<option value="mybuilder/phpunit-accelerator" />
<option value="orchestra/testbench" />
<option value="pdepend/pdepend" />
<option value="phan/phan" />
<option value="phing/phing" />
<option value="phpcompatibility/php-compatibility" />
<option value="phpmd/phpmd" />
<option value="phpro/grumphp" />
<option value="phpspec/phpspec" />
<option value="phpspec/prophecy" />
<option value="phpstan/phpstan" />
<option value="phpunit/phpunit" />
<option value="povils/phpmnd" />
<option value="roave/security-advisories" />
<option value="satooshi/php-coveralls" />
<option value="sebastian/phpcpd" />
<option value="slevomat/coding-standard" />
<option value="spatie/phpunit-watcher" />
<option value="squizlabs/php_codesniffer" />
<option value="sstalle/php7cc" />
<option value="symfony/debug" />
<option value="symfony/maker-bundle" />
<option value="symfony/phpunit-bridge" />
<option value="symfony/var-dumper" />
<option value="vimeo/psalm" />
<option value="wimg/php-compatibility" />
<option value="wp-coding-standards/wpcs" />
<option value="yiisoft/yii2-coding-standards" />
<option value="yiisoft/yii2-debug" />
<option value="yiisoft/yii2-gii" />
<option value="zendframework/zend-coding-standard" />
<option value="zendframework/zend-debug" />
<option value="zendframework/zend-test" />
</list>
</option>
</inspection_tool>
</profile>
</component>

18
.idea/php.xml generated
View File

@@ -1,10 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="codingStandard" value="Custom" />
<option name="rulesetPath" value=".php-cs-fixer.dist.php" />
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpCSFixer">
<phpcsfixer_settings>
<phpcs_fixer_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" standards="DoctrineAnnotation;PER;PER-CS;PER-CS1.0;PER-CS2.0;PHP54Migration;PHP56Migration;PHP70Migration;PHP71Migration;PHP73Migration;PHP74Migration;PHP80Migration;PHP81Migration;PHP82Migration;PHP83Migration;PHP84Migration;PHPUnit100Migration;PHPUnit30Migration;PHPUnit32Migration;PHPUnit35Migration;PHPUnit43Migration;PHPUnit48Migration;PHPUnit50Migration;PHPUnit52Migration;PHPUnit54Migration;PHPUnit55Migration;PHPUnit56Migration;PHPUnit57Migration;PHPUnit60Migration;PHPUnit75Migration;PHPUnit84Migration;PHPUnit91Migration;PSR1;PSR12;PSR2;PhpCsFixer;Symfony" tool_path="vendor/bin/php-cs-fixer" timeout="30000" />
@@ -24,7 +31,6 @@
<include_path>
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
<path value="$PROJECT_DIR$/vendor/composer" />
@@ -169,6 +175,13 @@
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
</include_path>
</component>
<component name="PhpInterpreters">
@@ -267,4 +280,7 @@
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

View File

@@ -32,6 +32,7 @@ RUN set -eux; \
opcache \
zip \
uuid \
gd \
;
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser

View File

@@ -14,6 +14,8 @@ exec *args:
shell:
@docker compose exec php bash
bash: shell
migrate: up
docker compose run --rm php bin/console doctrine:migrations:migrate --no-interaction

View File

@@ -15,6 +15,7 @@
"doctrine/orm": "^3.3.2",
"easycorp/easyadmin-bundle": "^4.24.6",
"phpdocumentor/reflection-docblock": "^5.6",
"phpoffice/phpspreadsheet": "*",
"phpstan/phpdoc-parser": "^2.1",
"runtime/frankenphp-symfony": "^0.2.0",
"sentry/sentry-symfony": "^5.2",

554
composer.lock generated
View File

@@ -4,8 +4,87 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b9d67a608b55f14f7d16d1407d13fb3e",
"content-hash": "eeae1ea54653efd8d35df74995953ea0",
"packages": [
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "doctrine/collections",
"version": "2.3.0",
@@ -1471,6 +1550,191 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.2"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^11.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2025-01-27T12:07:53+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
@@ -1646,6 +1910,112 @@
},
"time": "2024-11-09T15:12:26+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "4.2.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "5f6d7410e5fd72cac1aa67d4f05f4fe664d01ba6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/5f6d7410e5fd72cac1aa67d4f05f4fe664d01ba6",
"reference": "5f6d7410e5fd72cac1aa67d4f05f4fe664d01ba6",
"shasum": ""
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^8.1",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1 || ^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/4.2.0"
},
"time": "2025-04-17T02:41:45+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "2.1.0",
@@ -1893,6 +2263,58 @@
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-factory",
"version": "1.1.0",
@@ -2051,6 +2473,57 @@
},
"time": "2024-09-11T13:17:53+00:00"
},
{
"name": "psr/simple-cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/simple-cache.git",
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\SimpleCache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for simple caching",
"keywords": [
"cache",
"caching",
"psr",
"psr-16",
"simple-cache"
],
"support": {
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
"time": "2021-10-29T13:26:27+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
@@ -7354,85 +7827,6 @@
],
"time": "2022-12-23T10:58:28+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "composer/semver",
"version": "3.4.3",

View File

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

View File

@@ -11,6 +11,8 @@ return RectorConfig::configure()
__DIR__.'/src',
__DIR__.'/tests',
])
->withSymfonyContainerXml('var/cache/dev/App_KernelDevDebugContainer.xml')
->withParallel()
->withPhpSets()
->withPreparedSets(
deadCode: true,

View File

@@ -4,19 +4,28 @@ 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\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController]
#[IsGranted('ROLE_USER')]
@@ -26,6 +35,7 @@ final class BackofficeController extends AbstractController
private readonly SeasonRepository $seasonRepository,
private readonly CandidateRepository $candidateRepository,
private readonly Security $security,
private readonly TranslatorInterface $translator,
) {}
#[Route('/backoffice/', name: 'app_backoffice_index')]
@@ -43,6 +53,30 @@ final class BackofficeController extends AbstractController
]);
}
#[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
@@ -72,9 +106,58 @@ final class BackofficeController extends AbstractController
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
#[Route('/backoffice/{seasonCode}/{quiz}/yaml')]
public function testRoute(Season $season, Quiz $quiz, SerializerInterface $serializer): Response
#[Route('/backoffice/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
public function addCandidates(Season $season, Request $request, EntityManagerInterface $em): Response
{
return new Response($serializer->serialize(\App\Resource\Quiz::fromEntity($quiz)->questions, 'yaml', ['yaml_inline' => 100, 'yaml_flags' => 0]), headers: ['Content-Type' => 'text/yaml']);
$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;
}
}

View File

@@ -52,6 +52,7 @@ class KrtekFixtures extends Fixture
->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Vrouw', true))
->addAnswer(new Answer('Man'))
->setOrdering(1)
)
->addQuestion((new Question())
@@ -59,6 +60,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Geen', true))
->addAnswer(new Answer('1'))
->addAnswer(new Answer('2'))
->setOrdering(2)
)
->addQuestion((new Question())
@@ -68,12 +70,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Koningsdag'))
->addAnswer(new Answer('Kerst', true))
->addAnswer(new Answer('Oud en Nieuw'))
->setOrdering(3)
)
->addQuestion((new Question())
->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?')
->addAnswer(new Answer('Met het OV', true))
->addAnswer(new Answer('Met de auto'))
->setOrdering(4)
)
->addQuestion((new Question())
->setQuestion('Met wie keek de Krtek video bij binnenkomst?')
@@ -90,6 +94,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom', true))
->setOrdering(5)
)
->addQuestion((new Question())
@@ -102,6 +107,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Probeer ook eens buiten de lijntjes te kleuren', true))
->addAnswer(new Answer('Ga als je groot bent op groepsreis! '))
->addAnswer(new Answer('Trek minder aan van de mening van anderen, het is oké om anders te zijn.'))
->setOrdering(6)
)
->addQuestion((new Question())
@@ -112,6 +118,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Pantoffels'))
->addAnswer(new Answer('Hakken'))
->addAnswer(new Answer('Geen schoenen, alleen sokken'))
->setOrdering(7)
)
->addQuestion((new Question())
@@ -119,12 +126,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Fiets', true))
->addAnswer(new Answer('Auto'))
->addAnswer(new Answer('Trein'))
->setOrdering(8)
)
->addQuestion((new Question())
->setQuestion('Heeft de Krtek een eigen auto?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
->setOrdering(9)
)
->addQuestion((new Question())
@@ -144,12 +153,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Pieter'))
->addAnswer(new Answer('Renée Fokker'))
->addAnswer(new Answer('Sam, Davy', true))
->setOrdering(10)
)
->addQuestion((new Question())
->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topitos uit iemands rugzak stelen om te kunnen winnen?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
->setOrdering(11)
)
->addQuestion((new Question())
@@ -157,6 +168,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Éénpersoons, losstaand bed'))
->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true))
->addAnswer(new Answer('Tweepersoons bed'))
->setOrdering(12)
)
->addQuestion((new Question())
@@ -165,12 +177,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('6', true))
->addAnswer(new Answer('7'))
->addAnswer(new Answer('8'))
->setOrdering(13)
)
->addQuestion((new Question())
->setQuestion('Waar zat de Krtek aan tafel bij het diner?')
->addAnswer(new Answer('Met de rug naar de accommodatie'))
->addAnswer(new Answer('Met de rug naar de buitenmuur', true))
->setOrdering(14)
)
->addQuestion((new Question())
@@ -188,6 +202,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom'))
->setOrdering(15)
)
;
}
@@ -202,6 +217,7 @@ class KrtekFixtures extends Fixture
->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Man'))
->addAnswer(new Answer('Vrouw', true))
->setOrdering(1)
)
->addQuestion((new Question())
@@ -213,6 +229,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('De Krtek heeft een intolerantie'))
->addAnswer(new Answer('De Krtek eet geen rundvlees'))
->addAnswer(new Answer('De Krtek eet geen waterdieren'))
->setOrdering(2)
)
->addQuestion((new Question())
@@ -224,6 +241,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Tom'))
->addAnswer(new Answer('De huisdieren van de Krtek hebben geen naam'))
->addAnswer(new Answer('De Krtek heeft geen huisdieren', true))
->setOrdering(3)
)
->addQuestion((new Question())
@@ -234,6 +252,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Melk'))
->addAnswer(new Answer('Sap'))
->addAnswer(new Answer('Niks'))
->setOrdering(4)
)
->addQuestion((new Question())
@@ -245,6 +264,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Oostenrijk'))
->addAnswer(new Answer('Turkije'))
->addAnswer(new Answer('Zweden', true))
->setOrdering(5)
)
->addQuestion((new Question())
@@ -254,6 +274,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Het derde groepje'))
->addAnswer(new Answer('Het vierde groepje'))
->addAnswer(new Answer('Het vijfde groepje'))
->setOrdering(6)
)
->addQuestion((new Question())
@@ -262,12 +283,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Het universum', true))
->addAnswer(new Answer('Toeval'))
->addAnswer(new Answer('De Krtek is hindoeïstisch'))
->setOrdering(7)
)
->addQuestion((new Question())
->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?')
->addAnswer(new Answer('Ja', true))
->addAnswer(new Answer('Nee'))
->setOrdering(8)
)
->addQuestion((new Question())
@@ -276,6 +299,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true))
->addAnswer(new Answer('Tussen 2:00 en 2:59 uur'))
->addAnswer(new Answer('Na 3:00'))
->setOrdering(9)
)
->addQuestion((new Question())
@@ -284,6 +308,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('2'))
->addAnswer(new Answer('3'))
->addAnswer(new Answer('geen', true))
->setOrdering(10)
)
->addQuestion((new Question())
@@ -294,6 +319,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Sesamstraat'))
->addAnswer(new Answer('Spongebob Squarepants'))
->addAnswer(new Answer('Teletubbies'))
->setOrdering(11)
)
->addQuestion((new Question())
@@ -301,6 +327,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('In koffer(s)', true))
->addAnswer(new Answer('In losse tas(sen)'))
->addAnswer(new Answer('In een rugzak'))
->setOrdering(12)
)
->addQuestion((new Question())
@@ -313,12 +340,14 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Servies dat tegen elkaar klettert'))
->addAnswer(new Answer('Het geroekoe van een duif', true))
->addAnswer(new Answer('Piepschuim'))
->setOrdering(13)
)
->addQuestion((new Question())
->setQuestion('Wilde de Krtek penningmeester worden?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
->setOrdering(14)
)
->addQuestion((new Question())
@@ -336,6 +365,7 @@ class KrtekFixtures extends Fixture
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom'))
->setOrdering(15)
)
;
}

View File

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

View File

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

View File

@@ -30,6 +30,7 @@ class Quiz
/** @var Collection<int, Question> */
#[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
#[ORM\OrderBy(['ordering' => 'ASC'])]
private Collection $questions;
/** @var Collection<int, Correction> */

View File

@@ -15,6 +15,8 @@ use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: SeasonRepository::class)]
class Season
{
private const string SEASON_CODE_CHARACTERS = 'bcdfghjklmnpqrstvwxz';
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
@@ -148,4 +150,18 @@ class Season
{
return $this->owners->contains($user);
}
public function generateSeasonCode(): self
{
$code = '';
$len = mb_strlen(self::SEASON_CODE_CHARACTERS) - 1;
for ($i = 0; $i < 5; ++$i) {
$code .= self::SEASON_CODE_CHARACTERS[random_int(0, $len)];
}
$this->seasonCode = $code;
return $this;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\Quiz;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<Quiz> */
class UploadQuizFormType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => $this->translator->trans('Quiz name'),
])
->add('sheet', FileType::class, [
'label' => $this->translator->trans('Quiz (xlsx)'),
'mapped' => false,
'required' => true,
'constraints' => [
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
'mimeTypesMessage' => $this->translator->trans('Please upload a valid XLSX file'),
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Quiz::class,
]);
}
}

View File

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

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Answer;
use App\Entity\Question;
use App\Entity\Quiz;
use App\Exception\SpreadsheetDataException;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\File\File;
class QuizSpreadsheetService
{
public function generateTemplate(bool $fillExample = true): \Closure
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getStyle('1:1')->getFont()->setBold(true);
$sheet->setCellValue('A1', 'Question');
$sheet->getColumnDimension('A')->setWidth(30);
$sheet->getStyle('A:A')->getAlignment()->setWrapText(true);
$counter = 1;
foreach (range('B', 'L', 2) as $column) {
$sheet->setCellValue($column.'1', 'Answer '.$counter++);
$sheet->getColumnDimension($column)->setWidth(30);
$sheet->getStyle($column.':'.$column)->getAlignment()->setWrapText(true);
}
foreach (range('C', 'M', 2) as $column) {
$sheet->setCellValue($column.'1', 'Correct');
$sheet->getColumnDimension($column)->setAutoSize(true);
}
if ($fillExample) {
$sheet->setCellValue('B2', 'Man');
$sheet->setCellValue('C2', true);
$sheet->setCellValue('D2', 'Vrouw');
$sheet->setCellValue('E2', false);
$sheet->setCellValue('A2', 'Is de mol een man of een vrouw?');
}
return $this->toXlsx($spreadsheet);
}
/** @throws SpreadsheetDataException */
public function xlsxToQuiz(Quiz $quiz, File $file): Quiz
{
$spreadsheet = $this->readSheet($file);
$sheet = $spreadsheet->getSheet($spreadsheet->getFirstSheetIndex());
$answerLines = \array_slice($sheet->toArray(formatData: false), 1);
return $this->fillQuizFromArray($quiz, $answerLines);
}
private function readSheet(File $file): Spreadsheet
{
return (new \PhpOffice\PhpSpreadsheet\Reader\Xlsx())->setReadDataOnly(true)->load($file->getRealPath());
}
/**
* @param array<int, array<int, string|bool|null>> $sheet
*
* @throws SpreadsheetDataException
*/
private function fillQuizFromArray(Quiz $quiz, array $sheet): Quiz
{
$errors = [];
$questionCounter = 1;
foreach ($sheet as $questionArr) {
if (null === $questionArr[0]) {
break;
}
$question = new Question();
$question->setQuestion((string) $questionArr[0]);
$question->setOrdering($questionCounter++);
$answerCounter = 1;
$arrCounter = 1;
while (true) {
if (null === $questionArr[$arrCounter]) {
if (1 === $answerCounter) {
$errors[] = \sprintf('Question %d has no answers', $answerCounter);
}
break;
}
$answer = new Answer((string) $questionArr[$arrCounter++], (bool) $questionArr[$arrCounter++]);
$answer->setOrdering($answerCounter++);
$question->addAnswer($answer);
}
$quiz->addQuestion($question);
}
if ([] !== $errors) {
throw new SpreadsheetDataException($errors);
}
return $quiz;
}
public function quizToXlsx(Quiz $quiz): void {}
private function toXlsx(Spreadsheet $spreadsheet): \Closure
{
$writer = new Xlsx($spreadsheet);
return static fn () => $writer->save('php://output');
}
}

View File

@@ -3,9 +3,14 @@
{% block title %}Hello BackofficeController!{% endblock %}
{% block body %}
<h2 class="py-2">
<div class="d-flex flex-row align-items-center">
<h2 class="py-2 pe-2">
{{ is_granted('ROLE_ADMIN') ? 'All Seasons'|trans : 'Your Seasons'|trans }}
</h2>
<a class="link" href="{{ path('app_backoffice_season_add') }}">
{{ 'Add'|trans }}
</a>
</div>
<table class="table table-hover">
<thead>
<tr>

View File

@@ -17,6 +17,10 @@
<a class="nav-link{% if 'app_backoffice_index' == app.current_route() %} active{% endif %}"
href="{{ path('app_backoffice_index') }}">{{ 'Seasons'|trans }}</a>
</li>
<li class="nav-item">
<a class="nav-link"
href="{{ path('app_backoffice_template') }}">{{ 'Download Template'|trans }}</a>
</li>
</ul>
<ul class="navbar-nav mb-auto me-2 me-lg-0">
<li class="nav-item">

View File

@@ -0,0 +1,25 @@
{% extends 'backoffice/base.html.twig' %}
{% block body %}
<div class="row">
<div class="col-md-6 col-12">
<h2 class="py-2">{{ 'Add a quiz to '|trans }} {{ season.name }}</h2>
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.sheet) }}
<button type="submit" class="btn btn-primary">{{ 'Submit'|trans }}</button>
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
<p class="pt-5">
Hier kan nog tekst komen met wat uitleg
</p>
</div>
</div>
{% endblock %}
{% block title %}
{% endblock %}

View File

@@ -3,7 +3,11 @@
<h2 class="py-2">{{ 'Season'|trans }}: {{ season.name }}</h2>
<div class="row">
<div class="col-md-6 col-12">
<h4 class="py-2">{{ 'Quizzes'|trans }}</h4>
<div class="d-flex flex-row align-items-center">
<h4 class="py-2 pe-2">{{ 'Quizzes'|trans }}</h4>
<a class="link"
href="{{ path('app_backoffice_quiz_add', {seasonCode: season.seasonCode}) }}">{{ 'Add'|trans }}</a>
</div>
<div class="list-group">
{% for quiz in season.quizzes %}
<a class="list-group-item list-group-item-action{% if season.activeQuiz == quiz %} active{% endif %}"
@@ -14,9 +18,10 @@
</div>
</div>
<div class="col-md-3 col-12">
<div style="display: flex ;flex-direction: row; justify-content: space-between">
<h4 class="py-2">{{ 'Candidates'|trans }}</h4>
<button class="btn btn-outline-primary">{{ 'Add Candidate'|trans }}</button>
<div class="d-flex flex-row align-items-center">
<h4 class="py-2 pe-2">{{ 'Candidates'|trans }}</h4>
<a class="link"
href="{{ path('app_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}</a>
</div>
<ul>

View File

@@ -0,0 +1,22 @@
{% extends 'backoffice/base.html.twig' %}
{% block body %}
<div class="row">
<div class="col-md-6 col-12">
<h2 class="py-2">{{ 'Create a season'|trans }}</h2>
{{ form_start(form) }}
{{ form_row(form.name) }}
<button type="submit" class="btn btn-primary">{{ 'Submit'|trans }}</button>
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
<p class="pt-5">
Hier kan nog tekst komen met wat uitleg
</p>
</div>
</div>
{% endblock %}
{% block title %}
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'backoffice/base.html.twig' %}
{% block body %}
<div class="row">
<div class="col-md-6 col-12">
<h2 class="py-2">{{ 'Add Candidates'|trans }}</h2>
{{ form_start(form) }}
{{ form_row(form.candidates) }}
<button type="submit" class="btn btn-primary">{{ 'Submit'|trans }}</button>
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
<p class="pt-5">
Hier kan nog tekst komen met wat uitleg
</p>
</div>
</div>
{% endblock %}
{% block title %}
{% endblock %}

View File

@@ -1,5 +1,7 @@
'Active Quiz': 'Actieve test'
Add: Toevoegen
'Add Candidate': 'Voeg kandidaat toe'
'Add a quiz to': 'Voeg een test toe aan'
'All Seasons': 'Alle seizoenen'
'Already have an account? Log in': 'Heb je al een account? Log in'
Candidate: Kandidaat
@@ -7,7 +9,9 @@ Candidate: Kandidaat
Candidates: Kandidaten
'Correct Answers': 'Goede antwoorden'
Corrections: Jokers
'Create a season': 'Maak een seizoen aan'
'Create an account': 'Maak een account aan'
'Download Template': 'Download sjabloon'
Email: E-mail
'Enter your name': 'Voor je naam in'
'Invalid season code': 'Ongeldige seizoenscode'
@@ -24,19 +28,25 @@ Owner(s): Eigena(a)r(en)
Password: Wachtwoord
'Please Confirm your Email': messages
'Please sign in': 'Log in aub'
'Please upload a valid XLSX file': 'Upload een geldig XLSX-bestand'
'Prepare Custom Elimination': 'Bereid aangepaste eliminatie voor'
Questions: Vragen
Quiz: Test
'Quiz (xlsx)': 'Test (xlsx)'
'Quiz Added!': 'Test toegevoegd!'
'Quiz completed': 'Test voltooid'
'Quiz name': Testnaam
Quizzes: Tests
Register: Registreren
'Remember me': 'Onthoud mij'
Score: Score
Season: Seizoen
'Season Code': Seizoenscode
'Season Name': 'Seizoensnaam'
Seasons: Seizoenen
'Sign in': 'Log in'
'Start Elimination': 'Start eliminatie'
Submit: Verstuur
'There are no answers for this question': 'Er zijn geen antwoorden voor deze vraag'
Time: Tijd
'Your Seasons': 'Jouw seizoenen'

View File

@@ -0,0 +1,4 @@
Error: Fout
'Please enter a password': 'Voer je wachtwoord in'
'There is already an account with this email': 'Er is al een account met dit e-mailadres'
'Your password should be at least {{ limit }} characters': 'Je wachtwoord moet minimaal {{ limit }} karakters lang zijn'