diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d7c41ed2d..7a66c8486 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -113,7 +113,14 @@ generate-version: before_script: - apk add -q git script: - - VERSION="$(git describe --abbrev=0 --tags)-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}" + - > + VERSION="$(\ + git describe --exact-match --tags HEAD 2> /dev/null\ + || (\ + (git describe --abbrev=0 --tags | tr -d '\n')\ + && echo "-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"\ + )\ + )" - echo "${VERSION}" - echo -n "${VERSION}" > storage/app/VERSION @@ -155,6 +162,15 @@ yarn lint: - apk add --no-cache git - yarn lint +translations lint: + image: alpine + stage: prepare + before_script: + - apk add gettext + script: + - find resources/lang -type f -name '*.po' -exec sh -c 'msgfmt "${1%.*}.po" -o"${1%.*}.mo"' shell {} \; + - '[[ $(find resources/lang -type f -name "*.po" | wc -l) == $(find resources/lang -type f -name "*.mo" | wc -l) ]]' + # # Build # @@ -177,6 +193,7 @@ build-image: - composer validate - yarn check - yarn lint + - translations lint - generate-version dependencies: - generate-version @@ -184,6 +201,7 @@ build-image: - /kaniko/executor --context ${CI_PROJECT_DIR} --dockerfile ${CI_PROJECT_DIR}/docker/Dockerfile --destination "${TEST_IMAGE}" + --cache=true # # Test @@ -241,6 +259,9 @@ dump-database: - cd "${DOCROOT}" - ./bin/migrate script: + - >- + mysql -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" + -e 'UPDATE users SET api_key="" WHERE name="admin"' - >- mysqldump -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" > "${HOMEDIR}/initial-install.sql" @@ -270,7 +291,11 @@ release-image: - test dependencies: [ ] script: - - echo -e "FROM ${TEST_IMAGE}" | /kaniko/executor --dockerfile /dev/stdin --destination "${RELEASE_IMAGE}" + - echo -e "FROM ${TEST_IMAGE}" + | /kaniko/executor + --dockerfile /dev/stdin + --destination "${RELEASE_IMAGE}" + --cache=true only: - main @@ -426,7 +451,8 @@ deploy: GIT_STRATEGY: none when: manual script: - - kubectl delete all,ingress,pvc -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG + - TARGETS=all,ingress,pvc,certificate + - kubectl -n "${KUBE_NAMESPACE}" delete $TARGETS -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG deploy-k8s-review: <<: *deploy_k8s diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f775e381..0d043be70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ * Code must pass PHPStan checks (`composer phpstan`) * Order the composer/npm dependencies alphabetically. * Do not use code from the [includes](includes) directory anywhere else. +* Don't refactor [includes](includes) code just for the sake of change, it is legacy code that must only be replaced. * Please cover your code by unit tests, our goal is to stay at 100% line coverage. Code under `includes` does not require tests as it's mostly not testable and needs to be rewritten. * Do not use vendor prefixes like `-webkit` in styles. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d2a03217f..49f8ed054 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -13,10 +13,14 @@ Please ensure that your pull requests follow the [PSR-12](https://www.php-fig.or You can check that by running ```bash composer run phpcs +# with docker +docker exec engelsystem_dev-es_workspace-1 composer run phpcs ``` You may auto fix reported issues by running ```bash composer run phpcbf +# with docker +docker exec engelsystem_dev-es_workspace-1 composer run phpcbf ``` ## Pre-commit hooks @@ -68,7 +72,7 @@ docker compose exec es_workspace yarn build docker compose exec -e THEMES=0,1 es_workspace yarn build # Update the translation files -docker compose exec es_workspace find /var/www/resources/lang -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \; +docker compose exec es_workspace find /var/www/resources/lang -type f -name '*.po' -exec sh -c 'msgfmt "${1%.*}.po" -o"${1%.*}.mo"' shell {} \; # Run the migrations docker compose exec es_workspace bin/migrate @@ -114,7 +118,7 @@ The following instructions explain how to get, build and run the latest Engelsys ``` * Generate translation files ```bash - find resources/lang/ -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \; + find resources/lang/ -type f -name '*.po' -exec sh -c 'msgfmt "${1%.*}.po" -o"${1%.*}.mo"' shell {} \; ``` ## Testing diff --git a/README.md b/README.md index 82c7cd574..28b0184dc 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The Engelsystem may be installed manually or by using the provided [docker setup * Recommended: Directory Listing should be disabled. * There must be a MySQL database set up with a user who has full rights to that database. * If necessary, create a `config/config.php` to override values from `config/config.default.php`. - * To edit values from the `footer_items`, `themes`, `locales`, `tshirt_sizes` or `headers` lists, directly modify the `config/config.default.php` file or rename it to `config/config.php`. + * To disable/remove values from the `themes`, `tshirt_sizes`, `headers`, `header_items`, `footer_items`, or `locales` lists, set the value of the entry to `null`. * To import the database, the `bin/migrate` script has to be run. If you can't execute scripts, you can use the `initial-install.sql` file from the release zip. * In the browser, login with credentials `admin` : `asdfasdf` and change the password. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..943cc598b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Reporting a Vulnerability + +If you want to contact us directly regarding a security concern, please write an e-mail to contact@engelsystem.de and explain your findings. +Thank you! + +## Use of external reporting / bug bounty services + +We kindly ask you to not use any external reporting / bug bounty service. We do not collaborate with any external service and experiences in the past showed that these services usually add a lot of unnecessary overhead. + +Please send security critical bug reports to contact@engelsystem.de. + +If you feel like we are not reacting fast enough (generally no more than 14 days should go by until an initial response; This is a volunteer project mostly used internally after all), please feel free to go for full disclosure via our github issue tracker, and tag the issue there by creating a title prefixed with [SECURITY]. + +If you find a critical vulnerability that warrants a CVE, we will also take care of issuing a CVE without any bug bounty platform having to be involved. diff --git a/bin/pre-commit b/bin/pre-commit index 8049b2d12..252b92988 100755 --- a/bin/pre-commit +++ b/bin/pre-commit @@ -3,10 +3,24 @@ # immediate exit after an error set -e +testing() { + echo + echo "πŸ”Ž Checking ${1}" +} + +testing 'JS & CSS 🎨' yarn check yarn lint +testing 'PHP βš™οΈ' composer validate composer phpcs composer phpstan ./vendor/bin/phpunit + +testing 'translations πŸ—ΊοΈ' +find resources/lang -type f -name '*.po' -exec sh -c 'msgfmt "${1%.*}.po" -o"${1%.*}.mo"' shell {} \; +[ "$(find resources/lang -type f -name '*.po' | wc -l)" -eq "$(find resources/lang -type f -name '*.mo' | wc -l)" ] +find resources/lang -type f -name '*.mo' -exec rm {} \; + +echo 'βœ… Done πŸŽ‰' diff --git a/composer.json b/composer.json index 0db0769c8..120de0eb2 100644 --- a/composer.json +++ b/composer.json @@ -35,38 +35,40 @@ "ext-pdo": "*", "ext-simplexml": "*", "ext-xml": "*", - "doctrine/dbal": "^3.5", + "doctrine/dbal": "^3.7", "erusev/parsedown": "^1.7", "gettext/gettext": "^5.7", - "gettext/translator": "^1.1", - "guzzlehttp/guzzle": "^7.5", - "illuminate/container": "^9.43", - "illuminate/database": "^9.43", - "illuminate/support": "^9.43", - "league/oauth2-client": "^2.6", + "gettext/translator": "^1.2", + "guzzlehttp/guzzle": "^7.8", + "illuminate/container": "^10.38", + "illuminate/database": "^10.38", + "illuminate/support": "^10.38", + "league/oauth2-client": "^2.7", + "league/openapi-psr7-validator": "^0.21", "nikic/fast-route": "^1.3", - "nyholm/psr7": "^1.5", + "nyholm/psr7": "^1.8", "psr/container": "^2.0", + "psr/http-message": "^1.1", "psr/http-server-middleware": "^1.0", "psr/log": "^3.0", - "rcrowe/twigbridge": "^0.14.0", + "rcrowe/twigbridge": "^0.14.1", "respect/validation": "^1.1", - "symfony/http-foundation": "^6.2", - "symfony/mailer": "^6.2", - "symfony/psr-http-message-bridge": "^2.1", - "twig/twig": "^3.4", - "vlucas/phpdotenv": "^5.5" + "symfony/http-foundation": "^6.4", + "symfony/mailer": "^6.4", + "symfony/psr-http-message-bridge": "^2.3", + "twig/twig": "^3.8", + "vlucas/phpdotenv": "^5.6" }, "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.4", - "fakerphp/faker": "^1.20", + "dms/phpunit-arraysubset-asserts": "^0.5", + "fakerphp/faker": "^1.23", "fig/log-test": "^1.1", - "filp/whoops": "^2.14", - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^9.5", - "slevomat/coding-standard": "^8.6", - "squizlabs/php_codesniffer": "^3.7", - "symfony/var-dumper": "^6.2" + "filp/whoops": "^2.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.14", + "squizlabs/php_codesniffer": "^3.8", + "symfony/var-dumper": "^6.4" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index b3d3d59ee..89a94e0b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,206 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4b1eba8f27aab7aa6bd462a28843fd8c", + "content-hash": "df9efe0dff1f7d311e20747637f1b49f", "packages": [ + { + "name": "brick/math", + "version": "0.11.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "5.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.11.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-01-15T23:15:59+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" + }, + { + "name": "devizzent/cebe-php-openapi", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/DEVizzent/cebe-php-openapi.git", + "reference": "de3a406a1fa5daa275da3f6aed6765c170de7954" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DEVizzent/cebe-php-openapi/zipball/de3a406a1fa5daa275da3f6aed6765c170de7954", + "reference": "de3a406a1fa5daa275da3f6aed6765c170de7954", + "shasum": "" + }, + "require": { + "ext-json": "*", + "justinrainbow/json-schema": "^5.2", + "php": ">=7.1.0", + "symfony/yaml": "^3.4 || ^4 || ^5 || ^6" + }, + "conflict": { + "symfony/yaml": "3.4.0 - 3.4.4 || 4.0.0 - 4.4.17 || 5.0.0 - 5.1.9 || 5.2.0" + }, + "require-dev": { + "apis-guru/openapi-directory": "1.0.0", + "cebe/indent": "*", + "mermade/openapi3-examples": "1.0.0", + "nexmo/api-specification": "1.0.0", + "oai/openapi-specification-3.0": "3.0.3", + "oai/openapi-specification-3.1": "3.1.0", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5 || ^9.4" + }, + "bin": [ + "bin/php-openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "cebe\\openapi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "https://cebe.cc/", + "role": "Creator" + }, + { + "name": "Vicent Valls", + "email": "vizzent@gmail.com" + } + ], + "description": "Read and write OpenAPI yaml/json files and make the content accessable in PHP objects.", + "homepage": "https://github.com/DEVizzent/cebe-php-openapi#readme", + "keywords": [ + "openapi" + ], + "support": { + "issues": "https://github.com/DEVizzent/cebe-php-openapi/issues", + "source": "https://github.com/DEVizzent/cebe-php-openapi" + }, + "time": "2023-10-26T20:41:58+00:00" + }, { "name": "doctrine/cache", "version": "2.2.0", @@ -101,16 +299,16 @@ }, { "name": "doctrine/dbal", - "version": "3.5.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "f38ee8aaca2d58ee88653cb34a6a3880c23f38a5" + "reference": "0ac3c270590e54910715e9a1a044cc368df282b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/f38ee8aaca2d58ee88653cb34a6a3880c23f38a5", - "reference": "f38ee8aaca2d58ee88653cb34a6a3880c23f38a5", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/0ac3c270590e54910715e9a1a044cc368df282b2", + "reference": "0ac3c270590e54910715e9a1a044cc368df282b2", "shasum": "" }, "require": { @@ -123,16 +321,18 @@ "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "10.0.0", - "jetbrains/phpstorm-stubs": "2022.2", - "phpstan/phpstan": "1.8.10", - "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "9.5.25", - "psalm/plugin-phpunit": "0.17.0", - "squizlabs/php_codesniffer": "3.7.1", + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "1.10.42", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "9.6.13", + "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^5.4|^6.0", "symfony/console": "^4.4|^5.4|^6.0", - "vimeo/psalm": "4.29.0" + "vimeo/psalm": "4.30.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -192,7 +392,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.5.1" + "source": "https://github.com/doctrine/dbal/tree/3.7.2" }, "funding": [ { @@ -208,29 +408,33 @@ "type": "tidelift" } ], - "time": "2022-10-24T07:26:18+00:00" + "time": "2023-11-19T08:06:58+00:00" }, { "name": "doctrine/deprecations", - "version": "v1.0.0", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5|^8.5|^9.5", - "psr/log": "^1|^2|^3" + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -249,9 +453,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + "source": "https://github.com/doctrine/deprecations/tree/1.1.2" }, - "time": "2022-05-02T15:47:09+00:00" + "time": "2023-09-27T20:04:15+00:00" }, { "name": "doctrine/event-manager", @@ -346,28 +550,28 @@ }, { "name": "doctrine/inflector", - "version": "2.0.6", + "version": "2.0.8", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024" + "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", - "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff", + "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^10", + "doctrine/coding-standard": "^11.0", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.3", "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25" + "vimeo/psalm": "^4.25 || ^5.4" }, "type": "library", "autoload": { @@ -417,7 +621,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.6" + "source": "https://github.com/doctrine/inflector/tree/2.0.8" }, "funding": [ { @@ -433,35 +637,36 @@ "type": "tidelift" } ], - "time": "2022-10-20T09:10:12+00:00" + "time": "2023-06-16T13:40:37+00:00" }, { "name": "doctrine/lexer", - "version": "1.2.3", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + "reference": "84a527db05647743d50373e0ec53a152f2cde568" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/84a527db05647743d50373e0ec53a152f2cde568", + "reference": "84a527db05647743d50373e0ec53a152f2cde568", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.11" + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^9.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.0" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + "Doctrine\\Common\\Lexer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -493,7 +698,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.3" + "source": "https://github.com/doctrine/lexer/tree/3.0.0" }, "funding": [ { @@ -509,31 +714,30 @@ "type": "tidelift" } ], - "time": "2022-02-28T11:07:21+00:00" + "time": "2022-12-15T16:57:16+00:00" }, { "name": "egulias/email-validator", - "version": "3.2.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715" + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f88dcf4b14af14a98ad96b14b2b317969eab6715", - "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", "shasum": "" }, "require": { - "doctrine/lexer": "^1.2", - "php": ">=7.2", - "symfony/polyfill-intl-idn": "^1.15" + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^8.5.8|^9.3.3", - "vimeo/psalm": "^4" + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -541,7 +745,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { @@ -569,7 +773,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/3.2.1" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" }, "funding": [ { @@ -577,7 +781,7 @@ "type": "github" } ], - "time": "2022-06-18T20:57:19+00:00" + "time": "2023-10-06T06:47:41+00:00" }, { "name": "erusev/parsedown", @@ -779,16 +983,16 @@ }, { "name": "gettext/translator", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/php-gettext/Translator.git", - "reference": "b18ff33e8203de623854561f5e47e992fc5c50bb" + "reference": "a4fa5ed740f304a0ed7b3e169b2b554a195c7570" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Translator/zipball/b18ff33e8203de623854561f5e47e992fc5c50bb", - "reference": "b18ff33e8203de623854561f5e47e992fc5c50bb", + "url": "https://api.github.com/repos/php-gettext/Translator/zipball/a4fa5ed740f304a0ed7b3e169b2b554a195c7570", + "reference": "a4fa5ed740f304a0ed7b3e169b2b554a195c7570", "shasum": "" }, "require": { @@ -833,7 +1037,7 @@ "support": { "email": "oom@oscarotero.com", "issues": "https://github.com/php-gettext/Translator/issues", - "source": "https://github.com/php-gettext/Translator/tree/v1.1.1" + "source": "https://github.com/php-gettext/Translator/tree/v1.2.0" }, "funding": [ { @@ -849,28 +1053,28 @@ "type": "patreon" } ], - "time": "2022-02-23T20:29:40+00:00" + "time": "2023-11-06T15:42:03+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.0", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "a878d45c1914464426dc94da61c9e1d36ae262a8" + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/a878d45c1914464426dc94da61c9e1d36ae262a8", - "reference": "a878d45c1914464426dc94da61c9e1d36ae262a8", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862", + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9" + "phpoption/phpoption": "^1.9.2" }, "require-dev": { - "phpunit/phpunit": "^8.5.28 || ^9.5.21" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "type": "library", "autoload": { @@ -899,7 +1103,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.0" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2" }, "funding": [ { @@ -911,26 +1115,26 @@ "type": "tidelift" } ], - "time": "2022-07-30T15:56:11+00:00" + "time": "2023-11-12T22:16:48+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.5.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba" + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba", - "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.9 || ^2.4", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -939,10 +1143,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -955,9 +1160,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "7.5-dev" } }, "autoload": { @@ -1023,7 +1225,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.5.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" }, "funding": [ { @@ -1039,38 +1241,37 @@ "type": "tidelift" } ], - "time": "2022-08-28T15:39:27+00:00" + "time": "2023-12-03T20:35:24+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.2", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "b94b2807d85443f9719887892882d0329d1e2598" + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", - "reference": "b94b2807d85443f9719887892882d0329d1e2598", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -1107,7 +1308,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.2" + "source": "https://github.com/guzzle/promises/tree/2.0.2" }, "funding": [ { @@ -1123,26 +1324,26 @@ "type": "tidelift" } ], - "time": "2022-08-28T14:55:35+00:00" + "time": "2023-12-03T20:19:20+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.4.3", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "67c26b443f348a51926030c83481b85718457d3d" + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d", - "reference": "67c26b443f348a51926030c83481b85718457d3d", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0" }, "provide": { @@ -1150,9 +1351,9 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1162,9 +1363,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "2.4-dev" } }, "autoload": { @@ -1226,7 +1424,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.3" + "source": "https://github.com/guzzle/psr7/tree/2.6.2" }, "funding": [ { @@ -1242,28 +1440,28 @@ "type": "tidelift" } ], - "time": "2022-10-26T14:07:24+00:00" + "time": "2023-12-03T20:05:35+00:00" }, { "name": "illuminate/bus", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", - "reference": "c7f09872054f2b361f8ed9e9e988b3c9be06c596" + "reference": "8db4b00a3f6071075e9f08094c6c7b059f8deddf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/bus/zipball/c7f09872054f2b361f8ed9e9e988b3c9be06c596", - "reference": "c7f09872054f2b361f8ed9e9e988b3c9be06c596", + "url": "https://api.github.com/repos/illuminate/bus/zipball/8db4b00a3f6071075e9f08094c6c7b059f8deddf", + "reference": "8db4b00a3f6071075e9f08094c6c7b059f8deddf", "shasum": "" }, "require": { - "illuminate/collections": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/pipeline": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2" + "illuminate/collections": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/pipeline": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" }, "suggest": { "illuminate/queue": "Required to use closures when chaining jobs (^7.0)." @@ -1271,7 +1469,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1295,35 +1493,35 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-11-25T07:56:47+00:00" + "time": "2023-12-15T14:09:57+00:00" }, { "name": "illuminate/collections", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "7a8afa0875d7de162f30865d9fae33c8fb235fa2" + "reference": "63fc240a047788fbc2ebe153de85cb72fce88440" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/7a8afa0875d7de162f30865d9fae33c8fb235fa2", - "reference": "7a8afa0875d7de162f30865d9fae33c8fb235fa2", + "url": "https://api.github.com/repos/illuminate/collections/zipball/63fc240a047788fbc2ebe153de85cb72fce88440", + "reference": "63fc240a047788fbc2ebe153de85cb72fce88440", "shasum": "" }, "require": { - "illuminate/conditionable": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/macroable": "^9.0", - "php": "^8.0.2" + "illuminate/conditionable": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "php": "^8.1" }, "suggest": { - "symfony/var-dumper": "Required to use the dump method (^6.0)." + "symfony/var-dumper": "Required to use the dump method (^6.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1350,20 +1548,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-12-02T18:48:05+00:00" + "time": "2023-12-21T14:17:35+00:00" }, { "name": "illuminate/conditionable", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", - "reference": "5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883" + "reference": "d0958e4741fc9d6f516a552060fd1b829a85e009" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883", - "reference": "5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/d0958e4741fc9d6f516a552060fd1b829a85e009", + "reference": "d0958e4741fc9d6f516a552060fd1b829a85e009", "shasum": "" }, "require": { @@ -1372,7 +1570,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1396,25 +1594,25 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-07-29T19:44:19+00:00" + "time": "2023-02-03T08:06:17+00:00" }, { "name": "illuminate/container", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "8ca3036459e26dc7cdedaf0f882b625757cc341e" + "reference": "ddc26273085fad3c471b2602ad820e0097ff7939" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/8ca3036459e26dc7cdedaf0f882b625757cc341e", - "reference": "8ca3036459e26dc7cdedaf0f882b625757cc341e", + "url": "https://api.github.com/repos/illuminate/container/zipball/ddc26273085fad3c471b2602ad820e0097ff7939", + "reference": "ddc26273085fad3c471b2602ad820e0097ff7939", "shasum": "" }, "require": { - "illuminate/contracts": "^9.0", - "php": "^8.0.2", + "illuminate/contracts": "^10.0", + "php": "^8.1", "psr/container": "^1.1.1|^2.0.1" }, "provide": { @@ -1423,7 +1621,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1447,31 +1645,31 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-09-05T15:58:42+00:00" + "time": "2023-06-18T09:12:03+00:00" }, { "name": "illuminate/contracts", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "c7cc6e6198cac6dfdead111f9758de25413188b7" + "reference": "f6bf37a272fda164f6c451407c99f820eb1eb95b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/c7cc6e6198cac6dfdead111f9758de25413188b7", - "reference": "c7cc6e6198cac6dfdead111f9758de25413188b7", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/f6bf37a272fda164f6c451407c99f820eb1eb95b", + "reference": "f6bf37a272fda164f6c451407c99f820eb1eb95b", "shasum": "" }, "require": { - "php": "^8.0.2", + "php": "^8.1", "psr/container": "^1.1.1|^2.0.1", "psr/simple-cache": "^1.0|^2.0|^3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1495,45 +1693,50 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-10-31T22:25:40+00:00" + "time": "2023-10-30T00:59:22+00:00" }, { "name": "illuminate/database", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "9d08866faf75adee93e354315ae68c86915d1541" + "reference": "f014850671278a8d1a0e9fd2f421aad5b487ab9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/9d08866faf75adee93e354315ae68c86915d1541", - "reference": "9d08866faf75adee93e354315ae68c86915d1541", + "url": "https://api.github.com/repos/illuminate/database/zipball/f014850671278a8d1a0e9fd2f421aad5b487ab9e", + "reference": "f014850671278a8d1a0e9fd2f421aad5b487ab9e", "shasum": "" }, "require": { - "ext-json": "*", - "illuminate/collections": "^9.0", - "illuminate/container": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/macroable": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2", - "symfony/console": "^6.0.9" + "brick/math": "^0.9.3|^0.10.2|^0.11", + "ext-pdo": "*", + "illuminate/collections": "^10.0", + "illuminate/container": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" + }, + "conflict": { + "carbonphp/carbon-doctrine-types": ">=3.0", + "doctrine/dbal": ">=4.0" }, "suggest": { - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.13.3|^3.1.4).", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", - "illuminate/console": "Required to use the database commands (^9.0).", - "illuminate/events": "Required to use the observers with Eloquent (^9.0).", - "illuminate/filesystem": "Required to use the migrations (^9.0).", - "illuminate/pagination": "Required to paginate the result set (^9.0).", - "symfony/finder": "Required to use Eloquent model factories (^6.0)." + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.21).", + "illuminate/console": "Required to use the database commands (^10.0).", + "illuminate/events": "Required to use the observers with Eloquent (^10.0).", + "illuminate/filesystem": "Required to use the migrations (^10.0).", + "illuminate/pagination": "Required to paginate the result set (^10.0).", + "symfony/finder": "Required to use Eloquent model factories (^6.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1563,35 +1766,35 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-12-02T15:12:12+00:00" + "time": "2023-12-21T01:49:42+00:00" }, { "name": "illuminate/events", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", - "reference": "8e534676bac23bc17925f5c74c128f9c09b98f69" + "reference": "8d84d6220a6b3446a0bf3e4138e2eb0e10792bb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/8e534676bac23bc17925f5c74c128f9c09b98f69", - "reference": "8e534676bac23bc17925f5c74c128f9c09b98f69", + "url": "https://api.github.com/repos/illuminate/events/zipball/8d84d6220a6b3446a0bf3e4138e2eb0e10792bb1", + "reference": "8d84d6220a6b3446a0bf3e4138e2eb0e10792bb1", "shasum": "" }, "require": { - "illuminate/bus": "^9.0", - "illuminate/collections": "^9.0", - "illuminate/container": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/macroable": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2" + "illuminate/bus": "^10.0", + "illuminate/collections": "^10.0", + "illuminate/container": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1618,48 +1821,53 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-09-15T13:14:12+00:00" + "time": "2023-10-30T00:59:35+00:00" }, { "name": "illuminate/filesystem", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "3e29c07c25e759a99ec09377838e3926e33d9f5f" + "reference": "c765c61cf1308d4f5f3dc3c03ed5f920953b795c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/3e29c07c25e759a99ec09377838e3926e33d9f5f", - "reference": "3e29c07c25e759a99ec09377838e3926e33d9f5f", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/c765c61cf1308d4f5f3dc3c03ed5f920953b795c", + "reference": "c765c61cf1308d4f5f3dc3c03ed5f920953b795c", "shasum": "" }, "require": { - "illuminate/collections": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/macroable": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2", - "symfony/finder": "^6.0" + "illuminate/collections": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1", + "symfony/finder": "^6.2" }, "suggest": { + "ext-fileinfo": "Required to use the Filesystem class.", "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-hash": "Required to use the Filesystem class.", "illuminate/http": "Required for handling uploaded files (^7.0).", "league/flysystem": "Required to use the Flysystem local driver (^3.0.16).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^6.0).", - "symfony/mime": "Required to enable support for guessing extensions (^6.0)." + "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).", + "symfony/mime": "Required to enable support for guessing extensions (^6.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { + "files": [ + "functions.php" + ], "psr-4": { "Illuminate\\Filesystem\\": "" } @@ -1680,29 +1888,29 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-11-17T14:45:11+00:00" + "time": "2023-12-21T15:30:21+00:00" }, { "name": "illuminate/macroable", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", - "reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a" + "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/e3bfaf6401742a9c6abca61b9b10e998e5b6449a", - "reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/dff667a46ac37b634dcf68909d9d41e94dc97c27", + "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27", "shasum": "" }, "require": { - "php": "^8.0.2" + "php": "^8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1726,31 +1934,31 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-08-09T13:29:29+00:00" + "time": "2023-06-05T12:46:42+00:00" }, { "name": "illuminate/pipeline", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", - "reference": "e0be3f3f79f8235ad7334919ca4094d5074e02f6" + "reference": "f802187e917a171332cc90f8c1a102939c57405d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pipeline/zipball/e0be3f3f79f8235ad7334919ca4094d5074e02f6", - "reference": "e0be3f3f79f8235ad7334919ca4094d5074e02f6", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/f802187e917a171332cc90f8c1a102939c57405d", + "reference": "f802187e917a171332cc90f8c1a102939c57405d", "shasum": "" }, "require": { - "illuminate/contracts": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2" + "illuminate/contracts": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1774,50 +1982,51 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-06-09T14:13:53+00:00" + "time": "2023-12-19T14:47:26+00:00" }, { "name": "illuminate/support", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "f3ec55d0f6256cb9da7e13fe758c75b443895226" + "reference": "a9f486d76d5403b0c95b8532cd151a0a960f9565" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/f3ec55d0f6256cb9da7e13fe758c75b443895226", - "reference": "f3ec55d0f6256cb9da7e13fe758c75b443895226", + "url": "https://api.github.com/repos/illuminate/support/zipball/a9f486d76d5403b0c95b8532cd151a0a960f9565", + "reference": "a9f486d76d5403b0c95b8532cd151a0a960f9565", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", - "ext-json": "*", + "ext-ctype": "*", + "ext-filter": "*", "ext-mbstring": "*", - "illuminate/collections": "^9.0", - "illuminate/conditionable": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/macroable": "^9.0", - "nesbot/carbon": "^2.62.1", - "php": "^8.0.2", + "illuminate/collections": "^10.0", + "illuminate/conditionable": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "nesbot/carbon": "^2.67", + "php": "^8.1", "voku/portable-ascii": "^2.0" }, "conflict": { "tightenco/collect": "<5.5.33" }, "suggest": { - "illuminate/filesystem": "Required to use the composer class (^9.0).", + "illuminate/filesystem": "Required to use the composer class (^10.0).", "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).", - "ramsey/uuid": "Required to use Str::uuid() (^4.2.2).", - "symfony/process": "Required to use the composer class (^6.0).", - "symfony/uid": "Required to use Str::ulid() (^6.0).", - "symfony/var-dumper": "Required to use the dd function (^6.0).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the composer class (^6.2).", + "symfony/uid": "Required to use Str::ulid() (^6.2).", + "symfony/var-dumper": "Required to use the dd function (^6.2).", "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1844,37 +2053,37 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-12-05T15:05:31+00:00" + "time": "2023-12-19T15:11:55+00:00" }, { "name": "illuminate/view", - "version": "v9.43.0", + "version": "v10.38.2", "source": { "type": "git", "url": "https://github.com/illuminate/view.git", - "reference": "951a68bbbecaa5f744bfd01e8dcdd482d0604348" + "reference": "9c5c0a363175becb4fcdde2d3df7e665ca537288" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/view/zipball/951a68bbbecaa5f744bfd01e8dcdd482d0604348", - "reference": "951a68bbbecaa5f744bfd01e8dcdd482d0604348", + "url": "https://api.github.com/repos/illuminate/view/zipball/9c5c0a363175becb4fcdde2d3df7e665ca537288", + "reference": "9c5c0a363175becb4fcdde2d3df7e665ca537288", "shasum": "" }, "require": { - "ext-json": "*", - "illuminate/collections": "^9.0", - "illuminate/container": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/events": "^9.0", - "illuminate/filesystem": "^9.0", - "illuminate/macroable": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2" + "ext-tokenizer": "*", + "illuminate/collections": "^10.0", + "illuminate/container": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/events": "^10.0", + "illuminate/filesystem": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.x-dev" + "dev-master": "10.x-dev" } }, "autoload": { @@ -1892,48 +2101,354 @@ "email": "taylor@laravel.com" } ], - "description": "The Illuminate View package.", - "homepage": "https://laravel.com", + "description": "The Illuminate View package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-12-17T15:34:19+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "v5.2.13", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert SchΓΆnthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + }, + "time": "2023-09-26T02:20:38+00:00" + }, + { + "name": "league/oauth2-client", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "paragonie/random_compat": "^1 || ^2 || ^9.99", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", + "squizlabs/php_codesniffer": "^2.3 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0" + }, + "time": "2023-04-16T18:19:15+00:00" + }, + { + "name": "league/openapi-psr7-validator", + "version": "0.21", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/openapi-psr7-validator.git", + "reference": "bccdd3f5037c796fff3ef3f11dcf8c073aaa6192" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/openapi-psr7-validator/zipball/bccdd3f5037c796fff3ef3f11dcf8c073aaa6192", + "reference": "bccdd3f5037c796fff3ef3f11dcf8c073aaa6192", + "shasum": "" + }, + "require": { + "devizzent/cebe-php-openapi": "^1.0", + "ext-json": "*", + "league/uri": "^6.3", + "php": ">=7.2", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/http-message": "^1.0", + "psr/http-server-middleware": "^1.0", + "respect/validation": "^1.1.3 || ^2.0", + "riverline/multipart-parser": "^2.0.3", + "symfony/polyfill-php80": "^1.27", + "webmozart/assert": "^1.4" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "guzzlehttp/psr7": "^1.5", + "hansott/psr7-cookies": "^3.0.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-webmozart-assert": "^1", + "phpunit/phpunit": "^7 || ^8 || ^9", + "symfony/cache": "^5.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OpenAPIValidation\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Validate PSR-7 messages against OpenAPI (3.0.2) specifications expressed in YAML or JSON", + "homepage": "https://github.com/thephpleague/openapi-psr7-validator", + "keywords": [ + "http", + "openapi", + "psr7", + "validation" + ], + "support": { + "issues": "https://github.com/thephpleague/openapi-psr7-validator/issues", + "source": "https://github.com/thephpleague/openapi-psr7-validator/tree/0.21" + }, + "time": "2023-04-03T21:49:07+00:00" + }, + { + "name": "league/uri", + "version": "6.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/a700b4656e4c54371b799ac61e300ab25a2d1d39", + "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/uri-interfaces": "^2.3", + "php": "^8.1", + "psr/http-message": "^1.0.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.9.5", + "nyholm/psr7": "^1.5.1", + "php-http/psr7-integration-tests": "^1.1.1", + "phpbench/phpbench": "^1.2.6", + "phpstan/phpstan": "^1.8.5", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.4.3", + "phpunit/phpunit": "^9.5.24", + "psr/http-factory": "^1.0.1" + }, + "suggest": { + "ext-fileinfo": "Needed to create Data URI from a filepath", + "ext-intl": "Needed to improve host validation", + "league/uri-components": "Needed to easily manipulate URI objects", + "psr/http-factory": "Needed to use the URI factory" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri/issues", + "source": "https://github.com/thephpleague/uri/tree/6.8.0" }, - "time": "2022-11-18T19:51:25+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2022-09-13T19:58:47+00:00" }, { - "name": "league/oauth2-client", - "version": "2.6.1", + "name": "league/uri-interfaces", + "version": "2.3.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/oauth2-client.git", - "reference": "2334c249907190c132364f5dae0287ab8666aa19" + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/2334c249907190c132364f5dae0287ab8666aa19", - "reference": "2334c249907190c132364f5dae0287ab8666aa19", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^6.0 || ^7.0", - "paragonie/random_compat": "^1 || ^2 || ^9.99", - "php": "^5.6 || ^7.0 || ^8.0" + "ext-json": "*", + "php": "^7.2 || ^8.0" }, "require-dev": { - "mockery/mockery": "^1.3.5", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", - "squizlabs/php_codesniffer": "^2.3 || ^3.0" + "friendsofphp/php-cs-fixer": "^2.19", + "phpstan/phpstan": "^0.12.90", + "phpstan/phpstan-phpunit": "^0.12.19", + "phpstan/phpstan-strict-rules": "^0.12.9", + "phpunit/phpunit": "^8.5.15 || ^9.5" + }, + "suggest": { + "ext-intl": "to use the IDNA feature", + "symfony/intl": "to use the IDNA feature via Symfony Polyfill" }, "type": "library", "extra": { "branch-alias": { - "dev-2.x": "2.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "League\\OAuth2\\Client\\": "src/" + "League\\Uri\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1942,58 +2457,60 @@ ], "authors": [ { - "name": "Alex Bilbie", - "email": "hello@alexbilbie.com", - "homepage": "http://www.alexbilbie.com", - "role": "Developer" - }, - { - "name": "Woody Gilk", - "homepage": "https://github.com/shadowhand", - "role": "Contributor" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "OAuth 2.0 Client Library", + "description": "Common interface for URI representation", + "homepage": "http://github.com/thephpleague/uri-interfaces", "keywords": [ - "Authentication", - "SSO", - "authorization", - "identity", - "idp", - "oauth", - "oauth2", - "single sign on" + "rfc3986", + "rfc3987", + "uri", + "url" ], "support": { - "issues": "https://github.com/thephpleague/oauth2-client/issues", - "source": "https://github.com/thephpleague/oauth2-client/tree/2.6.1" + "issues": "https://github.com/thephpleague/uri-interfaces/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/2.3.0" }, - "time": "2021-12-22T16:42:49+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2021-06-28T04:27:21+00:00" }, { "name": "nesbot/carbon", - "version": "2.64.0", + "version": "2.72.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "889546413c97de2d05063b8cb7b193c2531ea211" + "reference": "2b3b3db0a2d0556a177392ff1a3bf5608fa09f78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/889546413c97de2d05063b8cb7b193c2531ea211", - "reference": "889546413c97de2d05063b8cb7b193c2531ea211", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/2b3b3db0a2d0556a177392ff1a3bf5608fa09f78", + "reference": "2b3b3db0a2d0556a177392ff1a3bf5608fa09f78", "shasum": "" }, "require": { + "carbonphp/carbon-doctrine-types": "*", "ext-json": "*", "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", "symfony/polyfill-mbstring": "^1.0", "symfony/polyfill-php80": "^1.16", "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" }, + "provide": { + "psr/clock-implementation": "1.0" + }, "require-dev": { - "doctrine/dbal": "^2.0 || ^3.1.4", - "doctrine/orm": "^2.7", + "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", + "doctrine/orm": "^2.7 || ^3.0", "friendsofphp/php-cs-fixer": "^3.0", "kylekatarnls/multi-tester": "^2.0", "ondrejmirtes/better-reflection": "*", @@ -2070,7 +2587,7 @@ "type": "tidelift" } ], - "time": "2022-11-26T17:36:00+00:00" + "time": "2023-12-08T23:47:49+00:00" }, { "name": "nikic/fast-route", @@ -2124,38 +2641,39 @@ }, { "name": "nyholm/psr7", - "version": "1.5.1", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "f734364e38a876a23be4d906a2a089e1315be18a" + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/f734364e38a876a23be4d906a2a089e1315be18a", - "reference": "f734364e38a876a23be4d906a2a089e1315be18a", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", "shasum": "" }, "require": { - "php": ">=7.1", - "php-http/message-factory": "^1.0", + "php": ">=7.2", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.1 || ^2.0" }, "provide": { + "php-http/message-factory-implementation": "1.0", "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5 || 8.5 || 9.4", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", "symfony/error-handler": "^4.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.8-dev" } }, "autoload": { @@ -2185,7 +2703,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.5.1" + "source": "https://github.com/Nyholm/psr7/tree/1.8.1" }, "funding": [ { @@ -2197,7 +2715,7 @@ "type": "github" } ], - "time": "2022-06-22T07:13:36+00:00" + "time": "2023-11-13T09:31:12+00:00" }, { "name": "paragonie/random_compat", @@ -2249,80 +2767,26 @@ }, "time": "2020-10-15T08:29:30+00:00" }, - { - "name": "php-http/message-factory", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "MΓ‘rk SΓ‘gi-KazΓ‘r", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org", - "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" - ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" - }, - "time": "2015-12-19T14:08:53+00:00" - }, { "name": "phpoption/phpoption", - "version": "1.9.0", + "version": "1.9.2", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab" + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dc5ff11e274a90cc1c743f66c9ad700ce50db9ab", - "reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820", + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "phpunit/phpunit": "^8.5.28 || ^9.5.21" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "type": "library", "extra": { @@ -2364,7 +2828,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.0" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.2" }, "funding": [ { @@ -2376,7 +2840,7 @@ "type": "tidelift" } ], - "time": "2022-07-30T15:51:26+00:00" + "time": "2023-11-12T21:59:55+00:00" }, { "name": "psr/cache", @@ -2427,6 +2891,54 @@ }, "time": "2021-02-03T23:26:27+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -2532,21 +3044,21 @@ }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "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" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -2566,7 +3078,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -2578,27 +3090,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "e616d01114759c4c489f93b099585439f795fe35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", "shasum": "" }, "require": { "php": ">=7.0.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -2618,7 +3130,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interfaces for PSR-7 HTTP message factories", @@ -2633,31 +3145,31 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2023-04-10T20:10:41+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -2686,27 +3198,27 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/http-server-handler", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", "shasum": "" }, "require": { "php": ">=7.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -2726,7 +3238,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP server-side request handler", @@ -2742,28 +3254,27 @@ "server" ], "support": { - "issues": "https://github.com/php-fig/http-server-handler/issues", - "source": "https://github.com/php-fig/http-server-handler/tree/master" + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" }, - "time": "2018-10-30T16:46:14+00:00" + "time": "2023-04-10T20:06:20+00:00" }, { "name": "psr/http-server-middleware", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-server-middleware.git", - "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", - "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", "shasum": "" }, "require": { "php": ">=7.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0" }, "type": "library", @@ -2784,7 +3295,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP server-side middleware", @@ -2800,9 +3311,9 @@ ], "support": { "issues": "https://github.com/php-fig/http-server-middleware/issues", - "source": "https://github.com/php-fig/http-server-middleware/tree/master" + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" }, - "time": "2018-10-30T17:12:04+00:00" + "time": "2023-04-11T06:14:47+00:00" }, { "name": "psr/log", @@ -2951,27 +3462,27 @@ }, { "name": "rcrowe/twigbridge", - "version": "v0.14.0", + "version": "v0.14.1", "source": { "type": "git", "url": "https://github.com/rcrowe/TwigBridge.git", - "reference": "f4968efb99537cc1b37c5bf20280614aadc31825" + "reference": "639345cb32156ff69845ed471bbf0778c52a28b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/f4968efb99537cc1b37c5bf20280614aadc31825", - "reference": "f4968efb99537cc1b37c5bf20280614aadc31825", + "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/639345cb32156ff69845ed471bbf0778c52a28b2", + "reference": "639345cb32156ff69845ed471bbf0778c52a28b2", "shasum": "" }, "require": { - "illuminate/support": "^6|^7|^8|^9", - "illuminate/view": "^6|^7|^8|^9", + "illuminate/support": "^6|^7|^8|^9|^10", + "illuminate/view": "^6|^7|^8|^9|^10", "php": "^7.2.5 || ^8.0", "twig/twig": "~3.0" }, "require-dev": { "ext-json": "*", - "laravel/framework": "^6|^7|^8|^9", + "laravel/framework": "^6|^7|^8|^9|^10", "mockery/mockery": "^1.3.1", "phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.7", "squizlabs/php_codesniffer": "^3.6" @@ -2983,7 +3494,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "0.13-dev" + "dev-master": "0.14-dev" }, "laravel": { "providers": [ @@ -3021,9 +3532,9 @@ ], "support": { "issues": "https://github.com/rcrowe/TwigBridge/issues", - "source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.0" + "source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.1" }, - "time": "2022-03-08T17:21:17+00:00" + "time": "2023-02-16T14:03:23+00:00" }, { "name": "respect/validation", @@ -3093,59 +3604,34 @@ "time": "2019-05-28T06:10:06+00:00" }, { - "name": "symfony/console", - "version": "v6.2.1", + "name": "riverline/multipart-parser", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "58f6cef5dc5f641b7bbdbf8b32b44cc926c35f3f" + "url": "https://github.com/Riverline/multipart-parser.git", + "reference": "2418bdfc2eab01e39bcffee808b1a365c166292a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/58f6cef5dc5f641b7bbdbf8b32b44cc926c35f3f", - "reference": "58f6cef5dc5f641b7bbdbf8b32b44cc926c35f3f", + "url": "https://api.github.com/repos/Riverline/multipart-parser/zipball/2418bdfc2eab01e39bcffee808b1a365c166292a", + "reference": "2418bdfc2eab01e39bcffee808b1a365c166292a", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.4|^6.0" - }, - "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "ext-mbstring": "*", + "php": ">=5.6.0" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/lock": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "laminas/laminas-diactoros": "^1.8.7 || ^2.11.1", + "phpunit/phpunit": "^5.7 || ^9.0", + "psr/http-message": "^1.0", + "symfony/psr-http-message-bridge": "^1.1 || ^2.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Riverline\\MultiPartParser\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3153,53 +3639,38 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Romain Cambien", + "email": "romain@cambien.net" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Riverline", + "homepage": "http://www.riverline.fr" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", + "description": "One class library to parse multipart content with encoding and charset support.", "keywords": [ - "cli", - "command line", - "console", - "terminal" + "http", + "multipart", + "parser" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.1" + "issues": "https://github.com/Riverline/multipart-parser/issues", + "source": "https://github.com/Riverline/multipart-parser/tree/2.1.1" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-12-01T13:44:20+00:00" + "time": "2023-04-28T18:53:59+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -3208,7 +3679,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3237,7 +3708,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -3253,28 +3724,29 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.2.0", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9efb1618fabee89515fe031314e8ed5625f85a53" + "reference": "c459b40ffe67c49af6fd392aac374c9edf8a027e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9efb1618fabee89515fe031314e8ed5625f85a53", - "reference": "9efb1618fabee89515fe031314e8ed5625f85a53", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c459b40ffe67c49af6fd392aac374c9edf8a027e", + "reference": "c459b40ffe67c49af6fd392aac374c9edf8a027e", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/event-dispatcher-contracts": "^2|^3" + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", @@ -3282,17 +3754,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/error-handler": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^5.4|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -3320,7 +3788,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.0" }, "funding": [ { @@ -3336,33 +3804,30 @@ "type": "tidelift" } ], - "time": "2022-11-02T09:08:04+00:00" + "time": "2023-07-27T16:29:09+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.2.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "0782b0b52a737a05b4383d0df35a474303cabdae" + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0782b0b52a737a05b4383d0df35a474303cabdae", - "reference": "0782b0b52a737a05b4383d0df35a474303cabdae", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", "shasum": "" }, "require": { "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3399,7 +3864,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" }, "funding": [ { @@ -3415,27 +3880,27 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/finder", - "version": "v6.2.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "eb2355f69519e4ef33f1835bca4c935f5d42e570" + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/eb2355f69519e4ef33f1835bca4c935f5d42e570", - "reference": "eb2355f69519e4ef33f1835bca4c935f5d42e570", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "symfony/filesystem": "^6.0" + "symfony/filesystem": "^6.0|^7.0" }, "type": "library", "autoload": { @@ -3463,7 +3928,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.2.0" + "source": "https://github.com/symfony/finder/tree/v6.4.0" }, "funding": [ { @@ -3479,41 +3944,40 @@ "type": "tidelift" } ], - "time": "2022-10-09T08:55:40+00:00" + "time": "2023-10-31T17:30:12+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.2.1", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "d0bbd5a7e81b38f32504399b9199f265505b7bac" + "reference": "44a6d39a9cc11e154547d882d5aac1e014440771" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0bbd5a7e81b38f32504399b9199f265505b7bac", - "reference": "d0bbd5a7e81b38f32504399b9199f265505b7bac", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/44a6d39a9cc11e154547d882d5aac1e014440771", + "reference": "44a6d39a9cc11e154547d882d5aac1e014440771", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.1" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" }, "conflict": { - "symfony/cache": "<6.2" + "symfony/cache": "<6.3" }, "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^5.4|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.3|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -3541,7 +4005,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.2.1" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.0" }, "funding": [ { @@ -3557,42 +4021,43 @@ "type": "tidelift" } ], - "time": "2022-12-04T18:26:13+00:00" + "time": "2023-11-20T16:41:16+00:00" }, { "name": "symfony/mailer", - "version": "v6.2.1", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "a18c3dd41cfcf011e3866802e39b9ae9e541deaf" + "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/a18c3dd41cfcf011e3866802e39b9ae9e541deaf", - "reference": "a18c3dd41cfcf011e3866802e39b9ae9e541deaf", + "url": "https://api.github.com/repos/symfony/mailer/zipball/ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba", + "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba", "shasum": "" }, "require": { - "egulias/email-validator": "^2.1.10|^3", + "egulias/email-validator": "^2.1.10|^3|^4", "php": ">=8.1", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/mime": "^6.2", - "symfony/service-contracts": "^1.1|^2|^3" + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { + "symfony/http-client-contracts": "<2.5", "symfony/http-kernel": "<5.4", "symfony/messenger": "<6.2", "symfony/mime": "<6.2", "symfony/twig-bridge": "<6.2.1" }, "require-dev": { - "symfony/console": "^5.4|^6.0", - "symfony/http-client-contracts": "^1.1|^2|^3", - "symfony/messenger": "^6.2", - "symfony/twig-bridge": "^6.2" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" }, "type": "library", "autoload": { @@ -3620,7 +4085,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.2.1" + "source": "https://github.com/symfony/mailer/tree/v6.4.0" }, "funding": [ { @@ -3636,24 +4101,24 @@ "type": "tidelift" } ], - "time": "2022-12-06T16:54:23+00:00" + "time": "2023-11-12T18:02:22+00:00" }, { "name": "symfony/mime", - "version": "v6.2.0", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "1e8005a7cbd79fb824ad81308ef2a76592a08bc0" + "reference": "0a2fff95c1a10df97f571d67e76c7ae0f0d4f535" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/1e8005a7cbd79fb824ad81308ef2a76592a08bc0", - "reference": "1e8005a7cbd79fb824ad81308ef2a76592a08bc0", + "url": "https://api.github.com/repos/symfony/mime/zipball/0a2fff95c1a10df97f571d67e76c7ae0f0d4f535", + "reference": "0a2fff95c1a10df97f571d67e76c7ae0f0d4f535", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -3661,17 +4126,17 @@ "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<5.4", - "symfony/serializer": "<6.2" + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4" }, "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1", + "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/property-access": "^5.4|^6.0", - "symfony/property-info": "^5.4|^6.0", - "symfony/serializer": "^6.2" + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -3703,7 +4168,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.2.0" + "source": "https://github.com/symfony/mime/tree/v7.0.0" }, "funding": [ { @@ -3719,20 +4184,20 @@ "type": "tidelift" } ], - "time": "2022-11-28T12:28:19+00:00" + "time": "2023-10-19T14:20:43+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -3747,7 +4212,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3785,7 +4250,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -3801,24 +4266,26 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "name": "symfony/polyfill-intl-idn", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", + "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" }, "suggest": { "ext-intl": "For best performance" @@ -3826,7 +4293,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3838,7 +4305,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + "Symfony\\Polyfill\\Intl\\Idn\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3847,26 +4314,30 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "grapheme", + "idn", "intl", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0" }, "funding": [ { @@ -3882,26 +4353,24 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:30:37+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.27.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -3909,7 +4378,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3921,8 +4390,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3930,30 +4402,26 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "idn", "intl", + "normalizer", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -3969,32 +4437,35 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { - "ext-intl": "For best performance" + "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4005,12 +4476,9 @@ "files": [ "bootstrap.php" ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4026,18 +4494,17 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "intl", - "normalizer", + "mbstring", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -4053,35 +4520,29 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "name": "symfony/polyfill-php72", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", "shasum": "" }, "require": { "php": ">=7.1" }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4093,7 +4554,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Polyfill\\Php72\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -4110,17 +4571,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" }, "funding": [ { @@ -4136,20 +4596,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.27.0", + "name": "symfony/polyfill-php80", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -4158,7 +4618,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4170,14 +4630,21 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -4187,7 +4654,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4196,7 +4663,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -4212,29 +4679,30 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "name": "symfony/polyfill-php83", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.1", + "symfony/polyfill-php80": "^1.14" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4246,7 +4714,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -4257,10 +4725,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -4270,7 +4734,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4279,7 +4743,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" }, "funding": [ { @@ -4295,36 +4759,37 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-08-16T06:22:46+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v2.1.4", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "a125b93ef378c492e274f217874906fb9babdebb" + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/a125b93ef378c492e274f217874906fb9babdebb", - "reference": "a125b93ef378c492e274f217874906fb9babdebb", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", "shasum": "" }, "require": { - "php": ">=7.1", - "psr/http-message": "^1.0", - "symfony/http-foundation": "^4.4 || ^5.0 || ^6.0" + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/http-foundation": "^5.4 || ^6.0" }, "require-dev": { "nyholm/psr7": "^1.1", "psr/log": "^1.1 || ^2 || ^3", - "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0", - "symfony/config": "^4.4 || ^5.0 || ^6.0", - "symfony/event-dispatcher": "^4.4 || ^5.0 || ^6.0", - "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", - "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", - "symfony/phpunit-bridge": "^5.4@dev || ^6.0" + "symfony/browser-kit": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^6.2" }, "suggest": { "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" @@ -4332,7 +4797,7 @@ "type": "symfony-bridge", "extra": { "branch-alias": { - "dev-main": "2.1-dev" + "dev-main": "2.3-dev" } }, "autoload": { @@ -4367,7 +4832,7 @@ ], "support": { "issues": "https://github.com/symfony/psr-http-message-bridge/issues", - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.1.4" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" }, "funding": [ { @@ -4383,20 +4848,20 @@ "type": "tidelift" } ], - "time": "2022-11-28T22:46:34+00:00" + "time": "2023-07-26T11:53:26+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.1.1", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838", "shasum": "" }, "require": { @@ -4406,13 +4871,10 @@ "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.1-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -4452,7 +4914,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.4.0" }, "funding": [ { @@ -4468,38 +4930,55 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:18:58+00:00" + "time": "2023-07-30T20:28:31+00:00" }, { - "name": "symfony/string", - "version": "v6.2.0", + "name": "symfony/translation", + "version": "v6.4.0", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "145702685e0d12f81d755c71127bfff7582fdd36" + "url": "https://github.com/symfony/translation.git", + "reference": "b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/145702685e0d12f81d755c71127bfff7582fdd36", - "reference": "145702685e0d12f81d755c71127bfff7582fdd36", + "url": "https://api.github.com/repos/symfony/translation/zipball/b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37", + "reference": "b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { - "symfony/translation-contracts": "<2.0" + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/intl": "^6.2", - "symfony/translation-contracts": "^2.0|^3.0", - "symfony/var-exporter": "^5.4|^6.0" + "nikic/php-parser": "^4.13", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4507,7 +4986,7 @@ "Resources/functions.php" ], "psr-4": { - "Symfony\\Component\\String\\": "" + "Symfony\\Component\\Translation\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -4519,26 +4998,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.0" + "source": "https://github.com/symfony/translation/tree/v6.4.0" }, "funding": [ { @@ -4554,69 +5025,41 @@ "type": "tidelift" } ], - "time": "2022-11-30T17:13:47+00:00" + "time": "2023-11-29T08:14:36+00:00" }, { - "name": "symfony/translation", - "version": "v6.2.0", + "name": "symfony/translation-contracts", + "version": "v3.4.0", "source": { "type": "git", - "url": "https://github.com/symfony/translation.git", - "reference": "c08de62caead8357244efcb809d0b1a2584f2198" + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/c08de62caead8357244efcb809d0b1a2584f2198", - "reference": "c08de62caead8357244efcb809d0b1a2584f2198", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/dee0c6e5b4c07ce851b462530088e64b255ac9c5", + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.3|^3.0" - }, - "conflict": { - "symfony/config": "<5.4", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", - "symfony/http-kernel": "<5.4", - "symfony/twig-bundle": "<5.4", - "symfony/yaml": "<5.4" - }, - "provide": { - "symfony/translation-implementation": "2.3|3.0" - }, - "require-dev": { - "nikic/php-parser": "^4.13", - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", - "symfony/http-client-contracts": "^1.1|^2.0|^3.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/intl": "^5.4|^6.0", - "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^5.4|^6.0", - "symfony/service-contracts": "^1.1.2|^2|^3", - "symfony/yaml": "^5.4|^6.0" - }, - "suggest": { - "nikic/php-parser": "To use PhpAstExtractor", - "psr/log-implementation": "To use logging capability in translator", - "symfony/config": "", - "symfony/yaml": "" + "php": ">=8.1" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, "autoload": { - "files": [ - "Resources/functions.php" - ], "psr-4": { - "Symfony\\Component\\Translation\\": "" + "Symfony\\Contracts\\Translation\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4625,18 +5068,26 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to internationalize your application", + "description": "Generic abstractions related to translation", "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "source": "https://github.com/symfony/translation/tree/v6.2.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.0" }, "funding": [ { @@ -4652,44 +5103,43 @@ "type": "tidelift" } ], - "time": "2022-11-02T09:08:04+00:00" + "time": "2023-07-25T15:08:44+00:00" }, { - "name": "symfony/translation-contracts", - "version": "v3.2.0", + "name": "symfony/yaml", + "version": "v6.4.0", "source": { "type": "git", - "url": "https://github.com/symfony/translation-contracts.git", - "reference": "68cce71402305a015f8c1589bfada1280dc64fe7" + "url": "https://github.com/symfony/yaml.git", + "reference": "4f9237a1bb42455d609e6687d2613dde5b41a587" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/68cce71402305a015f8c1589bfada1280dc64fe7", - "reference": "68cce71402305a015f8c1589bfada1280dc64fe7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4f9237a1bb42455d609e6687d2613dde5b41a587", + "reference": "4f9237a1bb42455d609e6687d2613dde5b41a587", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" }, - "suggest": { - "symfony/translation-implementation": "" + "conflict": { + "symfony/console": "<5.4" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.3-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", "autoload": { "psr-4": { - "Symfony\\Contracts\\Translation\\": "" + "Symfony\\Component\\Yaml\\": "" }, "exclude-from-classmap": [ - "/Test/" + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4698,26 +5148,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to translation", + "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/yaml/tree/v6.4.0" }, "funding": [ { @@ -4733,37 +5175,33 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-11-06T11:00:25+00:00" }, { "name": "twig/twig", - "version": "v3.4.3", + "version": "v3.8.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" + "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", "shasum": "" }, "require": { "php": ">=7.2.5", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php80": "^1.22" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "autoload": { "psr-4": { "Twig\\": "src/" @@ -4797,7 +5235,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.4.3" + "source": "https://github.com/twigphp/Twig/tree/v3.8.0" }, "funding": [ { @@ -4809,35 +5247,35 @@ "type": "tidelift" } ], - "time": "2022-09-28T08:42:51+00:00" + "time": "2023-11-21T18:54:41+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.5.0", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.0.2", - "php": "^7.1.3 || ^8.0", - "phpoption/phpoption": "^1.8", - "symfony/polyfill-ctype": "^1.23", - "symfony/polyfill-mbstring": "^1.23.1", - "symfony/polyfill-php80": "^1.23.1" + "graham-campbell/result-type": "^1.1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.2", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-filter": "*", - "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "suggest": { "ext-filter": "Required to use the boolean validator." @@ -4849,7 +5287,7 @@ "forward-command": true }, "branch-alias": { - "dev-master": "5.5-dev" + "dev-master": "5.6-dev" } }, "autoload": { @@ -4881,7 +5319,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0" }, "funding": [ { @@ -4893,7 +5331,7 @@ "type": "tidelift" } ], - "time": "2022-10-16T01:01:54+00:00" + "time": "2023-11-12T22:43:29+00:00" }, { "name": "voku/portable-ascii", @@ -4968,40 +5406,101 @@ } ], "time": "2022-03-08T17:03:00+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" } ], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", + "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5017,7 +5516,7 @@ }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -5041,32 +5540,31 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "dms/phpunit-arraysubset-asserts", - "version": "v0.4.0", + "version": "v0.5.0", "source": { "type": "git", "url": "https://github.com/rdohms/phpunit-arraysubset-asserts.git", - "reference": "428293c2a00eceefbad71a2dbdfb913febb35de2" + "reference": "aa6b9e858414e91cca361cac3b2035ee57d212e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rdohms/phpunit-arraysubset-asserts/zipball/428293c2a00eceefbad71a2dbdfb913febb35de2", - "reference": "428293c2a00eceefbad71a2dbdfb913febb35de2", + "url": "https://api.github.com/repos/rdohms/phpunit-arraysubset-asserts/zipball/aa6b9e858414e91cca361cac3b2035ee57d212e0", + "reference": "aa6b9e858414e91cca361cac3b2035ee57d212e0", "shasum": "" }, "require": { "php": "^5.4 || ^7.0 || ^8.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" }, "require-dev": { - "dms/coding-standard": "^9", - "squizlabs/php_codesniffer": "^3.4" + "dms/coding-standard": "^9" }, "type": "library", "autoload": { @@ -5087,36 +5585,36 @@ "description": "This package provides ArraySubset and related asserts once deprecated in PHPUnit 8", "support": { "issues": "https://github.com/rdohms/phpunit-arraysubset-asserts/issues", - "source": "https://github.com/rdohms/phpunit-arraysubset-asserts/tree/v0.4.0" + "source": "https://github.com/rdohms/phpunit-arraysubset-asserts/tree/v0.5.0" }, - "time": "2022-02-13T15:00:28+00:00" + "time": "2023-06-02T17:33:53+00:00" }, { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -5143,7 +5641,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -5159,20 +5657,20 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "fakerphp/faker", - "version": "v1.21.0", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d" + "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/92efad6a967f0b79c499705c69b662f738cc9e4d", - "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", + "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", "shasum": "" }, "require": { @@ -5225,9 +5723,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.21.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.23.0" }, - "time": "2022-12-13T13:54:32+00:00" + "time": "2023-06-12T08:44:38+00:00" }, { "name": "fig/log-test", @@ -5277,16 +5775,16 @@ }, { "name": "filp/whoops", - "version": "2.14.6", + "version": "2.15.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "f7948baaa0330277c729714910336383286305da" + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/f7948baaa0330277c729714910336383286305da", - "reference": "f7948baaa0330277c729714910336383286305da", + "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", "shasum": "" }, "require": { @@ -5336,7 +5834,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.14.6" + "source": "https://github.com/filp/whoops/tree/2.15.4" }, "funding": [ { @@ -5344,20 +5842,20 @@ "type": "github" } ], - "time": "2022-11-02T16:23:29+00:00" + "time": "2023-11-03T12:00:00+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -5395,7 +5893,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -5403,20 +5901,20 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", - "version": "v4.15.2", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", "shasum": "" }, "require": { @@ -5457,9 +5955,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" }, - "time": "2022-11-12T15:38:23+00:00" + "time": "2023-12-10T21:03:43+00:00" }, { "name": "phar-io/manifest", @@ -5574,22 +6072,24 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.13.1", + "version": "1.24.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "aac44118344d197e6d5f7c6cee91885f0a89acdd" + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/aac44118344d197e6d5f7c6cee91885f0a89acdd", - "reference": "aac44118344d197e6d5f7c6cee91885f0a89acdd", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", @@ -5613,22 +6113,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.13.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" }, - "time": "2022-11-20T08:52:26+00:00" + "time": "2023-12-16T09:33:33+00:00" }, { "name": "phpstan/phpstan", - "version": "1.9.3", + "version": "1.10.50", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "709999b91448d4f2bb07daffffedc889b33e461c" + "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/709999b91448d4f2bb07daffffedc889b33e461c", - "reference": "709999b91448d4f2bb07daffffedc889b33e461c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", + "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", "shasum": "" }, "require": { @@ -5657,8 +6157,11 @@ "static analysis" ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.9.3" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -5674,27 +6177,27 @@ "type": "tidelift" } ], - "time": "2022-12-13T10:28:10+00:00" + "time": "2023-12-13T10:59:42+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.20", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "af7463c955007de36db0c5e26d03e2f933c2e980" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/af7463c955007de36db0c5e26d03e2f933c2e980", - "reference": "af7463c955007de36db0c5e26d03e2f933c2e980", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -5709,8 +6212,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -5743,7 +6246,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.20" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -5751,7 +6255,7 @@ "type": "github" } ], - "time": "2022-12-13T07:49:28+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5996,20 +6500,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.27", + "version": "9.6.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38" + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38", - "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -6020,7 +6524,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-code-coverage": "^9.2.28", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -6038,8 +6542,8 @@ "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -6047,7 +6551,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -6078,7 +6582,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15" }, "funding": [ { @@ -6094,7 +6599,7 @@ "type": "tidelift" } ], - "time": "2022-12-09T07:31:23+00:00" + "time": "2023-12-01T16:55:19+00:00" }, { "name": "sebastian/cli-parser", @@ -6339,20 +6844,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -6384,7 +6889,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -6392,20 +6897,20 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -6450,7 +6955,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -6458,20 +6963,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -6513,7 +7018,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -6521,7 +7026,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -6602,16 +7107,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -6654,7 +7159,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -6662,24 +7167,24 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -6711,7 +7216,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -6719,7 +7224,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -6835,16 +7340,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -6883,10 +7388,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -6894,7 +7399,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -6953,16 +7458,16 @@ }, { "name": "sebastian/type", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { @@ -6997,7 +7502,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -7005,7 +7510,7 @@ "type": "github" } ], - "time": "2022-09-12T14:47:03+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -7062,32 +7567,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.6.4", + "version": "8.14.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "8a02c83e59c3230a2a4367b29956a2f2b56e3a24" + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/8a02c83e59c3230a2a4367b29956a2f2b56e3a24", - "reference": "8a02c83e59c3230a2a4367b29956a2f2b56e3a24", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.11.0 <1.14.0", + "phpstan/phpdoc-parser": "^1.23.1", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.9.2", - "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.2.2", - "phpstan/phpstan-strict-rules": "1.4.4", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.26" + "phpstan/phpstan": "1.10.37", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.14", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "8.5.21|9.6.8|10.3.5" }, "type": "phpcodesniffer-standard", "extra": { @@ -7097,7 +7602,7 @@ }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7111,7 +7616,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.6.4" + "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" }, "funding": [ { @@ -7123,20 +7628,20 @@ "type": "tidelift" } ], - "time": "2022-11-14T09:26:24+00:00" + "time": "2023-10-08T07:28:08+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "version": "3.8.0", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5805f7a4e4958dbb5e944ef1e6edae0a303765e7", + "reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7", "shasum": "" }, "require": { @@ -7146,7 +7651,7 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/phpcs", @@ -7165,56 +7670,77 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2022-06-18T07:21:10+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2023-12-08T12:32:31+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.2.1", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "1e7544c8698627b908657e5276854d52ab70087a" + "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1e7544c8698627b908657e5276854d52ab70087a", - "reference": "1e7544c8698627b908657e5276854d52ab70087a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c40f7d17e91d8b407582ed51a2bbf83c52c367f6", + "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/uid": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", "twig/twig": "^2.13|^3.0.4" }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" - }, "bin": [ "Resources/bin/var-dump-server" ], @@ -7251,7 +7777,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.2.1" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.0" }, "funding": [ { @@ -7267,20 +7793,20 @@ "type": "tidelift" } ], - "time": "2022-12-03T22:32:58+00:00" + "time": "2023-11-09T08:28:32+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -7309,7 +7835,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -7317,7 +7843,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" } ], "aliases": [], @@ -7335,5 +7861,5 @@ "ext-xml": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/app.php b/config/app.php index 2386dbbc8..651790969 100644 --- a/config/app.php +++ b/config/app.php @@ -28,7 +28,6 @@ \Engelsystem\Renderer\TwigServiceProvider::class, \Engelsystem\Middleware\RouteDispatcherServiceProvider::class, \Engelsystem\Middleware\RequestHandlerServiceProvider::class, - \Engelsystem\Middleware\SessionHandlerServiceProvider::class, \Engelsystem\Http\Validation\ValidationServiceProvider::class, \Engelsystem\Http\RedirectServiceProvider::class, @@ -38,6 +37,7 @@ \Engelsystem\Http\HttpClientServiceProvider::class, \Engelsystem\Helpers\DumpServerServiceProvider::class, \Engelsystem\Helpers\UuidServiceProvider::class, + \Engelsystem\Controllers\Api\UsesAuthServiceProvider::class, ], // Application middleware @@ -48,10 +48,13 @@ // Changes of request/response parameters \Engelsystem\Middleware\SetLocale::class, + \Engelsystem\Middleware\ETagHandler::class, \Engelsystem\Middleware\AddHeaders::class, + \Engelsystem\Middleware\TrimInput::class, // The application code \Engelsystem\Middleware\ErrorHandler::class, + \Engelsystem\Middleware\ApiRouteHandler::class, \Engelsystem\Middleware\VerifyCsrfToken::class, \Engelsystem\Middleware\RouteDispatcher::class, \Engelsystem\Middleware\SessionHandler::class, @@ -73,6 +76,7 @@ 'message.created' => \Engelsystem\Events\Listener\Messages::class . '@created', 'news.created' => \Engelsystem\Events\Listener\News::class . '@created', + 'news.updated' => \Engelsystem\Events\Listener\News::class . '@updated', 'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login', @@ -80,5 +84,7 @@ \Engelsystem\Events\Listener\Shift::class . '@deletedEntryCreateWorklog', \Engelsystem\Events\Listener\Shift::class . '@deletedEntrySendEmail', ], + + 'shift.updating' => \Engelsystem\Events\Listener\Shift::class . '@updatedShiftSendEmail', ], ]; diff --git a/config/config.default.php b/config/config.default.php index b39113114..352ab4e2f 100644 --- a/config/config.default.php +++ b/config/config.default.php @@ -13,7 +13,7 @@ 'password' => env('MYSQL_PASSWORD', ''), ], - // For accessing stats + // For accessing /metrics, /stats and custom FSR endpoints (e.g., pretix integration) 'api_key' => env('API_KEY', ''), // Enable maintenance mode (show a static page) @@ -25,19 +25,29 @@ // Set to development to enable debugging messages 'environment' => env('ENVIRONMENT', 'production'), - // Application URL and base path to use instead of the auto detected one + // Application URL and base path to use instead of the auto-detected one 'url' => env('APP_URL', null), // Header links // Available link placeholders: %lang% + // To disable a header_item in the config.php, you can set its value to null 'header_items' => [ - //'Foo' => 'https://foo.bar/batz-%lang%.html', + // Name can be a translation string, permission is a engelsystem privilege + // 'Name' => 'URL', + // 'Name' => ['URL', 'permission'], + + //'Foo' => ['https://foo.bar/batz-%lang%.html', 'logout'], // Permission: for logged-in users ], // Footer links + // To disable a footer item in the config.php, you can set its value to null 'footer_items' => [ + // Name can be a translation string, permission is a engelsystem privilege + // 'Name' => 'URL', + // 'Name' => ['URL', 'permission'], + // URL to the angel faq and job description - 'FAQ' => env('FAQ_URL', '/faq'), + 'faq.faq' => [env('FAQ_URL', '/faq'), 'faq.view'], // Contact email address, linked on every page 'Contact' => env('CONTACT_EMAIL', 'mailto:mail@example.com'), @@ -47,7 +57,7 @@ 'faq_text' => env('FAQ_TEXT', null), // Link to documentation/help - 'documentation_url' => 'https://engelsystem.de/doc/', + 'documentation_url' => env('DOCUMENTATION_URL', 'https://engelsystem.de/doc/'), // Email config 'email' => [ @@ -69,7 +79,7 @@ ], # Your privacy@ contact address - 'privacy_email' => '', + 'privacy_email' => env('PRIVACY_EMAIL', null), // Initial admin password 'setup_admin_password' => env('SETUP_ADMIN_PASSWORD', null), @@ -91,7 +101,7 @@ // User info URL which provides userdata 'url_info' => env('OIDC_USERINFO_URL', ''), // OAuth Scopes - // 'scope' => ['openid'], + 'scope' => ['openid', 'email', 'profile'], // Info unique user id field 'id' => env('OIDC_ID_CLAIM', 'uuid'), // The following fields are used for registration @@ -128,7 +138,19 @@ // Default theme, 1=style1.css 'theme' => env('THEME', 1), + // Supported themes + // To disable a theme in the config.php, you can set its value to null 'themes' => [ + 17 => [ + 'name' => 'Engelsystem 37c3 (2023)', + 'type' => 'dark', + 'navbar_classes' => 'navbar-dark', + ], + 16 => [ + 'name' => 'Engelsystem cccamp23 (2023)', + 'type' => 'dark', + 'navbar_classes' => 'navbar-dark', + ], 15 => [ 'name' => 'Engelsystem rC3 (2021)', 'type' => 'dark', @@ -221,6 +243,16 @@ // Users are able to sign up 'registration_enabled' => (bool) env('REGISTRATION_ENABLED', true), + // Required user fields + 'required_user_fields' => [ + 'pronoun' => (bool) env('PRONOUN_REQUIRED', false), + 'firstname' => (bool) env('FIRSTNAME_REQUIRED', false), + 'lastname' => (bool) env('LASTNAME_REQUIRED', false), + 'tshirt_size' => (bool) env('TSHIRT_SIZE_REQUIRED', true), + 'mobile' => (bool) env('MOBILE_REQUIRED', false), + 'dect' => (bool) env('DECT_REQUIRED', false), + ], + // Only arrived angels can sign up for shifts 'signup_requires_arrival' => (bool) env('SIGNUP_REQUIRES_ARRIVAL', false), @@ -247,7 +279,7 @@ // Define the algorithm to use for `password_verify()` // If the user uses an old algorithm the password will be converted to the new format // See https://secure.php.net/manual/en/password.constants.php for a complete list - 'password_algorithm' => PASSWORD_DEFAULT, + 'password_algorithm' => env('PASSWORD_ALGORITHM', PASSWORD_DEFAULT), // The minimum length for passwords 'min_password_length' => env('PASSWORD_MINIMUM_LENGTH', 8), @@ -263,6 +295,10 @@ // Whether the mobile number can be shown to other users 'enable_mobile_show' => (bool) env('ENABLE_MOBILE_SHOW', false), + // Regular expression describing a FALSE username. + // Per default usernames must only contain alphanumeric chars, "-", "_" or ".". + 'username_regex' => (string) env('USERNAME_REGEX', '/([^\p{L}\p{N}_.-]+)/ui'), + // Enables first name and last name 'enable_user_name' => (bool) env('ENABLE_USER_NAME', false), @@ -311,7 +347,15 @@ 'voucher_start' => env('VOUCHER_START', null) ?: null, ], + # Instruction in accordance with Β§ 43 Para. 1 of the German Infection Protection Act (IfSG) + 'ifsg_enabled' => (bool) env('IFSG_ENABLED', false), + + # Instruction only onsite in accordance with Β§ 43 Para. 1 of the German Infection Protection Act (IfSG) + 'ifsg_light_enabled' => (bool) env('IFSG_LIGHT_ENABLED', false) + && env('IFSG_ENABLED', false), + // Available locales in /resources/lang/ + // To disable a locale in the config.php, you can set its value to null 'locales' => [ 'de_DE' => 'Deutsch', 'en_US' => 'English', @@ -320,21 +364,28 @@ // The default locale to use 'default_locale' => env('DEFAULT_LOCALE', 'en_US'), - // Available T-Shirt sizes, set value to null if not available + // Available T-Shirt sizes + // To disable a t-shirt size in the config.php, you can set its value to null 'tshirt_sizes' => [ 'S' => 'Small Straight-Cut', - 'S-G' => 'Small Fitted-Cut', + 'S-F' => 'Small Fitted-Cut', 'M' => 'Medium Straight-Cut', - 'M-G' => 'Medium Fitted-Cut', + 'M-F' => 'Medium Fitted-Cut', 'L' => 'Large Straight-Cut', - 'L-G' => 'Large Fitted-Cut', + 'L-F' => 'Large Fitted-Cut', 'XL' => 'XLarge Straight-Cut', - 'XL-G' => 'XLarge Fitted-Cut', + 'XL-F' => 'XLarge Fitted-Cut', '2XL' => '2XLarge Straight-Cut', '3XL' => '3XLarge Straight-Cut', '4XL' => '4XLarge Straight-Cut', ], + // Whether to show the current day of the event (-2, -1, 0, 1, 2…) in footer and on the dashboard. + // The event start date has to be set for it to appear. + 'enable_show_day_of_event' => false, + // If true there will be a day 0 (-1, 0, 1…). If false there won't (-1, 1…) + 'event_has_day0' => true, + 'metrics' => [ // User work buckets in seconds 'work' => [1 * 60 * 60, 1.5 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 5 * 60 * 60, 10 * 60 * 60, 20 * 60 * 60], @@ -363,11 +414,16 @@ // Add additional headers 'add_headers' => (bool) env('ADD_HEADERS', true), + // Predefined headers + // To disable a header in the config.php, you can set its value to null 'headers' => [ 'X-Content-Type-Options' => 'nosniff', 'X-Frame-Options' => 'sameorigin', 'Referrer-Policy' => 'strict-origin-when-cross-origin', - 'Content-Security-Policy' => 'default-src \'self\' \'unsafe-inline\' \'unsafe-eval\'; img-src \'self\' data:;', + 'Content-Security-Policy' => + 'default-src \'self\'; ' + . ' style-src \'self\' \'unsafe-inline\'; ' + . 'img-src \'self\' data:;', 'X-XSS-Protection' => '1; mode=block', 'Feature-Policy' => 'autoplay \'none\'', //'Strict-Transport-Security' => 'max-age=7776000', diff --git a/config/routes.php b/config/routes.php index 1aaf52091..767a44c48 100644 --- a/config/routes.php +++ b/config/routes.php @@ -8,6 +8,8 @@ // Pages $route->get('/', 'HomeController@index'); +$route->get('/register', 'RegistrationController@view'); +$route->post('/register', 'RegistrationController@save'); $route->get('/credits', 'CreditsController@index'); $route->get('/health', 'HealthController@index'); @@ -38,7 +40,14 @@ function (RouteCollector $route): void { $route->post('/theme', 'SettingsController@saveTheme'); $route->get('/language', 'SettingsController@language'); $route->post('/language', 'SettingsController@saveLanguage'); + $route->get('/certificates', 'SettingsController@certificate'); + $route->post('/certificates/ifsg', 'SettingsController@saveIfsgCertificate'); + $route->post('/certificates/driving', 'SettingsController@saveDrivingLicense'); + $route->get('/api', 'SettingsController@api'); + $route->post('/api', 'SettingsController@apiKeyReset'); $route->get('/oauth', 'SettingsController@oauth'); + $route->get('/sessions', 'SettingsController@sessions'); + $route->post('/sessions', 'SettingsController@sessionsDelete'); } ); @@ -62,6 +71,11 @@ function (RouteCollector $route): void { $route->get('/about', 'AngelTypesController@about'); }); +// Shifts +$route->addGroup('/shifts', function (RouteCollector $route): void { + $route->get('/random', 'ShiftsController@random'); +}); + // News $route->get('/meetings', 'NewsController@meetings'); $route->addGroup( @@ -101,8 +115,46 @@ function (RouteCollector $route): void { ); // API -$route->get('/api/usershifts[/{email:.+}]', 'ApiController@usershifts'); -$route->get('/api[/{resource:.+}]', 'ApiController@index'); +$route->addGroup( + '/api', + function (RouteCollector $route): void { + $route->get('', 'Api\IndexController@index'); + + $route->addGroup( + '/v0-beta', + function (RouteCollector $route): void { + $route->addRoute(['OPTIONS'], '[/{resource:.+}]', 'Api\IndexController@options'); + $route->get('', 'Api\IndexController@indexV0'); + $route->get('/openapi', 'Api\IndexController@openApiV0'); + $route->get('/info', 'Api\IndexController@info'); + + $route->get('/angeltypes', 'Api\AngelTypeController@index'); + $route->get('/angeltypes/{angeltype_id:\d+}/shifts', 'Api\ShiftsController@entriesByAngeltype'); + + $route->get('/locations', 'Api\LocationsController@index'); + $route->get('/locations/{location_id:\d+}/shifts', 'Api\ShiftsController@entriesByLocation'); + + $route->get('/news', 'Api\NewsController@index'); + + $route->get('/users/{user_id:(?:\d+|self)}', 'Api\UsersController@user'); + $route->get('/users/{user_id:(?:\d+|self)}/angeltypes', 'Api\AngelTypeController@ofUser'); + $route->get('/users/{user_id:(?:\d+|self)}/shifts', 'Api\ShiftsController@entriesByUser'); + + $route->addRoute( + ['POST', 'PUT', 'DELETE', 'PATCH'], + '/[{resource:.+}]', + 'Api\IndexController@notImplemented' + ); + $route->get('/[{resource:.+}]', 'Api\IndexController@notFound'); + } + ); + + // Routes for custom FSR requests (e.g., pretix integration) + $route->get('/usershifts[/{email:.+}]', 'Api\FsrController@usershifts'); + + $route->get('/[{resource:.+}]', 'Api\IndexController@notFound'); + } +); // Feeds $route->get('/atom', 'FeedController@atom'); @@ -147,6 +199,27 @@ function (RouteCollector $route): void { } ); + // Shifts + $route->addGroup( + '/shifts', + function (RouteCollector $route): void { + $route->get('/history', 'Admin\\ShiftsController@history'); + $route->post('/history', 'Admin\\ShiftsController@deleteTransaction'); + } + ); + + // Shift types + $route->addGroup( + '/shifttypes', + function (RouteCollector $route): void { + $route->get('', 'Admin\\ShiftTypesController@index'); + $route->post('', 'Admin\\ShiftTypesController@delete'); + $route->get('/{shift_type_id:\d+}', 'Admin\\ShiftTypesController@view'); + $route->get('/edit[/{shift_type_id:\d+}]', 'Admin\\ShiftTypesController@edit'); + $route->post('/edit[/{shift_type_id:\d+}]', 'Admin\\ShiftTypesController@save'); + } + ); + // Questions $route->addGroup( '/questions', @@ -158,14 +231,14 @@ function (RouteCollector $route): void { } ); - // Rooms + // Locations $route->addGroup( - '/rooms', + '/locations', function (RouteCollector $route): void { - $route->get('', 'Admin\\RoomsController@index'); - $route->post('', 'Admin\\RoomsController@delete'); - $route->get('/edit[/{room_id:\d+}]', 'Admin\\RoomsController@edit'); - $route->post('/edit[/{room_id:\d+}]', 'Admin\\RoomsController@save'); + $route->get('', 'Admin\\LocationsController@index'); + $route->post('', 'Admin\\LocationsController@delete'); + $route->get('/edit[/{location_id:\d+}]', 'Admin\\LocationsController@edit'); + $route->post('/edit[/{location_id:\d+}]', 'Admin\\LocationsController@save'); } ); diff --git a/db/factories/AngelTypeFactory.php b/db/factories/AngelTypeFactory.php index 139441574..9a8e6f42b 100644 --- a/db/factories/AngelTypeFactory.php +++ b/db/factories/AngelTypeFactory.php @@ -24,9 +24,11 @@ public function definition(): array 'restricted' => $this->faker->boolean(), 'requires_driver_license' => $this->faker->boolean(), - 'no_self_signup' => $this->faker->boolean(), + 'requires_ifsg_certificate' => $this->faker->boolean(), + 'shift_self_signup' => $this->faker->boolean(), 'show_on_dashboard' => $this->faker->boolean(), 'hide_register' => $this->faker->boolean(), + 'hide_on_shift_view' => $this->faker->boolean(), ]; } } diff --git a/db/factories/RoomFactory.php b/db/factories/LocationFactory.php similarity index 66% rename from db/factories/RoomFactory.php rename to db/factories/LocationFactory.php index 0a06959b4..5dd37e92b 100644 --- a/db/factories/RoomFactory.php +++ b/db/factories/LocationFactory.php @@ -4,13 +4,13 @@ namespace Database\Factories\Engelsystem\Models; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Illuminate\Database\Eloquent\Factories\Factory; -class RoomFactory extends Factory +class LocationFactory extends Factory { /** @var string */ - protected $model = Room::class; // phpcs:ignore + protected $model = Location::class; // phpcs:ignore public function definition(): array { @@ -18,6 +18,7 @@ public function definition(): array 'name' => $this->faker->unique()->firstName(), 'map_url' => $this->faker->url(), 'description' => $this->faker->text(), + 'dect' => $this->faker->optional()->numberBetween(1000, 9999), ]; } } diff --git a/db/factories/NewsFactory.php b/db/factories/NewsFactory.php index 54b39d36a..f2af8ccfd 100644 --- a/db/factories/NewsFactory.php +++ b/db/factories/NewsFactory.php @@ -16,12 +16,12 @@ class NewsFactory extends Factory public function definition(): array { return [ - 'title' => $this->faker->text(50), - 'text' => $this->faker->realText(), - 'is_meeting' => $this->faker->boolean(), - 'is_pinned' => $this->faker->boolean(.1), - 'is_important' => $this->faker->boolean(.1), - 'user_id' => User::factory(), + 'title' => $this->faker->text(50), + 'text' => $this->faker->realText(), + 'is_meeting' => $this->faker->boolean(), + 'is_pinned' => $this->faker->boolean(.1), + 'is_highlighted' => $this->faker->boolean(.1), + 'user_id' => User::factory(), ]; } } diff --git a/db/factories/OAuthFactory.php b/db/factories/OAuthFactory.php new file mode 100644 index 000000000..0e377aa47 --- /dev/null +++ b/db/factories/OAuthFactory.php @@ -0,0 +1,30 @@ + + */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'provider' => $this->faker->unique()->word(), + 'identifier' => $this->faker->unique()->word(), + 'access_token' => $this->faker->unique()->word(), + 'refresh_token' => $this->faker->unique()->word(), + 'expires_at' => $this->faker->dateTimeInInterval('+5 days', '+3 months')->format('Y-m-d'), + ]; + } +} diff --git a/db/factories/SessionFactory.php b/db/factories/SessionFactory.php new file mode 100644 index 000000000..1f455140f --- /dev/null +++ b/db/factories/SessionFactory.php @@ -0,0 +1,24 @@ + $this->faker->lexify('????????????????????????????????'), + 'payload' => $this->faker->text(100), + 'user_id' => $this->faker->optional()->passthrough(User::factory()), + ]; + } +} diff --git a/db/factories/Shifts/NeededAngelTypeFactory.php b/db/factories/Shifts/NeededAngelTypeFactory.php index ee07e534e..7698068e4 100644 --- a/db/factories/Shifts/NeededAngelTypeFactory.php +++ b/db/factories/Shifts/NeededAngelTypeFactory.php @@ -5,7 +5,7 @@ namespace Database\Factories\Engelsystem\Models\Shifts; use Engelsystem\Models\AngelType; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Shift; use Illuminate\Database\Eloquent\Factories\Factory; @@ -17,11 +17,12 @@ class NeededAngelTypeFactory extends Factory public function definition(): array { - $forRoom = $this->faker->boolean(); + $type = $this->faker->numberBetween(0, 2); return [ - 'room_id' => $forRoom ? Room::factory() : null, - 'shift_id' => $forRoom ? null : Shift::factory(), + 'location_id' => $type == 0 ? Location::factory() : null, + 'shift_id' => $type == 1 ? null : Shift::factory(), + 'shift_type_id' => $type == 2 ? null : Shift::factory(), 'angel_type_id' => AngelType::factory(), 'count' => $this->faker->numberBetween(1, 5), ]; diff --git a/db/factories/Shifts/ScheduleFactory.php b/db/factories/Shifts/ScheduleFactory.php index 42a323af1..350d0be36 100644 --- a/db/factories/Shifts/ScheduleFactory.php +++ b/db/factories/Shifts/ScheduleFactory.php @@ -18,6 +18,7 @@ public function definition(): array 'name' => $this->faker->unique()->words(4, true), 'url' => $this->faker->parse('https://{{safeEmailDomain}}/{{slug}}.xml'), 'shift_type' => $this->faker->numberBetween(1, 5), + 'needed_from_shift_type' => $this->faker->boolean(.2), 'minutes_before' => 15, 'minutes_after' => 15, ]; diff --git a/db/factories/Shifts/ShiftFactory.php b/db/factories/Shifts/ShiftFactory.php index 6bf2f7966..8364ce731 100644 --- a/db/factories/Shifts/ShiftFactory.php +++ b/db/factories/Shifts/ShiftFactory.php @@ -4,7 +4,7 @@ namespace Database\Factories\Engelsystem\Models\Shifts; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\User\User; @@ -25,7 +25,7 @@ public function definition(): array 'start' => $start, 'end' => $this->faker->dateTimeInInterval($start, '+3 hours'), 'shift_type_id' => ShiftType::factory(), - 'room_id' => Room::factory(), + 'location_id' => Location::factory(), 'transaction_id' => $this->faker->optional()->uuid(), 'created_by' => User::factory(), 'updated_by' => $this->faker->optional(.3)->boolean() ? User::factory() : null, diff --git a/db/factories/User/ContactFactory.php b/db/factories/User/ContactFactory.php index dc26e60d8..360a9279e 100644 --- a/db/factories/User/ContactFactory.php +++ b/db/factories/User/ContactFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories\Engelsystem\Models\User; use Engelsystem\Models\User\Contact; +use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Factories\Factory; class ContactFactory extends Factory @@ -15,6 +16,7 @@ class ContactFactory extends Factory public function definition(): array { return [ + 'user_id' => User::factory(), 'dect' => $this->faker->optional()->numberBetween(1000, 9999), 'email' => $this->faker->unique()->optional()->safeEmail(), 'mobile' => $this->faker->optional(.2)->phoneNumber(), diff --git a/db/factories/User/LicenseFactory.php b/db/factories/User/LicenseFactory.php index 4c9ccecf4..5240c9712 100644 --- a/db/factories/User/LicenseFactory.php +++ b/db/factories/User/LicenseFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories\Engelsystem\Models\User; use Engelsystem\Models\User\License; +use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Factories\Factory; class LicenseFactory extends Factory @@ -21,13 +22,19 @@ public function definition(): array $drive_forklift = ($drive_car && $this->faker->boolean(.1)) || ($drive_12t && $this->faker->boolean(.7)); + $ifsg_certificate = $this->faker->boolean(0.1); + $ifsg_certificate_light = $this->faker->boolean(0.5) && !$ifsg_certificate; + return [ - 'has_car' => $drive_car && $this->faker->boolean(.7), - 'drive_forklift' => $drive_forklift, - 'drive_car' => $drive_car, - 'drive_3_5t' => $drive_3_5t, - 'drive_7_5t' => $drive_7_5t, - 'drive_12t' => $drive_12t, + 'user_id' => User::factory(), + 'has_car' => $drive_car && $this->faker->boolean(.7), + 'drive_forklift' => $drive_forklift, + 'drive_car' => $drive_car, + 'drive_3_5t' => $drive_3_5t, + 'drive_7_5t' => $drive_7_5t, + 'drive_12t' => $drive_12t, + 'ifsg_certificate' => $ifsg_certificate, + 'ifsg_certificate_light' => $ifsg_certificate_light, ]; } } diff --git a/db/factories/User/PasswordResetFactory.php b/db/factories/User/PasswordResetFactory.php index 69209a270..c0169568e 100644 --- a/db/factories/User/PasswordResetFactory.php +++ b/db/factories/User/PasswordResetFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories\Engelsystem\Models\User; use Engelsystem\Models\User\PasswordReset; +use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Factories\Factory; class PasswordResetFactory extends Factory @@ -15,6 +16,7 @@ class PasswordResetFactory extends Factory public function definition(): array { return [ + 'user_id' => User::factory(), 'token' => bin2hex(random_bytes(16)), ]; } diff --git a/db/factories/User/PersonalDataFactory.php b/db/factories/User/PersonalDataFactory.php index e69c7e44a..e47ee9fd2 100644 --- a/db/factories/User/PersonalDataFactory.php +++ b/db/factories/User/PersonalDataFactory.php @@ -6,6 +6,7 @@ use Carbon\Carbon; use Engelsystem\Models\User\PersonalData; +use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Factories\Factory; class PersonalDataFactory extends Factory @@ -19,6 +20,7 @@ public function definition(): array $departure = $this->faker->optional()->dateTimeThisMonth('2 weeks'); return [ + 'user_id' => User::factory(), 'first_name' => $this->faker->optional(.7)->firstName(), 'last_name' => $this->faker->optional()->lastName(), 'pronoun' => $this->faker->optional(.3)->pronoun(), diff --git a/db/factories/User/SettingsFactory.php b/db/factories/User/SettingsFactory.php index f399ed319..b5fb7f4f0 100644 --- a/db/factories/User/SettingsFactory.php +++ b/db/factories/User/SettingsFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories\Engelsystem\Models\User; use Engelsystem\Models\User\Settings; +use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Factories\Factory; class SettingsFactory extends Factory @@ -15,6 +16,7 @@ class SettingsFactory extends Factory public function definition(): array { return [ + 'user_id' => User::factory(), 'language' => $this->faker->locale(), 'theme' => $this->faker->numberBetween(1, 20), 'email_human' => $this->faker->boolean(), diff --git a/db/factories/User/StateFactory.php b/db/factories/User/StateFactory.php index 4f9a04e6d..277807779 100644 --- a/db/factories/User/StateFactory.php +++ b/db/factories/User/StateFactory.php @@ -6,6 +6,7 @@ use Carbon\Carbon; use Engelsystem\Models\User\State; +use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Factories\Factory; class StateFactory extends Factory @@ -18,8 +19,10 @@ public function definition(): array $arrival = $this->faker->optional()->dateTimeThisMonth(); return [ + 'user_id' => User::factory(), 'arrived' => (bool) $arrival, 'arrival_date' => $arrival ? Carbon::instance($arrival) : null, + 'user_info' => $this->faker->optional(.1)->text(), 'active' => $this->faker->boolean(.3), 'force_active' => $this->faker->boolean(.1), 'got_shirt' => $this->faker->boolean(), diff --git a/db/factories/User/UserFactory.php b/db/factories/User/UserFactory.php index ce37e7023..545931fca 100644 --- a/db/factories/User/UserFactory.php +++ b/db/factories/User/UserFactory.php @@ -19,6 +19,7 @@ public function definition(): array 'password' => crypt(random_bytes(16), '$1$salt$'), 'email' => $this->faker->unique()->safeEmail(), 'api_key' => bin2hex(random_bytes(32)), + 'updated_at' => $this->faker->dateTimeInInterval('-3 months', 'now'), ]; } } diff --git a/db/install.sql b/db/install.sql deleted file mode 100644 index e1c9d5798..000000000 --- a/db/install.sql +++ /dev/null @@ -1,802 +0,0 @@ --- phpMyAdmin SQL Dump --- version 4.5.2 --- http://www.phpmyadmin.net --- --- Host: localhost --- Erstellungszeit: 27. Sep 2016 um 17:48 --- Server-Version: 10.1.10-MariaDB --- PHP-Version: 7.0.4 - -SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; -SET time_zone = "+00:00"; - --- --- Datenbank: `engelsystem` --- - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `AngelTypes` --- - -DROP TABLE IF EXISTS `AngelTypes`; -CREATE TABLE `AngelTypes` ( - `id` int(11) NOT NULL, - `name` varchar(50) NOT NULL DEFAULT '', - `restricted` int(1) NOT NULL, - `description` text NOT NULL, - `requires_driver_license` tinyint(1) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `EventConfig` --- - -DROP TABLE IF EXISTS `EventConfig`; -CREATE TABLE `EventConfig` ( - `event_name` varchar(255) DEFAULT NULL, - `buildup_start_date` int(11) DEFAULT NULL, - `event_start_date` int(11) DEFAULT NULL, - `event_end_date` int(11) DEFAULT NULL, - `teardown_end_date` int(11) DEFAULT NULL, - `event_welcome_msg` varchar(255) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `GroupPrivileges` --- - -DROP TABLE IF EXISTS `GroupPrivileges`; -CREATE TABLE `GroupPrivileges` ( - `id` int(11) NOT NULL, - `group_id` int(11) NOT NULL, - `privilege_id` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `GroupPrivileges` --- - -INSERT INTO `GroupPrivileges` (`id`, `group_id`, `privilege_id`) VALUES -(85, -7, 10), -(87, -7, 18), -(86, -7, 21), -(216, -6, 5), -(212, -6, 6), -(207, -6, 7), -(211, -6, 12), -(208, -6, 13), -(210, -6, 14), -(214, -6, 16), -(209, -6, 21), -(213, -6, 28), -(206, -6, 31), -(215, -6, 33), -(257, -6, 38), -(219, -5, 14), -(221, -5, 25), -(220, -5, 33), -(241, -4, 5), -(238, -4, 14), -(240, -4, 16), -(237, -4, 19), -(242, -4, 25), -(235, -4, 27), -(239, -4, 28), -(236, -4, 32), -(218, -4, 39), -(258, -3, 31), -(247, -2, 3), -(246, -2, 4), -(255, -2, 8), -(252, -2, 9), -(254, -2, 11), -(248, -2, 15), -(251, -2, 17), -(256, -2, 24), -(253, -2, 26), -(245, -2, 30), -(244, -2, 34), -(249, -2, 35), -(243, -2, 36), -(250, -2, 37), -(88, -1, 1), -(23, -1, 2), -(24, -1, 5); - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `Groups` --- - -DROP TABLE IF EXISTS `Groups`; -CREATE TABLE `Groups` ( - `Name` varchar(35) NOT NULL, - `UID` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `Groups` --- - -INSERT INTO `Groups` (`Name`, `UID`) VALUES -('6-Developer', -7), -('5-BΓΌrokrat', -6), -('4-Team Coordinator', -5), -('3-Shift Coordinator', -4), -('Shirt-Manager', -3), -('2-Engel', -2), -('1-Gast', -1); - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `LogEntries` --- - -DROP TABLE IF EXISTS `LogEntries`; -CREATE TABLE `LogEntries` ( - `id` int(11) NOT NULL, - `timestamp` int(11) NOT NULL, - `nick` text NOT NULL, - `message` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `Messages` --- - -DROP TABLE IF EXISTS `Messages`; -CREATE TABLE `Messages` ( - `id` int(11) NOT NULL, - `Datum` int(11) NOT NULL, - `SUID` int(11) NOT NULL DEFAULT '0', - `RUID` int(11) NOT NULL DEFAULT '0', - `isRead` char(1) NOT NULL DEFAULT 'N', - `Text` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Fuers interen Communikationssystem'; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `NeededAngelTypes` --- - -DROP TABLE IF EXISTS `NeededAngelTypes`; -CREATE TABLE `NeededAngelTypes` ( - `id` int(11) NOT NULL, - `room_id` int(11) DEFAULT NULL, - `shift_id` int(11) DEFAULT NULL, - `angel_type_id` int(11) NOT NULL, - `count` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `News` --- - -DROP TABLE IF EXISTS `News`; -CREATE TABLE `News` ( - `ID` int(11) NOT NULL, - `Datum` int(11) NOT NULL, - `Betreff` varchar(150) NOT NULL DEFAULT '', - `Text` text NOT NULL, - `UID` int(11) NOT NULL DEFAULT '0', - `Treffen` tinyint(4) NOT NULL DEFAULT '0' -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `NewsComments` --- - -DROP TABLE IF EXISTS `NewsComments`; -CREATE TABLE `NewsComments` ( - `ID` bigint(11) NOT NULL, - `Refid` int(11) NOT NULL DEFAULT '0', - `Datum` datetime NOT NULL DEFAULT '0001-01-01 00:00:00', - `Text` text NOT NULL, - `UID` int(11) NOT NULL DEFAULT '0' -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `Privileges` --- - -DROP TABLE IF EXISTS `Privileges`; -CREATE TABLE `Privileges` ( - `id` int(11) NOT NULL, - `name` varchar(128) NOT NULL, - `desc` varchar(1024) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `Privileges` --- - -INSERT INTO `Privileges` (`id`, `name`, `desc`) VALUES -(1, 'start', 'Startseite fΓΌr GΓ€ste/Nicht eingeloggte User'), -(2, 'login', 'Logindialog'), -(3, 'news', 'Anzeigen der News-Seite'), -(4, 'logout', 'User darf sich ausloggen'), -(5, 'register', 'Einen neuen Engel registerieren'), -(6, 'admin_rooms', 'RΓ€ume administrieren'), -(7, 'admin_angel_types', 'Engel Typen administrieren'), -(8, 'user_settings', 'User profile settings'), -(9, 'user_messages', 'Writing and reading messages from user to user'), -(10, 'admin_groups', 'Manage usergroups and their rights'), -(11, 'user_questions', 'Let users ask questions'), -(12, 'admin_questions', 'Answer user''s questions'), -(13, 'admin_faq', 'Edit FAQs'), -(14, 'admin_news', 'Administrate the news section'), -(15, 'news_comments', 'User can comment news'), -(16, 'admin_user', 'Administrate the angels'), -(17, 'user_meetings', 'Lists meetings (news)'), -(18, 'admin_language', 'Translate the system'), -(19, 'admin_log', 'Display recent changes'), -(20, 'user_wakeup', 'User wakeup-service organization'), -(21, 'admin_import', 'Import rooms and shifts from pentabarf'), -(22, 'credits', 'View credits'), -(23, 'faq', 'View FAQ'), -(24, 'user_shifts', 'Signup for shifts'), -(25, 'user_shifts_admin', 'Signup other angels for shifts.'), -(26, 'user_myshifts', 'Allow angels to view their own shifts and cancel them.'), -(27, 'admin_arrive', 'Mark angels when they arrive.'), -(28, 'admin_shifts', 'Create shifts'), -(30, 'ical', 'iCal shift export'), -(31, 'admin_active', 'Mark angels as active and if they got a t-shirt.'), -(32, 'admin_free', 'Show a list of free/unemployed angels.'), -(33, 'admin_user_angeltypes', 'Confirm restricted angel types'), -(34, 'atom', ' Atom news export'), -(35, 'shifts_json_export', 'Export shifts in JSON format'), -(36, 'angeltypes', 'View angeltypes'), -(37, 'user_angeltypes', 'Join angeltypes.'), -(38, 'shifttypes', 'Administrate shift types'), -(39, 'admin_event_config', 'Allow editing event config'); - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `Questions` --- - -DROP TABLE IF EXISTS `Questions`; -CREATE TABLE `Questions` ( - `QID` bigint(20) NOT NULL, - `UID` int(11) NOT NULL DEFAULT '0', - `Question` text NOT NULL, - `AID` int(11) DEFAULT NULL, - `Answer` text -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Fragen und Antworten'; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `Room` --- - -DROP TABLE IF EXISTS `Room`; -CREATE TABLE `Room` ( - `RID` int(11) NOT NULL, - `Name` varchar(35) NOT NULL DEFAULT '', - `Man` text, - `FromPentabarf` char(1) NOT NULL DEFAULT 'N', - `show` char(1) NOT NULL DEFAULT 'Y', - `Number` int(11) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `Room` --- - -INSERT INTO `Room` (`RID`, `Name`, `Man`, `FromPentabarf`, `show`, `Number`) VALUES -(1, 'Testraum', NULL, '', 'Y', 0); - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `ShiftEntry` --- - -DROP TABLE IF EXISTS `ShiftEntry`; -CREATE TABLE `ShiftEntry` ( - `id` int(11) NOT NULL, - `SID` int(11) NOT NULL DEFAULT '0', - `TID` int(11) NOT NULL DEFAULT '0', - `UID` int(11) NOT NULL DEFAULT '0', - `Comment` text, - `freeload_comment` text, - `freeloaded` tinyint(1) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `Shifts` --- - -DROP TABLE IF EXISTS `Shifts`; -CREATE TABLE `Shifts` ( - `SID` int(11) NOT NULL, - `title` text, - `shifttype_id` int(11) NOT NULL, - `start` int(11) NOT NULL, - `end` int(11) NOT NULL, - `RID` int(11) NOT NULL DEFAULT '0', - `URL` text, - `PSID` int(11) DEFAULT NULL, - `created_by_user_id` int(11) DEFAULT NULL, - `created_at_timestamp` int(11) NOT NULL, - `edited_by_user_id` int(11) DEFAULT NULL, - `edited_at_timestamp` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `ShiftTypes` --- - -DROP TABLE IF EXISTS `ShiftTypes`; -CREATE TABLE `ShiftTypes` ( - `id` int(11) NOT NULL, - `name` varchar(255) NOT NULL, - `angeltype_id` int(11) DEFAULT NULL, - `description` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `ShiftTypes` --- - -INSERT INTO `ShiftTypes` (`id`, `name`, `angeltype_id`, `description`) VALUES -(4, 'Schichttyp1', NULL, ''); - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `User` --- - -DROP TABLE IF EXISTS `User`; -CREATE TABLE `User` ( - `UID` int(11) NOT NULL, - `Nick` varchar(23) NOT NULL DEFAULT '', - `Name` varchar(23) DEFAULT NULL, - `Vorname` varchar(23) DEFAULT NULL, - `Alter` int(4) DEFAULT NULL, - `Telefon` varchar(40) DEFAULT NULL, - `DECT` varchar(5) DEFAULT NULL, - `Handy` varchar(40) DEFAULT NULL, - `email` varchar(123) DEFAULT NULL, - `email_shiftinfo` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'User wants to be informed by mail about changes in his shifts', - `jabber` varchar(200) DEFAULT NULL, - `Size` varchar(4) DEFAULT NULL, - `Passwort` varchar(128) DEFAULT NULL, - `password_recovery_token` varchar(32) DEFAULT NULL, - `Gekommen` tinyint(4) NOT NULL DEFAULT '0', - `Aktiv` tinyint(4) NOT NULL DEFAULT '0', - `force_active` tinyint(1) NOT NULL, - `Tshirt` tinyint(4) DEFAULT '0', - `color` tinyint(4) DEFAULT '10', - `Sprache` char(64) NOT NULL, - `Menu` char(1) NOT NULL DEFAULT 'L', - `lastLogIn` int(11) NOT NULL, - `CreateDate` datetime NOT NULL DEFAULT '0001-01-01 00:00:00', - `Art` varchar(30) DEFAULT NULL, - `kommentar` text, - `Hometown` varchar(255) NOT NULL DEFAULT '', - `api_key` varchar(32) NOT NULL, - `got_voucher` int(11) NOT NULL, - `arrival_date` int(11) DEFAULT NULL, - `planned_arrival_date` int(11) NOT NULL, - `planned_departure_date` int(11) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `User` --- - -INSERT INTO `User` (`UID`, `Nick`, `Name`, `Vorname`, `Alter`, `Telefon`, `DECT`, `Handy`, `email`, `email_shiftinfo`, `jabber`, `Size`, `Passwort`, `password_recovery_token`, `Gekommen`, `Aktiv`, `force_active`, `Tshirt`, `color`, `Sprache`, `Menu`, `lastLogIn`, `CreateDate`, `Art`, `kommentar`, `Hometown`, `api_key`, `got_voucher`, `arrival_date`, `planned_arrival_date`, `planned_departure_date`) VALUES -(1, 'admin', 'Gates', 'Bill', 42, '', '-', '', 'admin@example.com', 1, '', 'XL', '$6$rounds=5000$hjXbIhoRTH3vKiRa$Wl2P2iI5T9iRR.HHu/YFHswBW0WVn0yxCfCiX0Keco9OdIoDK6bIAADswP6KvMCJSwTGdV8PgA8g8Xfw5l8BD1', NULL, 1, 1, 0, 1, 0, 'de_DE.UTF-8', 'L', 1474990948, '0001-01-01 00:00:00', '', '', '', '038850abdd1feb264406be3ffa746235', 0, 1439490478, 1436964455, 1440161255); - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `UserAngelTypes` --- - -DROP TABLE IF EXISTS `UserAngelTypes`; -CREATE TABLE `UserAngelTypes` ( - `id` int(11) NOT NULL, - `user_id` int(11) NOT NULL, - `angeltype_id` int(11) NOT NULL, - `confirm_user_id` int(11) DEFAULT NULL, - `coordinator` tinyint(1) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `UserDriverLicenses` --- - -DROP TABLE IF EXISTS `UserDriverLicenses`; -CREATE TABLE `UserDriverLicenses` ( - `user_id` int(11) NOT NULL, - `has_car` tinyint(1) NOT NULL, - `has_license_car` tinyint(1) NOT NULL, - `has_license_3_5t_transporter` tinyint(1) NOT NULL, - `has_license_7_5t_truck` tinyint(1) NOT NULL, - `has_license_12_5t_truck` tinyint(1) NOT NULL, - `has_license_forklift` tinyint(1) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `UserDriverLicenses` --- - -INSERT INTO `UserDriverLicenses` (`user_id`, `has_car`, `has_license_car`, `has_license_3_5t_transporter`, `has_license_7_5t_truck`, `has_license_12_5t_truck`, `has_license_forklift`) VALUES -(1, 1, 1, 1, 1, 1, 1); - --- -------------------------------------------------------- - --- --- Tabellenstruktur fΓΌr Tabelle `UserGroups` --- - -DROP TABLE IF EXISTS `UserGroups`; -CREATE TABLE `UserGroups` ( - `id` int(11) NOT NULL, - `uid` int(11) NOT NULL, - `group_id` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Daten fΓΌr Tabelle `UserGroups` --- - -INSERT INTO `UserGroups` (`id`, `uid`, `group_id`) VALUES -(3, 1, -7), -(4, 1, -6), -(12, 1, -5), -(2, 1, -4), -(1, 1, -2); - --- --- Indizes der exportierten Tabellen --- - --- --- Indizes fΓΌr die Tabelle `AngelTypes` --- -ALTER TABLE `AngelTypes` - ADD PRIMARY KEY (`id`), - ADD UNIQUE KEY `Name` (`name`); - --- --- Indizes fΓΌr die Tabelle `GroupPrivileges` --- -ALTER TABLE `GroupPrivileges` - ADD PRIMARY KEY (`id`), - ADD KEY `group_id` (`group_id`,`privilege_id`), - ADD KEY `privilege_id` (`privilege_id`); - --- --- Indizes fΓΌr die Tabelle `Groups` --- -ALTER TABLE `Groups` - ADD PRIMARY KEY (`UID`); - --- --- Indizes fΓΌr die Tabelle `LogEntries` --- -ALTER TABLE `LogEntries` - ADD PRIMARY KEY (`id`), - ADD KEY `timestamp` (`timestamp`); - --- --- Indizes fΓΌr die Tabelle `Messages` --- -ALTER TABLE `Messages` - ADD PRIMARY KEY (`id`), - ADD KEY `Datum` (`Datum`), - ADD KEY `SUID` (`SUID`), - ADD KEY `RUID` (`RUID`); - --- --- Indizes fΓΌr die Tabelle `NeededAngelTypes` --- -ALTER TABLE `NeededAngelTypes` - ADD PRIMARY KEY (`id`), - ADD KEY `room_id` (`room_id`,`angel_type_id`), - ADD KEY `shift_id` (`shift_id`), - ADD KEY `angel_type_id` (`angel_type_id`); - --- --- Indizes fΓΌr die Tabelle `News` --- -ALTER TABLE `News` - ADD PRIMARY KEY (`ID`), - ADD KEY `UID` (`UID`); - --- --- Indizes fΓΌr die Tabelle `NewsComments` --- -ALTER TABLE `NewsComments` - ADD PRIMARY KEY (`ID`), - ADD KEY `Refid` (`Refid`), - ADD KEY `UID` (`UID`); - --- --- Indizes fΓΌr die Tabelle `Privileges` --- -ALTER TABLE `Privileges` - ADD PRIMARY KEY (`id`), - ADD UNIQUE KEY `name` (`name`); - --- --- Indizes fΓΌr die Tabelle `Questions` --- -ALTER TABLE `Questions` - ADD PRIMARY KEY (`QID`), - ADD KEY `UID` (`UID`), - ADD KEY `AID` (`AID`); - --- --- Indizes fΓΌr die Tabelle `Room` --- -ALTER TABLE `Room` - ADD PRIMARY KEY (`RID`), - ADD UNIQUE KEY `Name` (`Name`); - --- --- Indizes fΓΌr die Tabelle `ShiftEntry` --- -ALTER TABLE `ShiftEntry` - ADD PRIMARY KEY (`id`), - ADD KEY `TID` (`TID`), - ADD KEY `UID` (`UID`), - ADD KEY `SID` (`SID`,`TID`), - ADD KEY `freeloaded` (`freeloaded`); - --- --- Indizes fΓΌr die Tabelle `Shifts` --- -ALTER TABLE `Shifts` - ADD PRIMARY KEY (`SID`), - ADD UNIQUE KEY `PSID` (`PSID`), - ADD KEY `RID` (`RID`), - ADD KEY `shifttype_id` (`shifttype_id`), - ADD KEY `created_by_user_id` (`created_by_user_id`), - ADD KEY `edited_by_user_id` (`edited_by_user_id`); - --- --- Indizes fΓΌr die Tabelle `ShiftTypes` --- -ALTER TABLE `ShiftTypes` - ADD PRIMARY KEY (`id`), - ADD KEY `angeltype_id` (`angeltype_id`); - --- --- Indizes fΓΌr die Tabelle `User` --- -ALTER TABLE `User` - ADD PRIMARY KEY (`UID`), - ADD UNIQUE KEY `Nick` (`Nick`), - ADD KEY `api_key` (`api_key`), - ADD KEY `password_recovery_token` (`password_recovery_token`), - ADD KEY `force_active` (`force_active`), - ADD KEY `arrival_date` (`arrival_date`,`planned_arrival_date`), - ADD KEY `planned_departure_date` (`planned_departure_date`); - --- --- Indizes fΓΌr die Tabelle `UserAngelTypes` --- -ALTER TABLE `UserAngelTypes` - ADD PRIMARY KEY (`id`), - ADD KEY `user_id` (`user_id`,`angeltype_id`,`confirm_user_id`), - ADD KEY `angeltype_id` (`angeltype_id`), - ADD KEY `confirm_user_id` (`confirm_user_id`), - ADD KEY `coordinator` (`coordinator`); - --- --- Indizes fΓΌr die Tabelle `UserDriverLicenses` --- -ALTER TABLE `UserDriverLicenses` - ADD PRIMARY KEY (`user_id`); - --- --- Indizes fΓΌr die Tabelle `UserGroups` --- -ALTER TABLE `UserGroups` - ADD PRIMARY KEY (`id`), - ADD KEY `uid` (`uid`,`group_id`), - ADD KEY `group_id` (`group_id`); - --- --- AUTO_INCREMENT fΓΌr exportierte Tabellen --- - --- --- AUTO_INCREMENT fΓΌr Tabelle `AngelTypes` --- -ALTER TABLE `AngelTypes` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4; --- --- AUTO_INCREMENT fΓΌr Tabelle `GroupPrivileges` --- -ALTER TABLE `GroupPrivileges` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=259; --- --- AUTO_INCREMENT fΓΌr Tabelle `LogEntries` --- -ALTER TABLE `LogEntries` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; --- --- AUTO_INCREMENT fΓΌr Tabelle `Messages` --- -ALTER TABLE `Messages` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; --- --- AUTO_INCREMENT fΓΌr Tabelle `NeededAngelTypes` --- -ALTER TABLE `NeededAngelTypes` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; --- --- AUTO_INCREMENT fΓΌr Tabelle `News` --- -ALTER TABLE `News` - MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT; --- --- AUTO_INCREMENT fΓΌr Tabelle `NewsComments` --- -ALTER TABLE `NewsComments` - MODIFY `ID` bigint(11) NOT NULL AUTO_INCREMENT; --- --- AUTO_INCREMENT fΓΌr Tabelle `Privileges` --- -ALTER TABLE `Privileges` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=40; --- --- AUTO_INCREMENT fΓΌr Tabelle `Questions` --- -ALTER TABLE `Questions` - MODIFY `QID` bigint(20) NOT NULL AUTO_INCREMENT; --- --- AUTO_INCREMENT fΓΌr Tabelle `Room` --- -ALTER TABLE `Room` - MODIFY `RID` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2; --- --- AUTO_INCREMENT fΓΌr Tabelle `ShiftEntry` --- -ALTER TABLE `ShiftEntry` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2; --- --- AUTO_INCREMENT fΓΌr Tabelle `Shifts` --- -ALTER TABLE `Shifts` - MODIFY `SID` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13; --- --- AUTO_INCREMENT fΓΌr Tabelle `ShiftTypes` --- -ALTER TABLE `ShiftTypes` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5; --- --- AUTO_INCREMENT fΓΌr Tabelle `User` --- -ALTER TABLE `User` - MODIFY `UID` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2; --- --- AUTO_INCREMENT fΓΌr Tabelle `UserAngelTypes` --- -ALTER TABLE `UserAngelTypes` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2; --- --- AUTO_INCREMENT fΓΌr Tabelle `UserGroups` --- -ALTER TABLE `UserGroups` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13; --- --- Constraints der exportierten Tabellen --- - --- --- Constraints der Tabelle `GroupPrivileges` --- -ALTER TABLE `GroupPrivileges` - ADD CONSTRAINT `groupprivileges_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `Groups` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `groupprivileges_ibfk_2` FOREIGN KEY (`privilege_id`) REFERENCES `Privileges` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `Messages` --- -ALTER TABLE `Messages` - ADD CONSTRAINT `messages_ibfk_1` FOREIGN KEY (`SUID`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `messages_ibfk_2` FOREIGN KEY (`RUID`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `NeededAngelTypes` --- -ALTER TABLE `NeededAngelTypes` - ADD CONSTRAINT `neededangeltypes_ibfk_1` FOREIGN KEY (`room_id`) REFERENCES `Room` (`RID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `neededangeltypes_ibfk_2` FOREIGN KEY (`shift_id`) REFERENCES `Shifts` (`SID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `neededangeltypes_ibfk_3` FOREIGN KEY (`angel_type_id`) REFERENCES `AngelTypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `News` --- -ALTER TABLE `News` - ADD CONSTRAINT `news_ibfk_1` FOREIGN KEY (`UID`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `NewsComments` --- -ALTER TABLE `NewsComments` - ADD CONSTRAINT `newscomments_ibfk_1` FOREIGN KEY (`Refid`) REFERENCES `News` (`ID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `newscomments_ibfk_2` FOREIGN KEY (`UID`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `Questions` --- -ALTER TABLE `Questions` - ADD CONSTRAINT `questions_ibfk_1` FOREIGN KEY (`UID`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `questions_ibfk_2` FOREIGN KEY (`AID`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `ShiftEntry` --- -ALTER TABLE `ShiftEntry` - ADD CONSTRAINT `shiftentry_ibfk_1` FOREIGN KEY (`SID`) REFERENCES `Shifts` (`SID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `shiftentry_ibfk_2` FOREIGN KEY (`UID`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `shiftentry_ibfk_3` FOREIGN KEY (`TID`) REFERENCES `AngelTypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `Shifts` --- -ALTER TABLE `Shifts` - ADD CONSTRAINT `shifts_ibfk_1` FOREIGN KEY (`RID`) REFERENCES `Room` (`RID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `shifts_ibfk_2` FOREIGN KEY (`shifttype_id`) REFERENCES `ShiftTypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `shifts_ibfk_3` FOREIGN KEY (`created_by_user_id`) REFERENCES `User` (`UID`) ON DELETE SET NULL ON UPDATE CASCADE, - ADD CONSTRAINT `shifts_ibfk_4` FOREIGN KEY (`edited_by_user_id`) REFERENCES `User` (`UID`) ON DELETE SET NULL ON UPDATE CASCADE; - --- --- Constraints der Tabelle `ShiftTypes` --- -ALTER TABLE `ShiftTypes` - ADD CONSTRAINT `shifttypes_ibfk_1` FOREIGN KEY (`angeltype_id`) REFERENCES `AngelTypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `UserAngelTypes` --- -ALTER TABLE `UserAngelTypes` - ADD CONSTRAINT `userangeltypes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `userangeltypes_ibfk_2` FOREIGN KEY (`angeltype_id`) REFERENCES `AngelTypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `userangeltypes_ibfk_3` FOREIGN KEY (`confirm_user_id`) REFERENCES `User` (`UID`) ON DELETE SET NULL ON UPDATE CASCADE; - --- --- Constraints der Tabelle `UserDriverLicenses` --- -ALTER TABLE `UserDriverLicenses` - ADD CONSTRAINT `userdriverlicenses_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE; - --- --- Constraints der Tabelle `UserGroups` --- -ALTER TABLE `UserGroups` - ADD CONSTRAINT `usergroups_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `Groups` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE, - ADD CONSTRAINT `usergroups_ibfk_2` FOREIGN KEY (`uid`) REFERENCES `User` (`UID`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/db/migrations/2018_01_01_000001_import_install_sql.php b/db/migrations/2018_01_01_000001_import_install_sql.php index f13559e27..175706c14 100644 --- a/db/migrations/2018_01_01_000001_import_install_sql.php +++ b/db/migrations/2018_01_01_000001_import_install_sql.php @@ -29,24 +29,9 @@ class ImportInstallSql extends Migration 'UserAngelTypes', 'UserDriverLicenses', 'UserGroups', + 'UserWorkLog', ]; - /** - * Run the migration - */ - public function up(): void - { - foreach ($this->oldTables as $table) { - if ($this->schema->hasTable($table)) { - return; - } - } - - $sql = file_get_contents(__DIR__ . '/../install.sql'); - $this->schema->getConnection()->unprepared($sql); - } - - /** * Reverse the migration */ @@ -54,6 +39,7 @@ public function down(): void { $this->schema->getConnection()->statement('SET FOREIGN_KEY_CHECKS=0;'); + // Delete all remaining tables foreach ($this->oldTables as $table) { if ($this->schema->hasTable($table)) { $this->schema->dropIfExists($table); diff --git a/db/migrations/2018_01_01_000002_import_update_sql.php b/db/migrations/2018_01_01_000002_import_update_sql.php index af311249c..44b6c8c28 100644 --- a/db/migrations/2018_01_01_000002_import_update_sql.php +++ b/db/migrations/2018_01_01_000002_import_update_sql.php @@ -8,24 +8,5 @@ class ImportUpdateSql extends Migration { - /** - * Run the migration - */ - public function up(): void - { - if ($this->schema->hasTable('UserWorkLog')) { - return; - } - - $sql = file_get_contents(__DIR__ . '/../update.sql'); - $this->schema->getConnection()->unprepared($sql); - } - - /** - * Reverse the migration - */ - public function down(): void - { - $this->schema->dropIfExists('UserWorkLog'); - } + // Do nothing as the tables will be created by later migrations and deleted by ImportInstall } diff --git a/db/migrations/2020_09_02_000000_create_rooms_table.php b/db/migrations/2020_09_02_000000_create_rooms_table.php index c527d096b..81b91f388 100644 --- a/db/migrations/2020_09_02_000000_create_rooms_table.php +++ b/db/migrations/2020_09_02_000000_create_rooms_table.php @@ -11,7 +11,6 @@ class CreateRoomsTable extends Migration { use ChangesReferences; - use Reference; /** * Run the migration diff --git a/db/migrations/2020_09_07_000000_create_worklogs_table.php b/db/migrations/2020_09_07_000000_create_worklogs_table.php index 7a078e1e0..cc6e2f4e8 100644 --- a/db/migrations/2020_09_07_000000_create_worklogs_table.php +++ b/db/migrations/2020_09_07_000000_create_worklogs_table.php @@ -37,6 +37,7 @@ public function up(): void ->get(); foreach ($previousRecords as $previousRecord) { + $worked_at = Carbon::createFromTimestamp($previousRecord->work_timestamp); $created_at = Carbon::createFromTimestamp($previousRecord->created_timestamp); $this->schema->getConnection() ->table('worklogs') @@ -44,7 +45,7 @@ public function up(): void 'id' => $previousRecord->id, 'user_id' => $previousRecord->user_id, 'creator_id' => $previousRecord->created_user_id, - 'worked_at' => $previousRecord->work_timestamp, + 'worked_at' => $worked_at, 'hours' => $previousRecord->work_hours, 'comment' => $previousRecord->comment, 'created_at' => $created_at, @@ -87,11 +88,11 @@ public function down(): void ->insert([ 'id' => $record->id, 'user_id' => $record->user_id, - 'work_timestamp' => $record->worked_at->timestamp, + 'work_timestamp' => Carbon::createFromFormat('Y-m-d', $record->worked_at)->timestamp, 'work_hours' => $record->hours, 'comment' => $record->comment, 'created_user_id' => $record->creator_id, - 'created_timestamp' => $record->created_at->timestamp, + 'created_timestamp' => Carbon::createFromFormat('Y-m-d H:i:s', $record->created_at)->timestamp, ]); } diff --git a/db/migrations/2020_09_27_000000_add_timestamps_to_questions.php b/db/migrations/2020_09_27_000000_add_timestamps_to_questions.php index c2a0abc1d..272c0c95f 100644 --- a/db/migrations/2020_09_27_000000_add_timestamps_to_questions.php +++ b/db/migrations/2020_09_27_000000_add_timestamps_to_questions.php @@ -10,8 +10,6 @@ class AddTimestampsToQuestions extends Migration { - use ChangesReferences; - /** * Run the migration */ diff --git a/db/migrations/2020_11_20_000000_add_name_minutes_and_timestamps_to_schedules.php b/db/migrations/2020_11_20_000000_add_name_minutes_and_timestamps_to_schedules.php index 923457f0e..c9aab6498 100644 --- a/db/migrations/2020_11_20_000000_add_name_minutes_and_timestamps_to_schedules.php +++ b/db/migrations/2020_11_20_000000_add_name_minutes_and_timestamps_to_schedules.php @@ -21,7 +21,7 @@ public function up(): void $this->schema->table('schedules', function (Blueprint $table): void { $table->string('name')->default('')->after('id'); - $table->integer('shift_type')->default(0)->after('name'); + $table->unsignedInteger('shift_type')->default(0)->after('name'); $table->integer('minutes_before')->default(0)->after('shift_type'); $table->integer('minutes_after')->default(0)->after('minutes_before'); $table->timestamps(); @@ -36,7 +36,7 @@ public function up(): void $this->schema->table('schedules', function (Blueprint $table): void { $table->string('name')->default(null)->change(); - $table->integer('shift_type')->default(null)->change(); + $table->unsignedInteger('shift_type')->default(null)->change(); $table->integer('minutes_before')->default(null)->change(); $table->integer('minutes_after')->default(null)->change(); }); diff --git a/db/migrations/2020_12_25_000000_add_email_news_to_users_settings.php b/db/migrations/2020_12_25_000000_add_email_news_to_users_settings.php index d7e17b832..c32c32b10 100644 --- a/db/migrations/2020_12_25_000000_add_email_news_to_users_settings.php +++ b/db/migrations/2020_12_25_000000_add_email_news_to_users_settings.php @@ -9,8 +9,6 @@ class AddEmailNewsToUsersSettings extends Migration { - use Reference; - /** * Run the migration */ diff --git a/db/migrations/2020_12_25_000000_oauth_add_tokens.php b/db/migrations/2020_12_25_000000_oauth_add_tokens.php index 1c02d8cd7..e8a0cba11 100644 --- a/db/migrations/2020_12_25_000000_oauth_add_tokens.php +++ b/db/migrations/2020_12_25_000000_oauth_add_tokens.php @@ -9,8 +9,6 @@ class OauthAddTokens extends Migration { - use Reference; - /** * Run the migration */ diff --git a/db/migrations/2020_12_26_000000_news_add_is_pinned.php b/db/migrations/2020_12_26_000000_news_add_is_pinned.php index e60e5a5ea..68d6e9835 100644 --- a/db/migrations/2020_12_26_000000_news_add_is_pinned.php +++ b/db/migrations/2020_12_26_000000_news_add_is_pinned.php @@ -9,8 +9,6 @@ class NewsAddIsPinned extends Migration { - use Reference; - /** * Run the migration */ diff --git a/db/migrations/2020_12_28_000001_oauth_change_tokens_to_text.php b/db/migrations/2020_12_28_000001_oauth_change_tokens_to_text.php index 685322c83..1db6eb400 100644 --- a/db/migrations/2020_12_28_000001_oauth_change_tokens_to_text.php +++ b/db/migrations/2020_12_28_000001_oauth_change_tokens_to_text.php @@ -9,8 +9,6 @@ class OauthChangeTokensToText extends Migration { - use Reference; - /** * Run the migration */ diff --git a/db/migrations/2021_05_23_000000_create_first_user.php b/db/migrations/2021_05_23_000000_create_first_user.php new file mode 100644 index 000000000..d01473a1e --- /dev/null +++ b/db/migrations/2021_05_23_000000_create_first_user.php @@ -0,0 +1,45 @@ +schema->getConnection(); + if ($db->table('users')->count() > 0) { + return; + } + + $db->table('users')->insert([ + 'name' => 'admin', + 'email' => 'admin@localhost', + 'password' => password_hash('asdfasdf', PASSWORD_DEFAULT), + 'api_key' => bin2hex(random_bytes(16)), + 'created_at' => Carbon::now(), + ]); + + /** @var stdClass $admin */ + $admin = $db->table('users')->where('name', 'admin')->first(); + foreach (['users_contact', 'users_personal_data', 'users_state'] as $table) { + $db->table($table)->insert(['user_id' => $admin->id]); + } + $db->table('users_settings')->insert(['user_id' => $admin->id, 'language' => 'en_US', 'theme' => 0]); + } +} diff --git a/db/migrations/2021_05_23_000000_set_admin_password.php b/db/migrations/2021_05_23_000000_set_admin_password.php index a02bc5baf..7bbd421f0 100644 --- a/db/migrations/2021_05_23_000000_set_admin_password.php +++ b/db/migrations/2021_05_23_000000_set_admin_password.php @@ -6,14 +6,12 @@ use Engelsystem\Config\Config; use Engelsystem\Database\Migration\Migration; -use Engelsystem\Helpers\Authenticator; use Illuminate\Database\Schema\Builder as SchemaBuilder; +use stdClass; class SetAdminPassword extends Migration { - use Reference; - - public function __construct(SchemaBuilder $schemaBuilder, protected Authenticator $auth, protected Config $config) + public function __construct(SchemaBuilder $schemaBuilder, protected Config $config) { parent::__construct($schemaBuilder); } @@ -23,12 +21,21 @@ public function __construct(SchemaBuilder $schemaBuilder, protected Authenticato */ public function up(): void { - $admin = $this->auth->authenticate('admin', 'asdfasdf'); + $db = $this->schema->getConnection(); + /** @var stdClass $admin */ + $admin = $db->table('users')->where('name', 'admin')->first(); $setupPassword = $this->config->get('setup_admin_password'); - if (!$admin || !$setupPassword) { + + if ( + !$admin + || !password_verify('asdfasdf', $admin->password) + || !$setupPassword + ) { return; } - $this->auth->setPassword($admin, $setupPassword); + $db->table('users') + ->where('id', $admin->id) + ->update(['password' => password_hash($setupPassword, PASSWORD_DEFAULT)]); } } diff --git a/db/migrations/2021_10_12_000000_add_shifts_description.php b/db/migrations/2021_10_12_000000_add_shifts_description.php index 47ad04dd2..b2de144ef 100644 --- a/db/migrations/2021_10_12_000000_add_shifts_description.php +++ b/db/migrations/2021_10_12_000000_add_shifts_description.php @@ -9,8 +9,6 @@ class AddShiftsDescription extends Migration { - use Reference; - /** * Run the migration */ diff --git a/db/migrations/2021_12_29_000000_users_settings_add_email_goody.php b/db/migrations/2021_12_29_000000_users_settings_add_email_goody.php index 643bab03e..bfff55fa1 100644 --- a/db/migrations/2021_12_29_000000_users_settings_add_email_goody.php +++ b/db/migrations/2021_12_29_000000_users_settings_add_email_goody.php @@ -9,8 +9,6 @@ class UsersSettingsAddEmailGoody extends Migration { - use Reference; - /** * Run the migration */ diff --git a/db/migrations/2022_10_23_000001_fill_privileges_and_groups_related_tables.php b/db/migrations/2022_10_23_000001_fill_privileges_and_groups_related_tables.php new file mode 100644 index 000000000..0453da923 --- /dev/null +++ b/db/migrations/2022_10_23_000001_fill_privileges_and_groups_related_tables.php @@ -0,0 +1,158 @@ +schema->getConnection(); + if ($db->table('privileges')->count() > 0) { + return; + } + + $db->table('groups') + ->insert([ + ['id' => 10, 'name' => 'Guest'], + ['id' => 20, 'name' => 'Angel'], + ['id' => 30, 'name' => 'Welcome Angel'], + ['id' => 35, 'name' => 'Voucher Angel'], + ['id' => 50, 'name' => 'Shirt Manager'], + ['id' => 60, 'name' => 'Shift Coordinator'], + ['id' => 65, 'name' => 'Team Coordinator'], + ['id' => 80, 'name' => 'Bureaucrat'], + ['id' => 85, 'name' => 'News Admin'], + ['id' => 90, 'name' => 'Developer'], + ]); + + $db->table('privileges') + ->insert([ + ['id' => 1, 'name' => 'start', 'description' => 'Startseite fΓΌr GΓ€ste/Nicht eingeloggte User'], + ['id' => 2, 'name' => 'login', 'description' => 'Logindialog'], + ['id' => 3, 'name' => 'news', 'description' => 'Anzeigen der News-Seite'], + ['id' => 4, 'name' => 'logout', 'description' => 'User darf sich ausloggen'], + ['id' => 5, 'name' => 'register', 'description' => 'Einen neuen Engel registerieren'], + ['id' => 6, 'name' => 'admin_rooms', 'description' => 'RΓ€ume administrieren'], + ['id' => 7, 'name' => 'admin_angel_types', 'description' => 'Engel Typen administrieren'], + ['id' => 8, 'name' => 'user_settings', 'description' => 'User profile settings'], + ['id' => 9, 'name' => 'user_messages', + 'description' => 'Writing and reading messages from user to user'], + ['id' => 10, 'name' => 'admin_groups', 'description' => 'Manage usergroups and their rights'], + ['id' => 14, 'name' => 'admin_news', 'description' => 'Administrate the news section'], + ['id' => 15, 'name' => 'news_comments', 'description' => 'User can comment news'], + ['id' => 16, 'name' => 'admin_user', 'description' => 'Administrate the angels'], + ['id' => 17, 'name' => 'user_meetings', 'description' => 'Lists meetings (news)'], + ['id' => 18, 'name' => 'admin_language', 'description' => 'Translate the system'], + ['id' => 19, 'name' => 'admin_log', 'description' => 'Display recent changes'], + ['id' => 21, 'name' => 'schedule.import', 'description' => 'Import rooms and shifts from schedule.xml'], + ['id' => 24, 'name' => 'user_shifts', 'description' => 'Signup for shifts'], + ['id' => 25, 'name' => 'user_shifts_admin', 'description' => 'Signup other angels for shifts.'], + ['id' => 26, 'name' => 'user_myshifts', + 'description' => 'Allow angels to view their own shifts and cancel them.'], + ['id' => 27, 'name' => 'admin_arrive', 'description' => 'Mark angels when they arrive.'], + ['id' => 28, 'name' => 'admin_shifts', 'description' => 'Create shifts'], + ['id' => 30, 'name' => 'ical', 'description' => 'iCal shift export'], + ['id' => 31, 'name' => 'admin_active', + 'description' => 'Mark angels as active and if they got a t-shirt.'], + ['id' => 32, 'name' => 'admin_free', 'description' => 'Show a list of free/unemployed angels.'], + ['id' => 33, 'name' => 'admin_user_angeltypes', 'description' => 'Confirm restricted angel types'], + ['id' => 34, 'name' => 'atom', 'description' => ' Atom news export'], + ['id' => 35, 'name' => 'shifts_json_export', 'description' => 'Export shifts in JSON format'], + ['id' => 36, 'name' => 'angeltypes', 'description' => 'View angeltypes'], + ['id' => 37, 'name' => 'user_angeltypes', 'description' => 'Join angeltypes.'], + ['id' => 38, 'name' => 'shifttypes', 'description' => 'Administrate shift types'], + ['id' => 39, 'name' => 'admin_event_config', 'description' => 'Allow editing event config'], + ['id' => 40, 'name' => 'view_rooms', 'description' => 'User can view rooms'], + ['id' => 41, 'name' => 'shiftentry_edit_angeltype_supporter', + 'description' => 'If user with this privilege is angeltype supporter, ' + . 'he can put users in shifts for their angeltype'], + ['id' => 43, 'name' => 'admin_user_worklog', 'description' => 'Manage user work log entries.'], + ['id' => 44, 'name' => 'faq.view', 'description' => 'View FAQ entries'], + ['id' => 45, 'name' => 'faq.edit', 'description' => 'Edit FAQ entries'], + ['id' => 46, 'name' => 'question.add', 'description' => 'Ask questions'], + ['id' => 47, 'name' => 'question.edit', 'description' => 'Answer questions'], + ['id' => 48, 'name' => 'user.edit.shirt', 'description' => 'Edit user shirts'], + ['id' => 49, 'name' => 'voucher.edit', 'description' => 'Edit vouchers'], + ]); + + $db->table('group_privileges')->insert([ + ['id' => 23, 'group_id' => 10, 'privilege_id' => 2], + ['id' => 24, 'group_id' => 10, 'privilege_id' => 5], + ['id' => 85, 'group_id' => 90, 'privilege_id' => 10], + ['id' => 86, 'group_id' => 90, 'privilege_id' => 21], + ['id' => 87, 'group_id' => 90, 'privilege_id' => 18], + ['id' => 88, 'group_id' => 10, 'privilege_id' => 1], + ['id' => 206, 'group_id' => 80, 'privilege_id' => 31], + ['id' => 207, 'group_id' => 80, 'privilege_id' => 7], + ['id' => 209, 'group_id' => 80, 'privilege_id' => 21], + ['id' => 210, 'group_id' => 80, 'privilege_id' => 14], + ['id' => 212, 'group_id' => 80, 'privilege_id' => 6], + ['id' => 213, 'group_id' => 80, 'privilege_id' => 28], + ['id' => 214, 'group_id' => 80, 'privilege_id' => 16], + ['id' => 215, 'group_id' => 80, 'privilege_id' => 33], + ['id' => 216, 'group_id' => 80, 'privilege_id' => 5], + ['id' => 218, 'group_id' => 60, 'privilege_id' => 39], + ['id' => 219, 'group_id' => 65, 'privilege_id' => 14], + ['id' => 220, 'group_id' => 65, 'privilege_id' => 33], + ['id' => 221, 'group_id' => 65, 'privilege_id' => 25], + ['id' => 235, 'group_id' => 60, 'privilege_id' => 27], + ['id' => 236, 'group_id' => 60, 'privilege_id' => 32], + ['id' => 237, 'group_id' => 60, 'privilege_id' => 19], + ['id' => 238, 'group_id' => 60, 'privilege_id' => 14], + ['id' => 239, 'group_id' => 60, 'privilege_id' => 28], + ['id' => 240, 'group_id' => 60, 'privilege_id' => 16], + ['id' => 241, 'group_id' => 60, 'privilege_id' => 5], + ['id' => 242, 'group_id' => 60, 'privilege_id' => 25], + ['id' => 243, 'group_id' => 20, 'privilege_id' => 36], + ['id' => 244, 'group_id' => 20, 'privilege_id' => 34], + ['id' => 245, 'group_id' => 20, 'privilege_id' => 30], + ['id' => 246, 'group_id' => 20, 'privilege_id' => 4], + ['id' => 247, 'group_id' => 20, 'privilege_id' => 3], + ['id' => 248, 'group_id' => 20, 'privilege_id' => 15], + ['id' => 249, 'group_id' => 20, 'privilege_id' => 35], + ['id' => 250, 'group_id' => 20, 'privilege_id' => 37], + ['id' => 251, 'group_id' => 20, 'privilege_id' => 17], + ['id' => 252, 'group_id' => 20, 'privilege_id' => 9], + ['id' => 253, 'group_id' => 20, 'privilege_id' => 26], + ['id' => 255, 'group_id' => 20, 'privilege_id' => 8], + ['id' => 256, 'group_id' => 20, 'privilege_id' => 24], + ['id' => 257, 'group_id' => 80, 'privilege_id' => 38], + ['id' => 258, 'group_id' => 50, 'privilege_id' => 31], + ['id' => 259, 'group_id' => 20, 'privilege_id' => 40], + ['id' => 260, 'group_id' => 85, 'privilege_id' => 14], + ['id' => 262, 'group_id' => 60, 'privilege_id' => 43], + ['id' => 263, 'group_id' => 20, 'privilege_id' => 41], + ['id' => 264, 'group_id' => 30, 'privilege_id' => 27], + ['id' => 265, 'group_id' => 10, 'privilege_id' => 44], + ['id' => 266, 'group_id' => 20, 'privilege_id' => 44], + ['id' => 267, 'group_id' => 60, 'privilege_id' => 45], + ['id' => 268, 'group_id' => 20, 'privilege_id' => 46], + ['id' => 269, 'group_id' => 60, 'privilege_id' => 47], + ['id' => 270, 'group_id' => 60, 'privilege_id' => 48], + ['id' => 271, 'group_id' => 50, 'privilege_id' => 48], + ['id' => 272, 'group_id' => 50, 'privilege_id' => 27], + ['id' => 273, 'group_id' => 60, 'privilege_id' => 49], + ['id' => 274, 'group_id' => 35, 'privilege_id' => 49], + ['id' => 275, 'group_id' => 35, 'privilege_id' => 27], + ]); + + /** @var stdClass $admin */ + $admin = $db->table('users')->where('name', 'admin')->first(); + if (!$admin) { + return; + } + + // Angel, ShiCo, Team coordinator, Bureaucrat, Dev + foreach ([20, 60, 65, 80, 90] as $group) { + $db->table('users_groups')->insert(['user_id' => $admin->id, 'group_id' => $group]); + } + } +} diff --git a/db/migrations/2022_12_06_000000_change_api_key_length.php b/db/migrations/2022_12_06_000000_change_api_key_length.php index d5e811ded..f51bcb742 100644 --- a/db/migrations/2022_12_06_000000_change_api_key_length.php +++ b/db/migrations/2022_12_06_000000_change_api_key_length.php @@ -6,6 +6,7 @@ use Engelsystem\Database\Migration\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Str; class ChangeApiKeyLength extends Migration { @@ -24,6 +25,19 @@ public function up(): void */ public function down(): void { + $connection = $this->schema->getConnection(); + $data = $connection->table('users')->get(['id', 'api_key']); + foreach ($data as $user) { + if (Str::length($user->api_key) <= 32) { + continue; + } + + $key = Str::substr($user->api_key, 0, 32); + $connection->table('users') + ->where('id', $user->id) + ->update(['api_key' => $key]); + } + $this->schema->table('users', function (Blueprint $table): void { $table->string('api_key', 32)->change(); }); diff --git a/db/migrations/2023_02_25_000000_fix_email_messages_migration_name.php b/db/migrations/2023_02_25_000000_fix_email_messages_migration_name.php new file mode 100644 index 000000000..4acfb4e1c --- /dev/null +++ b/db/migrations/2023_02_25_000000_fix_email_messages_migration_name.php @@ -0,0 +1,25 @@ +schema->getConnection() + ->table('migrations') + ->where('migration', '2023_02_26_000000_AddEmailMessagesToUsersSettings') + ->update([ + 'migration' => '2023_02_26_000000_add_email_messages_to_users_settings', + ]); + } + + // Down migration not needed when on same version +} diff --git a/db/migrations/2023_02_26_000000_AddEmailMessagesToUsersSettings.php b/db/migrations/2023_02_26_000000_add_email_messages_to_users_settings.php similarity index 97% rename from db/migrations/2023_02_26_000000_AddEmailMessagesToUsersSettings.php rename to db/migrations/2023_02_26_000000_add_email_messages_to_users_settings.php index 12a7450b6..52f72def1 100644 --- a/db/migrations/2023_02_26_000000_AddEmailMessagesToUsersSettings.php +++ b/db/migrations/2023_02_26_000000_add_email_messages_to_users_settings.php @@ -9,8 +9,6 @@ class AddEmailMessagesToUsersSettings extends Migration { - use Reference; - /** * Run the migration */ diff --git a/db/migrations/2023_05_21_000000_create_api_permissions.php b/db/migrations/2023_05_21_000000_create_api_permissions.php new file mode 100644 index 000000000..8c9b87021 --- /dev/null +++ b/db/migrations/2023_05_21_000000_create_api_permissions.php @@ -0,0 +1,45 @@ +schema->getConnection(); + $db->table('privileges')->insert([ + ['name' => 'api', 'description' => 'Use the API'], + ]); + $db->table('groups')->insert([ + ['id' => 40, 'name' => 'API'], + ]); + + $bureaucratGroup = 80; + $apiId = $db->table('privileges')->where('name', 'api')->first()->id; + $db->table('group_privileges')->insert([ + ['group_id' => $bureaucratGroup, 'privilege_id' => $apiId], + ['group_id' => 40, 'privilege_id' => $apiId], + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $db = $this->schema->getConnection(); + $db->table('privileges') + ->where('name', 'api') + ->delete(); + $db->table('groups') + ->where('id', 40) + ->delete(); + } +} diff --git a/db/migrations/2023_05_21_000001_cleanup_short_api_keys.php b/db/migrations/2023_05_21_000001_cleanup_short_api_keys.php new file mode 100644 index 000000000..7f666eefc --- /dev/null +++ b/db/migrations/2023_05_21_000001_cleanup_short_api_keys.php @@ -0,0 +1,28 @@ +schema->getConnection(); + foreach ($db->table('users')->get() as $user) { + if (Str::length($user->api_key) > 42) { + continue; + } + + $db->table('users') + ->where('id', $user->id) + ->update(['api_key' => bin2hex(random_bytes(32))]); + } + } +} diff --git a/db/migrations/2023_08_07_000000_add_ifsg_cerificates_to_users_licenses.php b/db/migrations/2023_08_07_000000_add_ifsg_cerificates_to_users_licenses.php new file mode 100644 index 000000000..461404d79 --- /dev/null +++ b/db/migrations/2023_08_07_000000_add_ifsg_cerificates_to_users_licenses.php @@ -0,0 +1,33 @@ +schema->table('users_licenses', function (Blueprint $table): void { + $table->boolean('ifsg_certificate_light')->default(false)->after('drive_12t'); + $table->boolean('ifsg_certificate')->default(false)->after('ifsg_certificate_light'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('users_licenses', function (Blueprint $table): void { + $table->dropColumn('ifsg_certificate_light'); + $table->dropColumn('ifsg_certificate'); + }); + } +} diff --git a/db/migrations/2023_08_08_000000_add_requires_ifsg_cerificate_to_angeltypes.php b/db/migrations/2023_08_08_000000_add_requires_ifsg_cerificate_to_angeltypes.php new file mode 100644 index 000000000..3e09ad394 --- /dev/null +++ b/db/migrations/2023_08_08_000000_add_requires_ifsg_cerificate_to_angeltypes.php @@ -0,0 +1,31 @@ +schema->table('angel_types', function (Blueprint $table): void { + $table->boolean('requires_ifsg_certificate')->default(false)->after('requires_driver_license'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('angel_types', function (Blueprint $table): void { + $table->dropColumn('requires_ifsg_certificate'); + }); + } +} diff --git a/db/migrations/2023_08_26_000000_angeltypes_rename_no_self_signup_to_shift_self_signup.php b/db/migrations/2023_08_26_000000_angeltypes_rename_no_self_signup_to_shift_self_signup.php new file mode 100644 index 000000000..c4078ec2d --- /dev/null +++ b/db/migrations/2023_08_26_000000_angeltypes_rename_no_self_signup_to_shift_self_signup.php @@ -0,0 +1,39 @@ +schema->table('angel_types', function (Blueprint $table): void { + $table->renameColumn('no_self_signup', 'shift_self_signup')->default(true); + $connection = $this->schema->getConnection(); + $connection->table('angel_types') + ->update(['no_self_signup' => $connection->raw('NOT no_self_signup'), + ]); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('angel_types', function (Blueprint $table): void { + $table->renameColumn('shift_self_signup', 'no_self_signup'); + $connection = $this->schema->getConnection(); + $connection->table('angel_types') + ->update(['shift_self_signup' => $connection->raw('NOT shift_self_signup'), + ]); + }); + } +} diff --git a/db/migrations/2023_08_27_000000_add_hide_on_shift_view_to_angeltypes.php b/db/migrations/2023_08_27_000000_add_hide_on_shift_view_to_angeltypes.php new file mode 100644 index 000000000..3fe8a1757 --- /dev/null +++ b/db/migrations/2023_08_27_000000_add_hide_on_shift_view_to_angeltypes.php @@ -0,0 +1,31 @@ +schema->table('angel_types', function (Blueprint $table): void { + $table->boolean('hide_on_shift_view')->default(false)->after('hide_register'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('angel_types', function (Blueprint $table): void { + $table->dropColumn('hide_on_shift_view'); + }); + } +} diff --git a/db/migrations/2023_09_17_000000_add_user_to_sessions_table.php b/db/migrations/2023_09_17_000000_add_user_to_sessions_table.php new file mode 100644 index 000000000..935e5be91 --- /dev/null +++ b/db/migrations/2023_09_17_000000_add_user_to_sessions_table.php @@ -0,0 +1,34 @@ +schema->table('sessions', function (Blueprint $table): void { + $this->referencesUser($table)->nullable()->index()->after('payload'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('sessions', function (Blueprint $table): void { + $table->dropForeign('sessions_user_id_foreign'); + $table->dropColumn('user_id'); + }); + } +} diff --git a/db/migrations/2023_09_18_000000_news_rename_important_to_highlight.php b/db/migrations/2023_09_18_000000_news_rename_important_to_highlight.php new file mode 100644 index 000000000..352ac3b62 --- /dev/null +++ b/db/migrations/2023_09_18_000000_news_rename_important_to_highlight.php @@ -0,0 +1,41 @@ +schema->table('news', function (Blueprint $table): void { + $table->renameColumn('is_important', 'is_highlighted'); + }); + + $this->schema->getConnection() + ->table('privileges') + ->where('name', 'news.important') + ->update(['name' => 'news.highlight', 'description' => 'Highlight News']); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('news', function (Blueprint $table): void { + $table->renameColumn('is_highlighted', 'is_important'); + }); + + $this->schema->getConnection() + ->table('privileges') + ->where('name', 'news.highlight') + ->update(['name' => 'news.important', 'description' => 'Make News Important']); + } +} diff --git a/db/migrations/2023_10_13_000000_rename_rooms_to_locations.php b/db/migrations/2023_10_13_000000_rename_rooms_to_locations.php new file mode 100644 index 000000000..f71a10d18 --- /dev/null +++ b/db/migrations/2023_10_13_000000_rename_rooms_to_locations.php @@ -0,0 +1,69 @@ +schema->rename('rooms', 'locations'); + + $this->schema->table('shifts', function (Blueprint $table): void { + $table->renameColumn('room_id', 'location_id'); + }); + + $this->schema->table('needed_angel_types', function (Blueprint $table): void { + $table->renameColumn('room_id', 'location_id'); + }); + + $db = $this->schema->getConnection(); + $db->table('privileges')->where('name', 'admin_rooms')->update([ + 'name' => 'admin_locations', + 'description' => 'Manage locations', + ]); + $db->table('privileges')->where('name', 'view_rooms')->update([ + 'name' => 'view_locations', + 'description' => 'User can view locations', + ]); + $db->table('privileges')->where('name', 'schedule.import')->update([ + 'description' => 'Import locations and shifts from schedule.xml', + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->rename('locations', 'rooms'); + + $this->schema->table('shifts', function (Blueprint $table): void { + $table->renameColumn('location_id', 'room_id'); + }); + + $this->schema->table('needed_angel_types', function (Blueprint $table): void { + $table->renameColumn('location_id', 'room_id'); + }); + + $db = $this->schema->getConnection(); + $db->table('privileges')->where('name', 'admin_locations')->update([ + 'name' => 'admin_rooms', + 'description' => 'RΓ€ume administrieren', + ]); + $db->table('privileges')->where('name', 'view_locations')->update([ + 'name' => 'view_rooms', + 'description' => 'User can view rooms', + ]); + $db->table('privileges')->where('name', 'schedule.import')->update([ + 'description' => 'Import rooms and shifts from schedule.xml', + ]); + } +} diff --git a/db/migrations/2023_10_22_000000_add_missing_schedule_foreign_keys.php b/db/migrations/2023_10_22_000000_add_missing_schedule_foreign_keys.php new file mode 100644 index 000000000..da95ab90f --- /dev/null +++ b/db/migrations/2023_10_22_000000_add_missing_schedule_foreign_keys.php @@ -0,0 +1,39 @@ +schema->getConnection()->getDoctrineSchemaManager(); + + $hasShiftTypeReference = $schemaManager->introspectTable('schedules') + ->hasIndex('schedules_shift_type_foreign'); + if (!$hasShiftTypeReference) { + $this->schema->table('schedules', function (Blueprint $table): void { + $table->unsignedInteger('shift_type')->change(); + $this->addReference($table, 'shift_type', 'shift_types'); + }); + } + + $hasShiftIdReference = $schemaManager->introspectTable('schedule_shift') + ->hasIndex('schedule_shift_schedule_id_foreign'); + if (!$hasShiftIdReference) { + $this->schema->table('schedule_shift', function (Blueprint $table): void { + $table->unsignedInteger('shift_id')->change(); + $this->addReference($table, 'shift_id', 'shifts'); + }); + } + } +} diff --git a/db/migrations/2023_10_25_000000_degender_shirt_sizes.php b/db/migrations/2023_10_25_000000_degender_shirt_sizes.php new file mode 100644 index 000000000..e41c8ee4d --- /dev/null +++ b/db/migrations/2023_10_25_000000_degender_shirt_sizes.php @@ -0,0 +1,50 @@ + 'S-F', + 'M-G' => 'M-F', + 'L-G' => 'L-F', + 'XL-G' => 'XL-F', + ]; + + /** + * Run the migration + */ + public function up(): void + { + $this->migrate($this->sizes); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->migrate(array_flip($this->sizes)); + } + + /** + * @param string[] $sizes + */ + private function migrate(array $sizes): void + { + $connection = $this->schema->getConnection(); + foreach ($sizes as $from => $to) { + $connection + ->table('users_personal_data') + ->where('shirt_size', $from) + ->update([ + 'shirt_size' => $to, + ]); + } + } +} diff --git a/db/migrations/2023_11_16_000000_add_user_info_to_users_state.php b/db/migrations/2023_11_16_000000_add_user_info_to_users_state.php new file mode 100644 index 000000000..53484a469 --- /dev/null +++ b/db/migrations/2023_11_16_000000_add_user_info_to_users_state.php @@ -0,0 +1,31 @@ +schema->table('users_state', function (Blueprint $table): void { + $table->string('user_info')->nullable()->default(null)->after('arrival_date'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('users_state', function (Blueprint $table): void { + $table->dropColumn('user_info'); + }); + } +} diff --git a/db/migrations/2023_11_16_000001_add_user_info_permissions.php b/db/migrations/2023_11_16_000001_add_user_info_permissions.php new file mode 100644 index 000000000..6d272c5b8 --- /dev/null +++ b/db/migrations/2023_11_16_000001_add_user_info_permissions.php @@ -0,0 +1,52 @@ +schema->getConnection(); + $db->table('privileges') + ->insert([ + ['name' => 'user.info.show', 'description' => 'Show User Info'], + ['name' => 'user.info.edit', 'description' => 'Edit User Info'], + ]); + + $showUserInfo = $db->table('privileges') + ->where('name', 'user.info.show') + ->get(['id']) + ->first(); + + $editUserInfo = $db->table('privileges') + ->where('name', 'user.info.edit') + ->get(['id']) + ->first(); + + $buerocrat = 80; + $shico = 60; + $db->table('group_privileges') + ->insertOrIgnore([ + ['group_id' => $buerocrat, 'privilege_id' => $editUserInfo->id], + ['group_id' => $shico, 'privilege_id' => $showUserInfo->id], + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $db = $this->schema->getConnection(); + $db->table('privileges') + ->whereIn('name', ['user.info.edit', 'user.info.show']) + ->delete(); + } +} diff --git a/db/migrations/2023_12_06_000000_add_user_id_to_log_entries.php b/db/migrations/2023_12_06_000000_add_user_id_to_log_entries.php new file mode 100644 index 000000000..a3bd80292 --- /dev/null +++ b/db/migrations/2023_12_06_000000_add_user_id_to_log_entries.php @@ -0,0 +1,38 @@ +schema->table('log_entries', function (Blueprint $table): void { + $table->unsignedInteger('user_id')->after('id')->nullable()->default(null); + $table->foreign('user_id') + ->references('id')->on('users') + ->onUpdate('cascade') + ->nullOnDelete(); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('log_entries', function (Blueprint $table): void { + $table->dropForeign('log_entries_user_id_foreign'); + $table->dropColumn('user_id'); + }); + } +} diff --git a/db/migrations/2023_12_06_000000_change_edit_shirt_require_bureaucrat.php b/db/migrations/2023_12_06_000000_change_edit_shirt_require_bureaucrat.php new file mode 100644 index 000000000..32e91ece7 --- /dev/null +++ b/db/migrations/2023_12_06_000000_change_edit_shirt_require_bureaucrat.php @@ -0,0 +1,57 @@ +db = $this->schema->getConnection(); + + $this->editShirt = $this->db->table('privileges') + ->where('name', 'user.edit.shirt') + ->get(['id']) + ->first()->id; + } + + /** + * Run the migration + */ + public function up(): void + { + $this->movePermission($this->editShirt, $this->shiCo, $this->bureaucrat); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->movePermission($this->editShirt, $this->bureaucrat, $this->shiCo); + } + + protected function movePermission(int $privilege, int $oldGroup, int $newGroup): void + { + $this->db->table('group_privileges') + ->insertOrIgnore(['group_id' => $newGroup, 'privilege_id' => $privilege]); + + $this->db->table('group_privileges') + ->where(['group_id' => $oldGroup, 'privilege_id' => $privilege]) + ->delete(); + } +} diff --git a/db/migrations/2023_12_06_000001_add_logs_all_permission.php b/db/migrations/2023_12_06_000001_add_logs_all_permission.php new file mode 100644 index 000000000..41a2df792 --- /dev/null +++ b/db/migrations/2023_12_06_000001_add_logs_all_permission.php @@ -0,0 +1,44 @@ +schema->getConnection(); + $db->table('privileges') + ->insert([ + ['name' => 'logs.all', 'description' => 'View all logs'], + ]); + + $logsAll = $db->table('privileges') + ->where('name', 'logs.all') + ->get(['id']) + ->first(); + + $bureaucrat = 80; + $db->table('group_privileges') + ->insertOrIgnore([ + ['group_id' => $bureaucrat, 'privilege_id' => $logsAll->id], + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $db = $this->schema->getConnection(); + $db->table('privileges') + ->where('name', 'logs.all') + ->delete(); + } +} diff --git a/db/migrations/2023_12_19_000000_schedule_shift_type_needed_angel_types.php b/db/migrations/2023_12_19_000000_schedule_shift_type_needed_angel_types.php new file mode 100644 index 000000000..e9ce54d59 --- /dev/null +++ b/db/migrations/2023_12_19_000000_schedule_shift_type_needed_angel_types.php @@ -0,0 +1,40 @@ +schema->table('schedules', function (Blueprint $table): void { + $table->boolean('needed_from_shift_type')->after('shift_type')->default(false); + }); + $this->schema->table('needed_angel_types', function (Blueprint $table): void { + $this->references($table, 'shift_types')->after('shift_id')->nullable(); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('schedules', function (Blueprint $table): void { + $table->dropColumn('needed_from_shift_type'); + }); + $this->schema->table('needed_angel_types', function (Blueprint $table): void { + $table->dropForeign('needed_angel_types_shift_type_id_foreign'); + $table->dropColumn('shift_type_id'); + }); + } +} diff --git a/db/migrations/2023_12_21_000000_add_user_edit_permission.php b/db/migrations/2023_12_21_000000_add_user_edit_permission.php new file mode 100644 index 000000000..922840bb9 --- /dev/null +++ b/db/migrations/2023_12_21_000000_add_user_edit_permission.php @@ -0,0 +1,44 @@ +schema->getConnection(); + $db->table('privileges') + ->insert([ + 'name' => 'user.edit', 'description' => 'Edit user', + ]); + + $editUser = $db->table('privileges') + ->where('name', 'user.edit') + ->get(['id']) + ->first(); + + $buerocrat = 80; + $db->table('group_privileges') + ->insertOrIgnore([ + 'group_id' => $buerocrat, 'privilege_id' => $editUser->id, + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $db = $this->schema->getConnection(); + $db->table('privileges') + ->where('name', 'user.edit') + ->delete(); + } +} diff --git a/db/migrations/2023_12_26_000000_create_schedule_locations_table.php b/db/migrations/2023_12_26_000000_create_schedule_locations_table.php new file mode 100644 index 000000000..2f5e5d3ef --- /dev/null +++ b/db/migrations/2023_12_26_000000_create_schedule_locations_table.php @@ -0,0 +1,52 @@ +schema->getConnection(); + + $this->schema->create('schedule_locations', function (Blueprint $table): void { + $table->increments('id'); + $this->references($table, 'schedules'); + $this->references($table, 'locations'); + + $table->index(['schedule_id', 'location_id']); + }); + + $scheduleLocations = $connection + ->table('schedule_shift') + ->select(['schedules.id AS schedule_id', 'locations.id AS location_id']) + ->leftJoin('schedules', 'schedules.id', 'schedule_shift.schedule_id') + ->leftJoin('shifts', 'shifts.id', 'schedule_shift.shift_id') + ->leftJoin('locations', 'locations.id', 'shifts.location_id') + ->groupBy(['schedules.id', 'locations.id']) + ->get(); + + foreach ($scheduleLocations as $scheduleLocation) { + $connection->table('schedule_locations') + ->insert((array) $scheduleLocation); + } + } + + /** + * Drops the table + */ + public function down(): void + { + $this->schema->drop('schedule_locations'); + } +} diff --git a/db/migrations/2023_12_27_000000_add_shifttypes_edit_permission_and_shifttypes_requires_shico.php b/db/migrations/2023_12_27_000000_add_shifttypes_edit_permission_and_shifttypes_requires_shico.php new file mode 100644 index 000000000..2237670bd --- /dev/null +++ b/db/migrations/2023_12_27_000000_add_shifttypes_edit_permission_and_shifttypes_requires_shico.php @@ -0,0 +1,77 @@ +db = $this->schema->getConnection(); + + $this->shifttypes = $this->db->table('privileges') + ->where('name', 'shifttypes') + ->get(['id']) + ->first()->id; + } + + /** + * Run the migration + */ + public function up(): void + { + $db = $this->schema->getConnection(); + $db->table('privileges') + ->insert([ + 'name' => 'shifttypes.edit', 'description' => 'Edit shift types', + ]); + + $editShifttypes = $db->table('privileges') + ->where('name', 'shifttypes.edit') + ->get(['id']) + ->first(); + + $this->movePermission($this->shifttypes, $this->bureaucrat, $this->shiCo); + + $db->table('group_privileges') + ->insertOrIgnore([ + 'group_id' => $this->bureaucrat, 'privilege_id' => $editShifttypes->id, + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $db = $this->schema->getConnection(); + $db->table('privileges') + ->where('name', 'shifttypes.edit') + ->delete(); + + $this->movePermission($this->shifttypes, $this->shiCo, $this->bureaucrat); + } + + protected function movePermission(int $privilege, int $oldGroup, int $newGroup): void + { + $this->db->table('group_privileges') + ->insertOrIgnore(['group_id' => $newGroup, 'privilege_id' => $privilege]); + + $this->db->table('group_privileges') + ->where(['group_id' => $oldGroup, 'privilege_id' => $privilege]) + ->delete(); + } +} diff --git a/db/migrations/Reference.php b/db/migrations/Reference.php index 7f502f3a9..0a242e9bd 100644 --- a/db/migrations/Reference.php +++ b/db/migrations/Reference.php @@ -30,7 +30,7 @@ protected function references( $table->primary($fromColumn); } - $this->addReference($table, $fromColumn, $targetTable, $targetColumn ?: 'id'); + $this->addReference($table, $fromColumn, $targetTable, $targetColumn); return $col; } diff --git a/db/update.sql b/db/update.sql deleted file mode 100644 index 56663071c..000000000 --- a/db/update.sql +++ /dev/null @@ -1,59 +0,0 @@ -INSERT INTO `Privileges` (`id`, `name`, `desc`) VALUES (40, 'view_rooms', 'User can view rooms'); -INSERT INTO `GroupPrivileges` (`id`, `group_id`, `privilege_id`) VALUES (NULL, '-2', '40'); - -ALTER TABLE `UserAngelTypes` CHANGE `coordinator` `supporter` BOOLEAN; - -ALTER TABLE `User` ADD COLUMN `email_by_human_allowed` BOOLEAN NOT NULL; - --- No Self Sign Up for some Angel Types -ALTER TABLE AngelTypes ADD no_self_signup TINYINT(1) NOT NULL; - -ALTER TABLE `AngelTypes` - ADD `contact_user_id` INT NULL, - ADD `contact_name` VARCHAR(250) NULL, - ADD `contact_dect` VARCHAR(5) NULL, - ADD `contact_email` VARCHAR(250) NULL, - ADD INDEX (`contact_user_id`); -ALTER TABLE `AngelTypes` - ADD FOREIGN KEY (`contact_user_id`) REFERENCES `User`(`UID`) ON DELETE SET NULL ON UPDATE CASCADE; - -INSERT INTO `Privileges` (`id`, `name`, `desc`) VALUES (NULL, 'shiftentry_edit_angeltype_supporter', 'If user with this privilege is angeltype supporter, he can put users in shifts for their angeltype'); - --- DB Performance -ALTER TABLE `Shifts` ADD INDEX(`start`); -ALTER TABLE `NeededAngelTypes` ADD INDEX(`count`); - --- Security -UPDATE `Groups` SET UID = UID * 10; -INSERT INTO `Groups` (Name, UID) VALUES ('News Admin', -65); -INSERT INTO `Privileges` (id, name, `desc`) VALUES (42, 'admin_news_html', 'Use HTML in news'); -INSERT INTO `GroupPrivileges` (group_id, privilege_id) VALUES (-65, 14), (-65, 42); - --- Add log level to LogEntries -ALTER TABLE `LogEntries` CHANGE COLUMN `nick` `level` VARCHAR(20) NOT NULL; - --- Angeltype contact update -ALTER TABLE `AngelTypes` DROP FOREIGN KEY angeltypes_ibfk_1; -ALTER TABLE `AngelTypes` DROP `contact_user_id`; - --- Room update -ALTER TABLE `Room` DROP `Number`; -ALTER TABLE `Room` DROP `show`; -ALTER TABLE `Room` DROP `Man`; -ALTER TABLE `Room` ADD `from_frab` BOOLEAN NOT NULL AFTER `FromPentabarf`; -UPDATE Room SET `from_frab` = (`FromPentabarf` = 'Y'); -ALTER TABLE `Room` DROP `FromPentabarf`; -ALTER TABLE `Room` ADD `map_url` VARCHAR(300) NULL AFTER `from_frab`; -ALTER TABLE `Room` ADD `description` TEXT NULL AFTER `map_url`; - --- Dashboard -ALTER TABLE `AngelTypes` ADD `show_on_dashboard` BOOLEAN NOT NULL AFTER `contact_email`; -UPDATE `AngelTypes` SET `show_on_dashboard` = TRUE; - --- Work Log -CREATE TABLE `UserWorkLog` ( `id` INT NOT NULL AUTO_INCREMENT , `user_id` INT NOT NULL , `work_hours` DECIMAL NOT NULL , `comment` VARCHAR(200) NOT NULL , `created_user_id` INT NOT NULL , `created_timestamp` INT NOT NULL , PRIMARY KEY (`id`), INDEX (`user_id`), INDEX (`created_user_id`)) ENGINE = InnoDB; -ALTER TABLE `UserWorkLog` ADD FOREIGN KEY (`created_user_id`) REFERENCES `User`(`UID`) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE `UserWorkLog` ADD FOREIGN KEY (`user_id`) REFERENCES `User`(`UID`) ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE `UserWorkLog` ADD INDEX(`created_timestamp`); -INSERT INTO `Privileges` (`id`, `name`, `desc`) VALUES (NULL, 'admin_user_worklog', 'Manage user work log entries.'); -ALTER TABLE `UserWorkLog` CHANGE `work_hours` `work_hours` DECIMAL(10,2) NOT NULL; -ALTER TABLE `UserWorkLog` ADD `work_timestamp` INT NOT NULL AFTER `user_id`; diff --git a/docker/Dockerfile b/docker/Dockerfile index 2d3385b76..58aa7c729 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,13 +5,13 @@ RUN composer --no-ansi install --no-dev --ignore-platform-reqs RUN composer --no-ansi dump-autoload --optimize # Generate .mo files -FROM alpine as translation +FROM alpine AS translation RUN apk add gettext COPY resources/lang/ /data -RUN find /data -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \; +RUN find /data -type f -name '*.po' -exec sh -c 'msgfmt "${1%.*}.po" -o"${1%.*}.mo"' shell {} \; # Build the themes -FROM node:14-alpine as themes +FROM node:20-alpine AS themes WORKDIR /app COPY .babelrc .browserslistrc package.json webpack.config.js yarn.lock /app/ RUN yarn --frozen-lockfile @@ -19,13 +19,14 @@ COPY resources/assets/ /app/resources/assets RUN yarn build # Generate application structure -FROM alpine as data +FROM alpine AS data COPY .babelrc .browserslistrc composer.json LICENSE package.json README.md webpack.config.js yarn.lock /app/ COPY bin/ /app/bin COPY config/ /app/config COPY db/ /app/db COPY includes/ /app/includes COPY public/ /app/public +COPY resources/api /app/resources/api COPY resources/views /app/resources/views COPY src/ /app/src COPY storage/ /app/storage diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 6998fc152..685cc8aa8 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -24,7 +24,7 @@ ENV TRUSTED_PROXIES 10.0.0.0/8,::ffff:10.0.0.0/8,\ # Engelsystem development workspace # Contains all tools required to build / manage the system FROM es_base AS es_workspace -RUN echo 'memory_limit = 512M' > /usr/local/etc/php/conf.d/docker-php.ini +RUN echo 'memory_limit = 1024M' > /usr/local/etc/php/conf.d/docker-php.ini RUN apk add --no-cache gettext git nodejs npm yarn COPY --from=composer:2 /usr/bin/composer /usr/bin/composer ENTRYPOINT php -r 'sleep(PHP_INT_MAX);' diff --git a/includes/controller/angeltypes_controller.php b/includes/controller/angeltypes_controller.php index bdc005720..741f8afb2 100644 --- a/includes/controller/angeltypes_controller.php +++ b/includes/controller/angeltypes_controller.php @@ -2,7 +2,7 @@ use Engelsystem\Helpers\Carbon; use Engelsystem\Models\AngelType; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\UserAngelType; use Engelsystem\ShiftsFilter; use Engelsystem\ShiftsFilterRenderer; @@ -17,7 +17,7 @@ */ function angeltypes_title() { - return __('Angeltypes'); + return __('angeltypes.angeltypes'); } /** @@ -48,7 +48,7 @@ function angeltypes_controller() function angeltype_link($angeltype_id, $params = []) { $params = array_merge(['action' => 'view', 'angeltype_id' => $angeltype_id], $params); - return page_link_to('angeltypes', $params); + return url('/angeltypes', $params); } /** @@ -59,7 +59,7 @@ function angeltype_link($angeltype_id, $params = []) function angeltype_delete_controller() { if (!auth()->can('admin_angel_types')) { - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } $angeltype = AngelType::findOrFail(request()->input('angeltype_id')); @@ -68,11 +68,11 @@ function angeltype_delete_controller() $angeltype->delete(); engelsystem_log('Deleted angeltype: ' . AngelType_name_render($angeltype, true)); success(sprintf(__('Angeltype %s deleted.'), $angeltype->name)); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } return [ - sprintf(__('Delete angeltype %s'), $angeltype->name), + sprintf(__('Delete angeltype %s'), htmlspecialchars($angeltype->name)), AngelType_delete_view($angeltype), ]; } @@ -92,14 +92,14 @@ function angeltype_edit_controller() // Edit existing angeltype $angeltype = AngelType::findOrFail($request->input('angeltype_id')); - if (!auth()->user()->isAngelTypeSupporter($angeltype) && !auth()->can('admin_user_angeltypes')) { - throw_redirect(page_link_to('angeltypes')); + if (!auth()->user()?->isAngelTypeSupporter($angeltype) && !auth()->can('admin_user_angeltypes')) { + throw_redirect(url('/angeltypes')); } } else { // New angeltype if ($supporter_mode) { // Supporters aren't allowed to create new angeltypes. - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } $angeltype = new AngelType(); } @@ -118,11 +118,13 @@ function angeltype_edit_controller() } $angeltype->restricted = $request->has('restricted'); - $angeltype->no_self_signup = $request->has('no_self_signup'); + $angeltype->shift_self_signup = $request->has('shift_self_signup'); $angeltype->show_on_dashboard = $request->has('show_on_dashboard'); $angeltype->hide_register = $request->has('hide_register'); + $angeltype->hide_on_shift_view = $request->has('hide_on_shift_view'); $angeltype->requires_driver_license = $request->has('requires_driver_license'); + $angeltype->requires_ifsg_certificate = $request->has('requires_ifsg_certificate'); } $angeltype->description = strip_request_item_nl('description', $angeltype->description); @@ -137,20 +139,22 @@ function angeltype_edit_controller() success('Angel type saved.'); engelsystem_log( 'Saved angeltype: ' . $angeltype->name . ($angeltype->restricted ? ', restricted' : '') - . ($angeltype->no_self_signup ? ', no_self_signup' : '') + . ($angeltype->shift_self_signup ? ', shift_self_signup' : '') . ($angeltype->requires_driver_license ? ', requires driver license' : '') . ', ' + . ($angeltype->requires_ifsg_certificate ? ', requires ifsg certificate' : '') . ', ' . $angeltype->contact_name . ', ' . $angeltype->contact_dect . ', ' . $angeltype->contact_email . ', ' . $angeltype->show_on_dashboard . ', ' - . $angeltype->hide_register + . $angeltype->hide_register . ', ' + . $angeltype->hide_on_shift_view ); throw_redirect(angeltype_link($angeltype->id)); } } return [ - sprintf(__('Edit %s'), $angeltype->name), + sprintf(__('Edit %s'), htmlspecialchars((string) $angeltype->name)), AngelType_edit_view($angeltype, $supporter_mode), ]; } @@ -165,14 +169,15 @@ function angeltype_controller() $user = auth()->user(); if (!auth()->can('angeltypes')) { - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } $angeltype = AngelType::findOrFail(request()->input('angeltype_id')); /** @var UserAngelType $user_angeltype */ $user_angeltype = UserAngelType::whereUserId($user->id)->where('angel_type_id', $angeltype->id)->first(); - $members = $angeltype->userAngelTypes->sortBy('name'); - + $members = $angeltype->userAngelTypes + ->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE) + ->load(['state', 'personalData', 'contact']); $days = angeltype_controller_shiftsFilterDays($angeltype); $shiftsFilter = angeltype_controller_shiftsFilter($angeltype, $days); if (request()->input('showFilledShifts')) { @@ -192,7 +197,7 @@ function angeltype_controller() $isSupporter = !is_null($user_angeltype) && $user_angeltype->supporter; return [ - sprintf(__('Team %s'), $angeltype->name), + sprintf(__('Team %s'), htmlspecialchars($angeltype->name)), AngelType_view( $angeltype, $members, @@ -221,9 +226,8 @@ function angeltype_controller_shiftsFilterDays(AngelType $angeltype) $days = []; foreach ($all_shifts as $shift) { $day = Carbon::make($shift['start'])->format('Y-m-d'); - $dayFormatted = Carbon::make($shift['start'])->format(__('Y-m-d')); if (!isset($days[$day])) { - $days[$day] = $dayFormatted; + $days[$day] = dateWithEventDay($day); } } ksort($days); @@ -240,13 +244,13 @@ function angeltype_controller_shiftsFilterDays(AngelType $angeltype) function angeltype_controller_shiftsFilter(AngelType $angeltype, $days) { $request = request(); - $roomIds = Room::query() + $locationIds = Location::query() ->select('id') ->pluck('id') ->toArray(); $shiftsFilter = new ShiftsFilter( auth()->can('user_shifts_admin'), - $roomIds, + $locationIds, [$angeltype->id] ); $selected_day = date('Y-m-d'); @@ -270,59 +274,70 @@ function angeltype_controller_shiftsFilter(AngelType $angeltype, $days) function angeltypes_list_controller() { $user = auth()->user(); + $admin_angeltypes = auth()->can('admin_angel_types'); if (!auth()->can('angeltypes')) { - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } $angeltypes = AngelTypes_with_user($user->id); foreach ($angeltypes as $angeltype) { $actions = [ button( - page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), - icon('eye') . __('view'), - 'btn-sm' + url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), + icon('eye') . ($admin_angeltypes ? '' : __('View')), + 'btn-sm btn-info', + '', + ($admin_angeltypes ? __('View') : '') ), ]; - if (auth()->can('admin_angel_types')) { + if ($admin_angeltypes) { $actions[] = button( - page_link_to('angeltypes', ['action' => 'edit', 'angeltype_id' => $angeltype->id]), - icon('pencil') . __('edit'), - 'btn-sm' + url('/angeltypes', ['action' => 'edit', 'angeltype_id' => $angeltype->id]), + icon('pencil'), + 'btn-sm', + '', + __('form.edit') ); $actions[] = button( - page_link_to('angeltypes', ['action' => 'delete', 'angeltype_id' => $angeltype->id]), - icon('trash') . __('delete'), - 'btn-sm' + url('/angeltypes', ['action' => 'delete', 'angeltype_id' => $angeltype->id]), + icon('trash'), + 'btn-sm btn-danger', + '', + __('form.delete') ); } $angeltype->membership = AngelType_render_membership($angeltype); if (!empty($angeltype->user_angel_type_id)) { $actions[] = button( - page_link_to( - 'user_angeltypes', + url( + '/user-angeltypes', ['action' => 'delete', 'user_angeltype_id' => $angeltype->user_angel_type_id] ), - icon('box-arrow-right') . __('leave'), - 'btn-sm' + icon('box-arrow-right') . ($admin_angeltypes ? '' : __('Leave')), + 'btn-sm', + '', + ($admin_angeltypes ? __('Leave') : '') ); } else { $actions[] = button( - page_link_to('user_angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]), - icon('box-arrow-in-right') . __('join'), - 'btn-sm' + url('/user_angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]), + icon('box-arrow-in-right') . ($admin_angeltypes ? '' : __('Join')), + 'btn-sm', + '', + ($admin_angeltypes ? __('Join') : '') ); } $angeltype->is_restricted = $angeltype->restricted ? icon('mortarboard-fill') : ''; - $angeltype->no_self_signup_allowed = $angeltype->no_self_signup ? '' : icon('pencil-square'); + $angeltype->shift_self_signup_allowed = $angeltype->shift_self_signup ? icon('pencil-square') : ''; $angeltype->name = '' - . $angeltype->name + . htmlspecialchars($angeltype->name) . ''; $angeltype->actions = table_buttons($actions); diff --git a/includes/controller/event_config_controller.php b/includes/controller/event_config_controller.php index 24e07df37..16322acf7 100644 --- a/includes/controller/event_config_controller.php +++ b/includes/controller/event_config_controller.php @@ -17,7 +17,7 @@ function event_config_title() function event_config_edit_controller() { if (!auth()->can('admin_event_config')) { - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } $request = request(); @@ -117,8 +117,8 @@ function event_config_edit_controller() $teardown_end_date ? $teardown_end_date->format('Y-m-d H:i') : '' ) ); - success(__('Settings saved.')); - throw_redirect(page_link_to('admin_event_config')); + success(__('settings.success')); + throw_redirect(url('/admin_event_config')); } } diff --git a/includes/controller/rooms_controller.php b/includes/controller/locations_controller.php similarity index 52% rename from includes/controller/rooms_controller.php rename to includes/controller/locations_controller.php index 2136565ab..36bb80947 100644 --- a/includes/controller/rooms_controller.php +++ b/includes/controller/locations_controller.php @@ -1,40 +1,40 @@ can('view_rooms')) { - throw_redirect(page_link_to()); + if (!auth()->can('view_locations')) { + throw_redirect(url('/')); } $request = request(); - $room = load_room(); + $location = load_location(); - $all_shifts = $room->shifts->sortBy('start'); + $all_shifts = $location->shifts->sortBy('start'); $days = []; foreach ($all_shifts as $shift) { $day = $shift->start->format('Y-m-d'); if (!isset($days[$day])) { - $days[$day] = $shift->start->format(__('Y-m-d')); + $days[$day] = dateWithEventDay($day); } } $shiftsFilter = new ShiftsFilter( true, - [$room->id], + [$location->id], AngelType::query()->get('id')->pluck('id')->toArray() ); $selected_day = date('Y-m-d'); @@ -53,17 +53,17 @@ function room_controller(): array $shiftCalendarRenderer = shiftCalendarRendererByShiftFilter($shiftsFilter); return [ - $room->name, - Room_view($room, $shiftsFilterRenderer, $shiftCalendarRenderer), + htmlspecialchars($location->name), + location_view($location, $shiftsFilterRenderer, $shiftCalendarRenderer), ]; } /** - * Dispatch different room actions. + * Dispatch different location actions. * * @return array */ -function rooms_controller(): array +function locations_controller(): array { $request = request(); $action = $request->input('action'); @@ -72,36 +72,36 @@ function rooms_controller(): array } return match ($action) { - 'view' => room_controller(), - 'list' => throw_redirect(page_link_to('admin/rooms')), - default => throw_redirect(page_link_to('admin/rooms')), + 'view' => location_controller(), + 'list' => throw_redirect(url('/admin/locations')), + default => throw_redirect(url('/admin/locations')), }; } /** - * @param Room $room + * @param Location $location * @return string */ -function room_link(Room $room) +function location_link(Location $location) { - return page_link_to('rooms', ['action' => 'view', 'room_id' => $room->id]); + return url('/locations', ['action' => 'view', 'location_id' => $location->id]); } /** - * Loads room by request param room_id + * Loads location by request param location_id * - * @return Room + * @return Location */ -function load_room() +function load_location() { - if (!test_request_int('room_id')) { - throw_redirect(page_link_to()); + if (!test_request_int('location_id')) { + throw_redirect(url('/')); } - $room = Room::find(request()->input('room_id')); - if (!$room) { - throw_redirect(page_link_to()); + $location = Location::find(request()->input('location_id')); + if (!$location) { + throw_redirect(url('/')); } - return $room; + return $location; } diff --git a/includes/controller/public_dashboard_controller.php b/includes/controller/public_dashboard_controller.php index d4f73434f..223705776 100644 --- a/includes/controller/public_dashboard_controller.php +++ b/includes/controller/public_dashboard_controller.php @@ -1,8 +1,8 @@ get('filtered')) { - $requestRooms = check_request_int_array('rooms'); + $requestLocations = check_request_int_array('locations'); $requestAngelTypes = check_request_int_array('types'); - if (!$requestRooms && !$requestAngelTypes) { + if (!$requestLocations && !$requestAngelTypes) { $sessionFilter = collect(session()->get('shifts-filter', [])); - $requestRooms = $sessionFilter->get('rooms', []); + $requestLocations = $sessionFilter->get('locations', []); $requestAngelTypes = $sessionFilter->get('types', []); } $angelTypes = collect(unrestricted_angeltypes()); - $rooms = $requestRooms ?: Room::orderBy('name')->get()->pluck('id')->toArray(); + $locations = $requestLocations ?: Location::orderBy('name')->get()->pluck('id')->toArray(); $angelTypes = $requestAngelTypes ?: $angelTypes->pluck('id')->toArray(); $filterValues = [ 'userShiftsAdmin' => false, 'filled' => [], - 'rooms' => $rooms, + 'locations' => $locations, 'types' => $angelTypes, 'startTime' => null, 'endTime' => null, @@ -57,14 +57,14 @@ function public_dashboard_controller() } } - $important_news = News::whereIsImportant(true) + $highlighted_news = News::whereIsHighlighted(true) ->orderBy('updated_at') ->limit(1) ->get(); return [ __('Public Dashboard'), - public_dashboard_view($stats, $free_shifts, $important_news), + public_dashboard_view($stats, $free_shifts, $highlighted_news), ]; } @@ -87,7 +87,7 @@ function public_dashboard_controller_free_shift(Shift $shift, ShiftsFilter $filt 'duration' => round(($shift->end->timestamp - $shift->start->timestamp) / 3600), 'shifttype_name' => $shift->shiftType->name, 'title' => $shift->title, - 'room_name' => $shift->room->name, + 'location_name' => $shift->location->name, 'needed_angels' => public_dashboard_needed_angels($shift->neededAngels, $filter), ]; @@ -136,5 +136,5 @@ function public_dashboard_needed_angels($needed_angels, ShiftsFilter $filter = n */ function public_dashboard_link(array $parameters = []): string { - return page_link_to('public-dashboard', $parameters); + return url('/public-dashboard', $parameters); } diff --git a/includes/controller/shift_entries_controller.php b/includes/controller/shift_entries_controller.php index ccb0515c9..0951a8a58 100644 --- a/includes/controller/shift_entries_controller.php +++ b/includes/controller/shift_entries_controller.php @@ -18,7 +18,7 @@ function shift_entries_controller(): array { $user = auth()->user(); if (!$user) { - throw_redirect(page_link_to('login')); + throw_redirect(url('/login')); } $action = strip_request_item('action'); @@ -44,7 +44,7 @@ function shift_entry_create_controller(): array $request = request(); if ($user->isFreeloader()) { - throw_redirect(page_link_to('user_myshifts')); + throw_redirect(url('/user_myshifts')); } $shift = Shift($request->input('shift_id')); @@ -113,17 +113,21 @@ function shift_entry_create_controller_admin(Shift $shift, ?AngelType $angeltype } /** @var User[]|Collection $users */ - $users = User::query()->orderBy('name')->get(); + $users = User::with('userAngelTypes')->orderBy('name')->get(); $users_select = []; foreach ($users as $user) { - $users_select[$user->id] = $user->displayName; + $name = $user->displayName; + if ($user->userAngelTypes->where('id', $angeltype->id)->isEmpty()) { + $name = __('%s (not "%s")', [$name, $angeltype->name]); + } + $users_select[$user->id] = $name; } $angeltypes_select = $angeltypes->pluck('name', 'id')->toArray(); - $room = $shift->room; + $location = $shift->location; return [ ShiftEntry_create_title(), - ShiftEntry_create_view_admin($shift, $room, $angeltype, $angeltypes_select, $signup_user, $users_select), + ShiftEntry_create_view_admin($shift, $location, $angeltype, $angeltypes_select, $signup_user, $users_select), ]; } @@ -167,10 +171,10 @@ function shift_entry_create_controller_supporter(Shift $shift, AngelType $angelt $users_select[$u->id] = $u->displayName; } - $room = $shift->room; + $location = $shift->location; return [ ShiftEntry_create_title(), - ShiftEntry_create_view_supporter($shift, $room, $angeltype, $signup_user, $users_select), + ShiftEntry_create_view_supporter($shift, $location, $angeltype, $signup_user, $users_select), ]; } @@ -206,7 +210,7 @@ function shift_entry_create_controller_user(Shift $shift, AngelType $angeltype): $request = request(); $signup_user = auth()->user(); - $needed_angeltype = (new AngelType())->forceFill(NeededAngeltype_by_Shift_and_Angeltype($shift, $angeltype)); + $needed_angeltype = (new AngelType())->forceFill(NeededAngeltype_by_Shift_and_Angeltype($shift, $angeltype) ?: []); $shift_entries = $shift->shiftEntries() ->where('angel_type_id', $angeltype->id) ->get(); @@ -250,10 +254,10 @@ function shift_entry_create_controller_user(Shift $shift, AngelType $angeltype): throw_redirect(shift_link($shift)); } - $room = $shift->room; + $location = $shift->location; return [ ShiftEntry_create_title(), - ShiftEntry_create_view_user($shift, $room, $angeltype, $comment), + ShiftEntry_create_view_user($shift, $location, $angeltype, $comment), ]; } @@ -272,7 +276,7 @@ function shift_entry_create_link(Shift $shift, AngelType $angeltype, $params = [ 'shift_id' => $shift->id, 'angeltype_id' => $angeltype->id, ], $params); - return page_link_to('shift_entries', $params); + return url('/shift-entries', $params); } /** @@ -288,7 +292,7 @@ function shift_entry_create_link_admin(Shift $shift, $params = []) 'action' => 'create', 'shift_id' => $shift->id, ], $params); - return page_link_to('shift_entries', $params); + return url('/shift-entries', $params); } /** @@ -301,7 +305,7 @@ function shift_entry_load() $request = request(); if (!$request->has('shift_entry_id') || !test_request_int('shift_entry_id')) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } $shiftEntry = ShiftEntry::findOrFail($request->input('shift_entry_id')); @@ -362,5 +366,5 @@ function shift_entry_delete_link($shiftEntry, $params = []) 'action' => 'delete', 'shift_entry_id' => $shiftEntry['shift_entry_id'] ?? $shiftEntry['id'], ], $params); - return page_link_to('shift_entries', $params); + return url('/shift-entries', $params); } diff --git a/includes/controller/shifts_controller.php b/includes/controller/shifts_controller.php index fe3b8939b..c552f91b2 100644 --- a/includes/controller/shifts_controller.php +++ b/includes/controller/shifts_controller.php @@ -1,8 +1,8 @@ $shift->id]); + return url('/user-shifts', ['delete_shift' => $shift->id]); } /** @@ -38,7 +38,7 @@ function shift_delete_link(Shift $shift) */ function shift_edit_link(Shift $shift) { - return page_link_to('user_shifts', ['edit_shift' => $shift->id]); + return url('/user-shifts', ['edit_shift' => $shift->id]); } /** @@ -52,11 +52,11 @@ function shift_edit_controller() $request = request(); if (!auth()->can('admin_shifts')) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } if (!$request->has('edit_shift') || !test_request_int('edit_shift')) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } $shift_id = $request->input('edit_shift'); @@ -67,14 +67,14 @@ function shift_edit_controller() )); } - $rooms = []; - foreach (Room::orderBy('name')->get() as $room) { - $rooms[$room->id] = $room->name; + $locations = []; + foreach (Location::orderBy('name')->get() as $location) { + $locations[$location->id] = $location->name; } $angeltypes = AngelType::all()->pluck('name', 'id')->toArray(); $shifttypes = ShiftType::all()->pluck('name', 'id')->toArray(); - $needed_angel_types = collect(NeededAngelTypes_by_shift($shift_id))->pluck('count', 'angel_type_id')->toArray(); + $needed_angel_types = collect(NeededAngelTypes_by_shift($shift))->pluck('count', 'angel_type_id')->toArray(); foreach (array_keys($angeltypes) as $angeltype_id) { if (!isset($needed_angel_types[$angeltype_id])) { $needed_angel_types[$angeltype_id] = 0; @@ -84,7 +84,7 @@ function shift_edit_controller() $shifttype_id = $shift->shift_type_id; $title = $shift->title; $description = $shift->description; - $rid = $shift->room_id; + $rid = $shift->location_id; $start = $shift->start; $end = $shift->end; @@ -97,12 +97,12 @@ function shift_edit_controller() if ( $request->has('rid') && preg_match('/^\d+$/', $request->input('rid')) - && isset($rooms[$request->input('rid')]) + && isset($locations[$request->input('rid')]) ) { $rid = $request->input('rid'); } else { $valid = false; - error(__('Please select a room.')); + error(__('Please select a location.')); } if ($request->has('shifttype_id') && isset($shifttypes[$request->input('shifttype_id')])) { @@ -154,13 +154,16 @@ function shift_edit_controller() $shift->shift_type_id = $shifttype_id; $shift->title = $title; $shift->description = $description; - $shift->room_id = $rid; + $shift->location_id = $rid; $shift->start = $start; $shift->end = $end; $shift->updatedBy()->associate(auth()->user()); $shift->save(); - mail_shift_change($oldShift, $shift); + event('shift.updating', [ + 'shift' => $shift, + 'oldShift' => $oldShift, + ]); NeededAngelType::whereShiftId($shift_id)->delete(); $needed_angel_types_info = []; @@ -194,13 +197,16 @@ function shift_edit_controller() foreach ($angeltypes as $angeltype_id => $angeltype_name) { $angel_types_spinner .= form_spinner( 'angeltype_count_' . $angeltype_id, - $angeltype_name, - $needed_angel_types[$angeltype_id] + htmlspecialchars($angeltype_name), + $needed_angel_types[$angeltype_id], + [], + ScheduleShift::whereShiftId($shift->id)->first() ? true : false, ); } + $link = button(url('/shifts', ['action' => 'view', 'shift_id' => $shift_id]), icon('chevron-left'), 'btn-sm', '', __('general.back')); return page_with_title( - shifts_title(), + $link . ' ' . shifts_title(), [ msg(), '', form([ form_select('shifttype_id', __('Shifttype'), $shifttypes, $shifttype_id), - form_text('title', __('Title'), $title), - form_select('rid', __('Room:'), $rooms, $rid), + form_text('title', __('title.title'), $title), + form_select('rid', __('Location:'), $locations, $rid), form_text('start', __('Start:'), $start->format('Y-m-d H:i')), form_text('end', __('End:'), $end->format('Y-m-d H:i')), form_textarea('description', __('Additional description'), $description), - form_info('', __('This description is for single shifts, otherwise please use the description in shift type.')), + form_info( + '', + __('This description is for single shifts, otherwise please use the description in shift type.') + ), '

' . __('Needed angels') . '

', $angel_types_spinner, - form_submit('submit', __('Save')), + form_submit('submit', icon('save') . __('form.save')), ]), ] ); @@ -230,18 +239,18 @@ function shift_delete_controller() $request = request(); if (!auth()->can('user_shifts_admin')) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } // Schicht komplett lΓΆschen (nur fΓΌr admins/user mit user_shifts_admin privileg) if (!$request->has('delete_shift') || !preg_match('/^\d+$/', $request->input('delete_shift'))) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } $shift_id = $request->input('delete_shift'); $shift = Shift($shift_id); if (empty($shift)) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } // Schicht lΓΆschen bestΓ€tigt @@ -254,7 +263,7 @@ function shift_delete_controller() 'name' => $shift->shiftType->name, 'title' => $shift->title, 'type' => $entry->angelType->name, - 'room' => $shift->room, + 'location' => $shift->location, 'freeloaded' => $entry->freeloaded, ]); } @@ -267,21 +276,25 @@ function shift_delete_controller() . ' to ' . $shift->end->format('Y-m-d H:i') ); success(__('Shift deleted.')); - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } - return page_with_title(shifts_title(), [ - error(sprintf( - __('Do you want to delete the shift %s from %s to %s?'), - $shift->shiftType->name, - $shift->start->format(__('Y-m-d H:i')), - $shift->end->format(__('H:i')) - ), true), - form([ - form_hidden('delete_shift', $shift->id), - form_submit('delete', __('delete')), - ]), - ]); + $link = button(url('/shifts', ['action' => 'view', 'shift_id' => $shift_id]), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title( + $link . ' ' . shifts_title(), + [ + error(sprintf( + __('Do you want to delete the shift %s from %s to %s?'), + $shift->shiftType->name, + $shift->start->format(__('general.datetime')), + $shift->end->format(__('H:i')) + ), true), + form([ + form_hidden('delete_shift', $shift->id), + form_submit('delete', icon('trash') . __('form.delete'), '', true, 'danger'), + ]), + ] + ); } /** @@ -293,21 +306,21 @@ function shift_controller() $request = request(); if (!auth()->can('user_shifts')) { - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } if (!$request->has('shift_id')) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } $shift = Shift($request->input('shift_id')); if (empty($shift)) { error(__('Shift could not be found.')); - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } $shifttype = $shift->shiftType; - $room = $shift->room; + $location = $shift->location; /** @var AngelType[] $angeltypes */ $angeltypes = AngelType::all(); $user_shifts = Shifts_by_user($user->id); @@ -338,8 +351,8 @@ function shift_controller() } return [ - $shift->shiftType->name, - Shift_view($shift, $shifttype, $room, $angeltypes, $shift_signup_state), + htmlspecialchars($shift->shiftType->name), + Shift_view($shift, $shifttype, $location, $angeltypes, $shift_signup_state), ]; } @@ -350,13 +363,13 @@ function shifts_controller() { $request = request(); if (!$request->has('action')) { - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } return match ($request->input('action')) { 'view' => shift_controller(), 'next' => shift_next_controller(), // throws redirect - default => throw_redirect(page_link_to('/')), + default => throw_redirect(url('/')), }; } @@ -366,7 +379,7 @@ function shifts_controller() function shift_next_controller() { if (!auth()->can('user_shifts')) { - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } $upcoming_shifts = ShiftEntries_upcoming_for_user(auth()->user()); @@ -375,5 +388,5 @@ function shift_next_controller() throw_redirect(shift_link($upcoming_shifts[0]->shift)); } - throw_redirect(page_link_to('user_shifts')); + throw_redirect(url('/user-shifts')); } diff --git a/includes/controller/shifttypes_controller.php b/includes/controller/shifttypes_controller.php deleted file mode 100644 index 800034fce..000000000 --- a/includes/controller/shifttypes_controller.php +++ /dev/null @@ -1,163 +0,0 @@ - 'view', 'shifttype_id' => $shifttype->id]); -} - -/** - * Delete a shifttype. - * - * @return array - */ -function shifttype_delete_controller() -{ - $request = request(); - if (!$request->has('shifttype_id')) { - throw_redirect(page_link_to('shifttypes')); - } - - $shifttype = ShiftType::findOrFail($request->input('shifttype_id')); - if ($request->hasPostData('delete')) { - engelsystem_log('Deleted shifttype ' . $shifttype->name); - success(sprintf(__('Shifttype %s deleted.'), $shifttype->name)); - - $shifttype->delete(); - throw_redirect(page_link_to('shifttypes')); - } - - return [ - sprintf(__('Delete shifttype %s'), $shifttype->name), - ShiftType_delete_view($shifttype), - ]; -} - -/** - * Edit or create shift type. - * - * @return array - */ -function shifttype_edit_controller() -{ - $shifttype_id = null; - $name = ''; - $description = ''; - - $request = request(); - - if ($request->has('shifttype_id')) { - $shifttype = ShiftType::findOrFail($request->input('shifttype_id')); - $shifttype_id = $shifttype->id; - $name = $shifttype->name; - $description = $shifttype->description; - } - - if ($request->hasPostData('submit')) { - $valid = true; - - if ($request->has('name') && $request->input('name') != '') { - $name = strip_request_item('name'); - } else { - $valid = false; - error(__('Please enter a name.')); - } - - if ($request->has('description')) { - $description = strip_request_item_nl('description'); - } - - if ($valid) { - $shiftType = ShiftType::findOrNew($shifttype_id); - $shiftType->name = $name; - $shiftType->description = $description; - $shiftType->save(); - - if ($shifttype_id) { - engelsystem_log('Updated shifttype ' . $name); - success(__('Updated shifttype.')); - } else { - $shifttype_id = $shiftType->id; - - engelsystem_log('Created shifttype ' . $name); - success(__('Created shifttype.')); - } - - throw_redirect(page_link_to('shifttypes', ['action' => 'view', 'shifttype_id' => $shifttype_id])); - } - } - - return [ - shifttypes_title(), - ShiftType_edit_view($name, $description, $shifttype_id), - ]; -} - -/** - * @return array - */ -function shifttype_controller() -{ - $request = request(); - if (!$request->has('shifttype_id')) { - throw_redirect(page_link_to('shifttypes')); - } - $shifttype = ShiftType::findOrFail($request->input('shifttype_id')); - - return [ - $shifttype->name, - ShiftType_view($shifttype), - ]; -} - -/** - * List all shift types. - * - * @return array - */ -function shifttypes_list_controller() -{ - $shifttypes = ShiftType::all(); - - return [ - shifttypes_title(), - ShiftTypes_list_view($shifttypes), - ]; -} - -/** - * Text for shift type related links. - * - * @return string - */ -function shifttypes_title() -{ - return __('Shifttypes'); -} - -/** - * Route shift type actions - * - * @return array - */ -function shifttypes_controller() -{ - $request = request(); - $action = 'list'; - if ($request->has('action')) { - $action = $request->input('action'); - } - - return match ($action) { - 'view' => shifttype_controller(), - 'edit' => shifttype_edit_controller(), - 'delete' => shifttype_delete_controller(), - 'list' => shifttypes_list_controller(), - default => shifttypes_list_controller(), - }; -} diff --git a/includes/controller/user_angeltypes_controller.php b/includes/controller/user_angeltypes_controller.php index d0ab2499f..d37398681 100644 --- a/includes/controller/user_angeltypes_controller.php +++ b/includes/controller/user_angeltypes_controller.php @@ -5,8 +5,6 @@ use Engelsystem\Models\User\User; use Engelsystem\Models\UserAngelType; use Illuminate\Database\Eloquent\Collection; -use Psr\Log\LoggerInterface; -use Symfony\Component\Mailer\Exception\TransportException; /** * Display a hint for team/angeltype supporters if there are unconfirmed users for his angeltype. @@ -37,9 +35,9 @@ function user_angeltypes_unconfirmed_hint() $unconfirmed_links = []; foreach ($unconfirmed_user_angeltypes as $user_angeltype) { - $unconfirmed_links[] = '' . $user_angeltype->angelType->name + $unconfirmed_links[] = '' . htmlspecialchars($user_angeltype->angelType->name) . ' (+' . $user_angeltype->count . ')' . ''; } @@ -67,13 +65,13 @@ function user_angeltypes_delete_all_controller(): array if (!$request->has('angeltype_id')) { error(__('Angeltype doesn\'t exist.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } $angeltype = AngelType::findOrFail($request->input('angeltype_id')); if (!auth()->user()->isAngelTypeSupporter($angeltype) && !auth()->can('admin_user_angeltypes')) { error(__('You are not allowed to delete all users for this angeltype.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } if ($request->hasPostData('deny_all')) { @@ -83,7 +81,7 @@ function user_angeltypes_delete_all_controller(): array engelsystem_log(sprintf('Denied all users for angeltype %s', AngelType_name_render($angeltype, true))); success(sprintf(__('Denied all users for angeltype %s.'), $angeltype->name)); - throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); + throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); } return [ @@ -104,13 +102,13 @@ function user_angeltypes_confirm_all_controller(): array if (!$request->has('angeltype_id')) { error(__('Angeltype doesn\'t exist.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } $angeltype = AngelType::findOrFail($request->input('angeltype_id')); if (!auth()->can('admin_user_angeltypes') && !$user->isAngelTypeSupporter($angeltype)) { error(__('You are not allowed to confirm all users for this angeltype.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } if ($request->hasPostData('confirm_all')) { @@ -127,7 +125,7 @@ function user_angeltypes_confirm_all_controller(): array user_angeltype_confirm_email($user, $angeltype); } - throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); + throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); } return [ @@ -148,7 +146,7 @@ function user_angeltype_confirm_controller(): array if (!$request->has('user_angeltype_id')) { error(__('User angeltype doesn\'t exist.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } /** @var UserAngelType $user_angeltype */ @@ -156,7 +154,7 @@ function user_angeltype_confirm_controller(): array $angeltype = $user_angeltype->angelType; if (!$user->isAngelTypeSupporter($angeltype) && !auth()->can('admin_user_angeltypes')) { error(__('You are not allowed to confirm this users angeltype.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } $user_source = $user_angeltype->user; @@ -173,7 +171,7 @@ function user_angeltype_confirm_controller(): array user_angeltype_confirm_email($user_source, $angeltype); - throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); + throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); } return [ @@ -188,23 +186,14 @@ function user_angeltype_confirm_email(User $user, AngelType $angeltype): void return; } - try { - /** @var EngelsystemMailer $mailer */ - $mailer = app(EngelsystemMailer::class); - $mailer->sendViewTranslated( - $user, - 'notification.angeltype.confirmed', - 'emails/angeltype-confirmed', - ['name' => $angeltype->name, 'angeltype' => $angeltype, 'username' => $user->displayName] - ); - } catch (TransportException $e) { - /** @var LoggerInterface $logger */ - $logger = app('logger'); - $logger->error( - 'Unable to send email "{title}" to user {user} with {exception}', - ['title' => __('notification.angeltype.confirmed'), 'user' => $user->name, 'exception' => $e] - ); - } + /** @var EngelsystemMailer $mailer */ + $mailer = app(EngelsystemMailer::class); + $mailer->sendViewTranslated( + $user, + 'notification.angeltype.confirmed', + 'emails/angeltype-confirmed', + ['name' => $angeltype->name, 'angeltype' => $angeltype, 'username' => $user->displayName] + ); } function user_angeltype_add_email(User $user, AngelType $angeltype): void @@ -213,23 +202,14 @@ function user_angeltype_add_email(User $user, AngelType $angeltype): void return; } - try { - /** @var EngelsystemMailer $mailer */ - $mailer = app(EngelsystemMailer::class); - $mailer->sendViewTranslated( - $user, - 'notification.angeltype.added', - 'emails/angeltype-added', - ['name' => $angeltype->name, 'angeltype' => $angeltype, 'username' => $user->displayName] - ); - } catch (TransportException $e) { - /** @var LoggerInterface $logger */ - $logger = app('logger'); - $logger->error( - 'Unable to send email "{title}" to user {user} with {exception}', - ['title' => __('notification.angeltype.added'), 'user' => $user->name, 'exception' => $e] - ); - } + /** @var EngelsystemMailer $mailer */ + $mailer = app(EngelsystemMailer::class); + $mailer->sendViewTranslated( + $user, + 'notification.angeltype.added', + 'emails/angeltype-added', + ['name' => $angeltype->name, 'angeltype' => $angeltype, 'username' => $user->displayName] + ); } /** @@ -244,7 +224,7 @@ function user_angeltype_delete_controller(): array if (!$request->has('user_angeltype_id')) { error(__('User angeltype doesn\'t exist.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } /** @var UserAngelType $user_angeltype */ @@ -257,7 +237,7 @@ function user_angeltype_delete_controller(): array && !auth()->can('admin_user_angeltypes') ) { error(__('You are not allowed to delete this users angeltype.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } if ($request->hasPostData('delete')) { @@ -266,7 +246,7 @@ function user_angeltype_delete_controller(): array engelsystem_log(sprintf('User %s removed from %s.', User_Nick_render($user_source, true), $angeltype->name)); success(sprintf(__('User %s removed from %s.'), $user_source->displayName, $angeltype->name)); - throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); + throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); } return [ @@ -287,19 +267,19 @@ function user_angeltype_update_controller(): array if (!auth()->can('admin_angel_types')) { error(__('You are not allowed to set supporter rights.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } if (!$request->has('user_angeltype_id')) { error(__('User angeltype doesn\'t exist.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } if ($request->has('supporter') && preg_match('/^[01]$/', $request->input('supporter'))) { $supporter = $request->input('supporter') == '1'; } else { error(__('No supporter update given.')); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } /** @var UserAngelType $user_angeltype */ @@ -321,7 +301,7 @@ function user_angeltype_update_controller(): array )); success(sprintf($msg, $angeltype->name, $user_source->displayName)); - throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); + throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); } return [ @@ -382,7 +362,7 @@ function user_angeltype_add_controller(): array user_angeltype_add_email($user_source, $angeltype); - throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); + throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); } } @@ -406,7 +386,7 @@ function user_angeltype_join_controller(AngelType $angeltype) $user_angeltype = UserAngelType::whereUserId($user->id)->where('angel_type_id', $angeltype->id)->first(); if (!empty($user_angeltype)) { error(sprintf(__('You are already a %s.'), $angeltype->name)); - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } $request = request(); @@ -434,11 +414,11 @@ function user_angeltype_join_controller(AngelType $angeltype) )); } - throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); + throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); } return [ - sprintf(__('Become a %s'), $angeltype->name), + sprintf(__('Become a %s'), htmlspecialchars($angeltype->name)), UserAngelType_join_view($user, $angeltype), ]; } @@ -452,7 +432,7 @@ function user_angeltypes_controller(): array { $request = request(); if (!$request->has('action')) { - throw_redirect(page_link_to('angeltypes')); + throw_redirect(url('/angeltypes')); } return match ($request->input('action')) { @@ -462,6 +442,6 @@ function user_angeltypes_controller(): array 'delete' => user_angeltype_delete_controller(), 'update' => user_angeltype_update_controller(), 'add' => user_angeltype_add_controller(), - default => throw_redirect(page_link_to('angeltyps')), + default => throw_redirect(url('/angeltyps')), }; } diff --git a/includes/controller/user_driver_licenses_controller.php b/includes/controller/user_driver_licenses_controller.php deleted file mode 100644 index d19776561..000000000 --- a/includes/controller/user_driver_licenses_controller.php +++ /dev/null @@ -1,143 +0,0 @@ -user(); - - // User has already entered data, no hint needed. - if ($user->license->wantsToDrive()) { - return null; - } - - $angeltypes = $user->userAngelTypes; - foreach ($angeltypes as $angeltype) { - if ($angeltype->requires_driver_license) { - return sprintf( - __('You joined an angeltype which requires a driving license. Please edit your driving license information here: %s.'), - '' . __('driving license information') . '' - ); - } - } - - return null; -} - -/** - * Route user driver licenses actions. - * - * @return array - */ -function user_driver_licenses_controller() -{ - $user = auth()->user(); - - if (!$user) { - throw_redirect(page_link_to()); - } - - $action = strip_request_item('action', 'edit'); - - return match ($action) { - 'edit' => user_driver_license_edit_controller(), - default => user_driver_license_edit_controller(), - }; -} - -/** - * Link to user driver license edit page for given user. - * - * @param User $user - * @return string - */ -function user_driver_license_edit_link($user = null) -{ - if (!$user) { - return page_link_to('user_driver_licenses'); - } - - return page_link_to('user_driver_licenses', ['user_id' => $user->id]); -} - -/** - * Loads the user for the driver license. - * - * @return User - */ -function user_driver_license_load_user() -{ - $request = request(); - $user_source = auth()->user(); - - if ($request->has('user_id')) { - $user_source = User::find($request->input('user_id')); - if (empty($user_source)) { - throw_redirect(user_driver_license_edit_link()); - } - } - - return $user_source; -} - -/** - * Edit a users driver license information. - * - * @return array - */ -function user_driver_license_edit_controller() -{ - $user = auth()->user(); - $request = request(); - $user_source = user_driver_license_load_user(); - - // only privilege admin_user can edit other users driver license information - if ($user->id != $user_source->id && !auth()->can('admin_user')) { - throw_redirect(user_driver_license_edit_link()); - } - - $driverLicense = $user_source->license; - if ($request->hasPostData('submit')) { - if ($request->has('wants_to_drive')) { - $driverLicense->has_car = $request->has('has_car'); - $driverLicense->drive_car = $request->has('has_license_car'); - $driverLicense->drive_3_5t = $request->has('has_license_3_5t_transporter'); - $driverLicense->drive_7_5t = $request->has('has_license_7_5t_truck'); - $driverLicense->drive_12t = $request->has('has_license_12t_truck'); - $driverLicense->drive_forklift = $request->has('has_license_forklift'); - - if ($driverLicense->wantsToDrive()) { - $driverLicense->save(); - - engelsystem_log('Driver license information updated.'); - success(__('Your driver license information has been saved.')); - throw_redirect(user_link($user_source->id)); - } else { - error(__('Please select at least one driving license.')); - } - } else { - $driverLicense->has_car = false; - $driverLicense->drive_car = false; - $driverLicense->drive_3_5t = false; - $driverLicense->drive_7_5t = false; - $driverLicense->drive_12t = false; - $driverLicense->drive_forklift = false; - $driverLicense->save(); - - engelsystem_log('Driver license information removed.'); - success(__('Your driver license information has been removed.')); - throw_redirect(user_link($user_source->id)); - } - } - - return [ - sprintf(__('Edit %s driving license information'), $user_source->displayName), - UserDriverLicense_edit_view($user_source, $driverLicense), - ]; -} diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php index 5ed7a7e6e..91b311143 100644 --- a/includes/controller/users_controller.php +++ b/includes/controller/users_controller.php @@ -7,6 +7,7 @@ use Engelsystem\ShiftCalendarRenderer; use Engelsystem\ShiftsFilter; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Str; /** @@ -20,7 +21,7 @@ function users_controller() $request = request(); if (!$user) { - throw_redirect(page_link_to()); + throw_redirect(url('/')); } $action = 'list'; @@ -55,7 +56,7 @@ function user_delete_controller() } if (!auth()->can('admin_user')) { - throw_redirect(page_link_to()); + throw_redirect(url('/')); } // You cannot delete yourself @@ -91,7 +92,7 @@ function user_delete_controller() } return [ - sprintf(__('Delete %s'), $user_source->displayName), + sprintf(__('Delete %s'), htmlspecialchars($user_source->displayName)), User_delete_view($user_source), ]; } @@ -101,7 +102,7 @@ function user_delete_controller() */ function users_link() { - return page_link_to('users'); + return url('/users'); } /** @@ -110,7 +111,7 @@ function users_link() */ function user_edit_link($userId) { - return page_link_to('admin_user', ['user_id' => $userId]); + return url('/admin-user', ['user_id' => $userId]); } /** @@ -119,7 +120,7 @@ function user_edit_link($userId) */ function user_delete_link($userId) { - return page_link_to('users', ['action' => 'delete', 'user_id' => $userId]); + return url('/users', ['action' => 'delete', 'user_id' => $userId]); } /** @@ -128,7 +129,7 @@ function user_delete_link($userId) */ function user_link($userId) { - return page_link_to('users', ['action' => 'view', 'user_id' => $userId]); + return url('/users', ['action' => 'view', 'user_id' => $userId]); } /** @@ -149,7 +150,7 @@ function user_edit_vouchers_controller() (!auth()->can('admin_user') && !auth()->can('voucher.edit')) || !config('enable_voucher') ) { - throw_redirect(page_link_to()); + throw_redirect(url('/')); } if ($request->hasPostData('submit')) { @@ -182,7 +183,7 @@ function user_edit_vouchers_controller() } return [ - sprintf(__('%s\'s vouchers'), $user_source->displayName), + sprintf(__('%s\'s vouchers'), htmlspecialchars($user_source->displayName)), User_edit_vouchers_view($user_source), ]; } @@ -200,7 +201,7 @@ function user_controller() $user_source = User::find($request->input('user_id')); if (!$user_source) { error(__('User not found.')); - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } } @@ -234,7 +235,7 @@ function user_controller() } if (empty($user_source->api_key)) { - User_reset_api_key($user_source, false); + auth()->resetApiKey($user_source); } if ($user_source->state->force_active) { @@ -243,8 +244,12 @@ function user_controller() $tshirt_score = sprintf('%.2f', User_tshirt_score($user_source->id)) . ' h'; } + $worklogs = $user_source->worklogs() + ->with(['user', 'creator']) + ->get(); + return [ - $user_source->displayName, + htmlspecialchars($user_source->displayName), User_view( $user_source, auth()->can('admin_user'), @@ -256,7 +261,7 @@ function user_controller() $tshirt_score, auth()->can('admin_active'), auth()->can('admin_user_worklog'), - UserWorkLogsForUser($user_source->id) + $worklogs ), ]; } @@ -271,7 +276,7 @@ function users_list_controller() $request = request(); if (!auth()->can('admin_user')) { - throw_redirect(page_link_to()); + throw_redirect(url('/')); } $order_by = 'name'; @@ -297,13 +302,15 @@ function users_list_controller() } /** @var User[]|Collection $users */ - $users = User::with(['contact', 'personalData', 'state']) + $users = User::with(['contact', 'personalData', 'state', 'shiftEntries' => function (HasMany $query) { + $query->where('freeloaded', true); + }]) ->orderBy('name') ->get(); foreach ($users as $user) { $user->setAttribute( 'freeloads', - $user->shiftEntries() + $user->shiftEntries ->where('freeloaded', true) ->count() ); @@ -343,13 +350,13 @@ function load_user() { $request = request(); if (!$request->has('user_id')) { - throw_redirect(page_link_to()); + throw_redirect(url('/')); } $user = User::find($request->input('user_id')); if (!$user) { error(__('User doesn\'t exist.')); - throw_redirect(page_link_to()); + throw_redirect(url('/')); } return $user; @@ -437,3 +444,57 @@ function shiftCalendarRendererByShiftFilter(ShiftsFilter $shiftsFilter) return new ShiftCalendarRenderer($filtered_shifts, $needed_angeltypes, $shift_entries, $shiftsFilter); } + +/** + * Generates a hint, if user joined angeltypes that require a driving license and the user has no driver license + * information provided. + * + * @return string|null + */ +function user_driver_license_required_hint() +{ + $user = auth()->user(); + + // User has already entered data, no hint needed. + if ($user->license->wantsToDrive()) { + return null; + } + + $angeltypes = $user->userAngelTypes; + foreach ($angeltypes as $angeltype) { + if ($angeltype->requires_driver_license) { + return sprintf( + __('angeltype.driving_license.required.info.here'), + '' . __('driving_license.info') . '' + ); + } + } + + return null; +} + +function user_ifsg_certificate_required_hint() +{ + $user = auth()->user(); + + // User has already entered data, no hint needed. + if (!config('ifsg_enabled') || $user->license->ifsg_light || $user->license->ifsg) { + return null; + } + + $angeltypes = $user->userAngelTypes; + foreach ($angeltypes as $angeltype) { + if ( + $angeltype->requires_ifsg_certificate && !( + $user->license->ifsg_certificate || $user->license->ifsg_certificate_light + ) + ) { + return sprintf( + __('angeltype.ifsg.required.info.here'), + '' . __('ifsg.info') . '' + ); + } + } + + return null; +} diff --git a/includes/engelsystem.php b/includes/engelsystem.php index b156c68a7..0c368dc81 100644 --- a/includes/engelsystem.php +++ b/includes/engelsystem.php @@ -23,7 +23,7 @@ http_response_code(503); $url = $app->get(UrlGeneratorInterface::class); $maintenance = file_get_contents(__DIR__ . '/../resources/views/layouts/maintenance.html'); - $maintenance = str_replace('%APP_NAME%', $app->get('config')->get('app_name'), $maintenance); + $maintenance = str_replace('%APP_NAME%', htmlspecialchars($app->get('config')->get('app_name')), $maintenance); $maintenance = str_replace('%ASSETS_PATH%', $url->to(''), $maintenance); echo $maintenance; die(); diff --git a/includes/helper/email_helper.php b/includes/helper/email_helper.php index fb3401077..b5ef0186e 100644 --- a/includes/helper/email_helper.php +++ b/includes/helper/email_helper.php @@ -1,9 +1,7 @@ get('translator'); - $locale = $translator->getLocale(); - - $status = true; - try { - /** @var EngelsystemMailer $mailer */ - $mailer = app('mailer'); - - $translator->setLocale($recipientUser->settings->language); - $mailer->sendView( - $recipientUser->contact->email ?: $recipientUser->email, - $title, - 'emails/mail', - ['username' => $recipientUser->displayName, 'message' => $message] - ); - } catch (Exception $e) { - $status = false; - engelsystem_log(sprintf( - 'An exception occurred while sending a mail to %s in %s:%u: %s', - $recipientUser->name, - $e->getFile(), - $e->getLine(), - $e->getMessage() - ), LogLevel::CRITICAL); - } - - $translator->setLocale($locale); + /** @var EngelsystemMailer $mailer */ + $mailer = app('mailer'); + $status = $mailer->sendViewTranslated( + $recipientUser, + $title, + 'emails/mail', + ['username' => $recipientUser->displayName, 'message' => $message] + ); if (!$status) { - error(sprintf(__('User %s could not be notified by email due to an error.'), $recipientUser->displayName)); - engelsystem_log(sprintf('User %s could not be notified by email due to an error.', $recipientUser->name)); + error(sprintf(__('User %s could not be notified by e-mail due to an error.'), $recipientUser->displayName)); + engelsystem_log(sprintf('User %s could not be notified by e-mail due to an error.', $recipientUser->name)); } return $status; diff --git a/includes/helper/legacy_helper.php b/includes/helper/legacy_helper.php index 8cfeb5c47..220a6268d 100644 --- a/includes/helper/legacy_helper.php +++ b/includes/helper/legacy_helper.php @@ -3,6 +3,8 @@ declare(strict_types=1); use Engelsystem\Renderer\Twig\Extensions\Globals; +use Engelsystem\Helpers\Carbon; +use Engelsystem\Helpers\DayOfEvent; function theme_id(): int { @@ -25,3 +27,16 @@ function theme_type(): string { return theme()['type']; } + +function dateWithEventDay(string $day): string +{ + $date = Carbon::createFromFormat('Y-m-d', $day); + $dayOfEvent = DayOfEvent::get($date); + $dateFormatted = $date->format(__('general.date')); + + if (!config('enable_show_day_of_event') || is_null($dayOfEvent)) { + return $dateFormatted; + } + + return $dateFormatted . ' (' . $dayOfEvent . ')'; +} diff --git a/includes/helper/message_helper.php b/includes/helper/message_helper.php index 13526a630..d550fd44a 100644 --- a/includes/helper/message_helper.php +++ b/includes/helper/message_helper.php @@ -17,11 +17,12 @@ function msg() * * @param string $msg * @param bool $immediately + * @param bool $immediatelyRaw * @return string */ -function info($msg, $immediately = false) +function info($msg, $immediately = false, $immediatelyRaw = false) { - return alert(NotificationType::INFORMATION, $msg, $immediately); + return alert(NotificationType::INFORMATION, $msg, $immediately, $immediatelyRaw); } /** @@ -29,11 +30,12 @@ function info($msg, $immediately = false) * * @param string $msg * @param bool $immediately + * @param bool $immediatelyRaw * @return string */ -function warning($msg, $immediately = false) +function warning($msg, $immediately = false, $immediatelyRaw = false) { - return alert(NotificationType::WARNING, $msg, $immediately); + return alert(NotificationType::WARNING, $msg, $immediately, $immediatelyRaw); } /** @@ -41,11 +43,12 @@ function warning($msg, $immediately = false) * * @param string $msg * @param bool $immediately + * @param bool $immediatelyRaw * @return string */ -function error($msg, $immediately = false) +function error($msg, $immediately = false, $immediatelyRaw = false) { - return alert(NotificationType::ERROR, $msg, $immediately); + return alert(NotificationType::ERROR, $msg, $immediately, $immediatelyRaw); } /** @@ -53,24 +56,27 @@ function error($msg, $immediately = false) * * @param string $msg * @param bool $immediately + * @param bool $immediatelyRaw * @return string */ -function success($msg, $immediately = false) +function success($msg, $immediately = false, $immediatelyRaw = false) { - return alert(NotificationType::MESSAGE, $msg, $immediately); + return alert(NotificationType::MESSAGE, $msg, $immediately, $immediatelyRaw); } /** * Renders an alert message with the given alert-* class or sets it in session * - * @see \Engelsystem\Controllers\HasUserNotifications - * * @param NotificationType $type * @param string $msg * @param bool $immediately + * @param bool $immediatelyRaw * @return string + * + * @see \Engelsystem\Controllers\HasUserNotifications + * */ -function alert(NotificationType $type, $msg, $immediately = false) +function alert(NotificationType $type, $msg, $immediately = false, $immediatelyRaw = false) { if (empty($msg)) { return ''; @@ -87,6 +93,7 @@ function alert(NotificationType $type, $msg, $immediately = false) ['danger', 'warning', 'info', 'success'], $type->value ); + $msg = $immediatelyRaw ? $msg : htmlspecialchars($msg); return ''; } diff --git a/includes/helper/shift_helper.php b/includes/helper/shift_helper.php index e6edb6141..fd27f70bb 100644 --- a/includes/helper/shift_helper.php +++ b/includes/helper/shift_helper.php @@ -5,11 +5,13 @@ use Carbon\Carbon; use Engelsystem\Helpers\Shifts; use Engelsystem\Mail\EngelsystemMailer; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; +use Engelsystem\Models\Shifts\Shift as ShiftModel; +use Engelsystem\Models\Shifts\ShiftEntry; use Engelsystem\Models\User\User; use Engelsystem\Models\Worklog; +use Illuminate\Database\Eloquent\Collection; use Psr\Log\LoggerInterface; -use Symfony\Component\Mailer\Exception\TransportException; class Shift { @@ -26,7 +28,7 @@ public function deletedEntryCreateWorklog( string $name, string $title, string $type, - Room $room, + Location $location, bool $freeloaded ): void { if ($freeloaded || $start > Carbon::now()) { @@ -45,9 +47,9 @@ public function deletedEntryCreateWorklog( $name, $title, $type, - $room->name, - $start->format(__('Y-m-d H:i')), - $end->format(__('Y-m-d H:i')) + $location->name, + $start->format(__('general.datetime')), + $end->format(__('general.datetime')) ); $workLog->save(); @@ -64,34 +66,70 @@ public function deletedEntrySendEmail( string $name, string $title, string $type, - Room $room, + Location $location, bool $freeloaded ): void { if (!$user->settings->email_shiftinfo) { return; } - $subject = 'notification.shift.deleted'; - try { + $this->mailer->sendViewTranslated( + $user, + 'notification.shift.deleted', + 'emails/worklog-from-shift', + [ + 'name' => $name, + 'title' => $title, + 'start' => $start, + 'end' => $end, + 'location' => $location, + 'freeloaded' => $freeloaded, + 'username' => $user->displayName, + ] + ); + } + + public function updatedShiftSendEmail( + ShiftModel $shift, + ShiftModel $oldShift + ): void { + // Only send e-mail on relevant changes + if ( + $oldShift->shift_type_id == $shift->shift_type_id + && $oldShift->title == $shift->title + && $oldShift->start == $shift->start + && $oldShift->end == $shift->end + && $oldShift->location_id == $shift->location_id + ) { + return; + } + + $shift->load(['shiftType', 'location']); + $oldShift->load(['shiftType', 'location']); + /** @var ShiftEntry[]|Collection $shiftEntries */ + $shiftEntries = $shift->shiftEntries() + ->with(['angelType', 'user.settings']) + ->get(); + + foreach ($shiftEntries as $shiftEntry) { + $user = $shiftEntry->user; + $angelType = $shiftEntry->angelType; + + if (!$user->settings->email_shiftinfo || $shift->end < Carbon::now()) { + continue; + } + $this->mailer->sendViewTranslated( $user, - $subject, - 'emails/worklog-from-shift', + 'notification.shift.updated', + 'emails/updated-shift', [ - 'name' => $name, - 'title' => $title, - 'start' => $start, - 'end' => $end, - 'room' => $room, - 'freeloaded' => $freeloaded, - 'username' => $user->displayName, + 'shift' => $shift, + 'oldShift' => $oldShift, + 'angelType' => $angelType, + 'username' => $user->displayName, ] ); - } catch (TransportException $e) { - $this->log->error( - 'Unable to send email "{title}" to user {user} with {exception}', - ['title' => $subject, 'user' => $user->name, 'exception' => $e] - ); } } } diff --git a/includes/includes.php b/includes/includes.php index e16c4170d..d8df64f76 100644 --- a/includes/includes.php +++ b/includes/includes.php @@ -18,35 +18,30 @@ __DIR__ . '/../includes/model/ShiftSignupState.php', __DIR__ . '/../includes/model/Stats.php', __DIR__ . '/../includes/model/User_model.php', - __DIR__ . '/../includes/model/UserWorkLog_model.php', __DIR__ . '/../includes/model/ValidationResult.php', __DIR__ . '/../includes/view/AngelTypes_view.php', __DIR__ . '/../includes/view/EventConfig_view.php', __DIR__ . '/../includes/view/PublicDashboard_view.php', - __DIR__ . '/../includes/view/Rooms_view.php', + __DIR__ . '/../includes/view/Locations_view.php', __DIR__ . '/../includes/view/ShiftCalendarLane.php', __DIR__ . '/../includes/view/ShiftCalendarRenderer.php', __DIR__ . '/../includes/view/ShiftCalendarShiftRenderer.php', __DIR__ . '/../includes/view/ShiftsFilterRenderer.php', __DIR__ . '/../includes/view/Shifts_view.php', __DIR__ . '/../includes/view/ShiftEntry_view.php', - __DIR__ . '/../includes/view/ShiftTypes_view.php', __DIR__ . '/../includes/view/UserAngelTypes_view.php', - __DIR__ . '/../includes/view/UserDriverLicenses_view.php', __DIR__ . '/../includes/view/UserHintsRenderer.php', __DIR__ . '/../includes/view/User_view.php', __DIR__ . '/../includes/controller/angeltypes_controller.php', __DIR__ . '/../includes/controller/event_config_controller.php', __DIR__ . '/../includes/controller/public_dashboard_controller.php', - __DIR__ . '/../includes/controller/rooms_controller.php', + __DIR__ . '/../includes/controller/locations_controller.php', __DIR__ . '/../includes/controller/shift_entries_controller.php', __DIR__ . '/../includes/controller/shifts_controller.php', - __DIR__ . '/../includes/controller/shifttypes_controller.php', __DIR__ . '/../includes/controller/users_controller.php', __DIR__ . '/../includes/controller/user_angeltypes_controller.php', - __DIR__ . '/../includes/controller/user_driver_licenses_controller.php', __DIR__ . '/../includes/helper/legacy_helper.php', __DIR__ . '/../includes/helper/message_helper.php', @@ -62,7 +57,6 @@ __DIR__ . '/../includes/pages/admin_groups.php', __DIR__ . '/../includes/pages/admin_shifts.php', __DIR__ . '/../includes/pages/admin_user.php', - __DIR__ . '/../includes/pages/guest_login.php', __DIR__ . '/../includes/pages/user_myshifts.php', __DIR__ . '/../includes/pages/user_shifts.php', diff --git a/includes/mailer/shifts_mailer.php b/includes/mailer/shifts_mailer.php index ce037e54e..5de920a36 100644 --- a/includes/mailer/shifts_mailer.php +++ b/includes/mailer/shifts_mailer.php @@ -1,87 +1,7 @@ shiftEntries() - ->with(['user', 'user.settings']) - ->get(); - $old_room = $old_shift->room; - $new_room = $new_shift->room; - - $noticeable_changes = false; - - $message = __('A Shift you are registered on has changed:'); - $message .= "\n"; - - if ($old_shift->shift_type_id != $new_shift->shift_type_id) { - $message .= sprintf( - __('* Shift type changed from %s to %s'), - $old_shift->shiftType->name, - $new_shift->shiftType->name - ) . "\n"; - $noticeable_changes = true; - } - - if ($old_shift->title != $new_shift->title) { - $message .= sprintf(__('* Shift title changed from %s to %s'), $old_shift->title, $new_shift->title) . "\n"; - $noticeable_changes = true; - } - - if ($old_shift->start->timestamp != $new_shift->start->timestamp) { - $message .= sprintf( - __('* Shift Start changed from %s to %s'), - $old_shift->start->format(__('Y-m-d H:i')), - $new_shift->start->format(__('Y-m-d H:i')) - ) . "\n"; - $noticeable_changes = true; - } - - if ($old_shift->end->timestamp != $new_shift->end->timestamp) { - $message .= sprintf( - __('* Shift End changed from %s to %s'), - $old_shift->end->format(__('Y-m-d H:i')), - $new_shift->end->format(__('Y-m-d H:i')) - ) . "\n"; - $noticeable_changes = true; - } - - if ($old_shift->room_id != $new_shift->room_id) { - $message .= sprintf(__('* Shift Location changed from %s to %s'), $old_room->name, $new_room->name) . "\n"; - $noticeable_changes = true; - } - - if (!$noticeable_changes) { - // There are no changes worth sending an E-Mail - return; - } - - $message .= "\n"; - $message .= __('The updated Shift:') . "\n"; - - $message .= $new_shift->shiftType->name . "\n"; - $message .= $new_shift->title . "\n"; - $message .= $new_shift->start->format(__('Y-m-d H:i')) . ' - ' . $new_shift->end->format(__('H:i')) . "\n"; - $message .= $new_room->name . "\n\n"; - $message .= url('/shifts', ['action' => 'view', 'shift_id' => $new_shift->id]) . "\n"; - - foreach ($shiftEntries as $shiftEntry) { - $user = $shiftEntry->user; - if ($user->settings->email_shiftinfo) { - engelsystem_email_to_user( - $user, - __('Your Shift has changed'), - $message, - true - ); - } - } -} function mail_shift_assign(User $user, Shift $shift) { @@ -89,13 +9,11 @@ function mail_shift_assign(User $user, Shift $shift) return; } - $room = $shift->room; - $message = __('You have been assigned to a Shift:') . "\n"; $message .= $shift->shiftType->name . "\n"; $message .= $shift->title . "\n"; - $message .= $shift->start->format(__('Y-m-d H:i')) . ' - ' . $shift->end->format(__('H:i')) . "\n"; - $message .= $room->name . "\n\n"; + $message .= $shift->start->format(__('general.datetime')) . ' - ' . $shift->end->format(__('H:i')) . "\n"; + $message .= $shift->location->name . "\n\n"; $message .= url('/shifts', ['action' => 'view', 'shift_id' => $shift->id]) . "\n"; engelsystem_email_to_user($user, __('Assigned to Shift'), $message, true); @@ -107,13 +25,11 @@ function mail_shift_removed(User $user, Shift $shift) return; } - $room = $shift->room; - $message = __('You have been removed from a Shift:') . "\n"; $message .= $shift->shiftType->name . "\n"; $message .= $shift->title . "\n"; - $message .= $shift->start->format(__('Y-m-d H:i')) . ' - ' . $shift->end->format(__('H:i')) . "\n"; - $message .= $room->name . "\n"; + $message .= $shift->start->format(__('general.datetime')) . ' - ' . $shift->end->format(__('H:i')) . "\n"; + $message .= $shift->location->name . "\n"; engelsystem_email_to_user($user, __('Removed from Shift'), $message, true); } diff --git a/includes/mailer/users_mailer.php b/includes/mailer/users_mailer.php index 6135bcf7d..1438a5a9c 100644 --- a/includes/mailer/users_mailer.php +++ b/includes/mailer/users_mailer.php @@ -10,7 +10,7 @@ function mail_user_delete($user) { return engelsystem_email_to_user( $user, - __('Your account has been deleted'), + __('Your account has been deleted.'), __( 'Your %s account has been deleted. If you have any questions regarding your account deletion, please contact heaven.', [config('app_name')] diff --git a/includes/model/NeededAngelTypes_model.php b/includes/model/NeededAngelTypes_model.php index 9922b7eb7..c1edbaae3 100644 --- a/includes/model/NeededAngelTypes_model.php +++ b/includes/model/NeededAngelTypes_model.php @@ -1,47 +1,70 @@ schedule) { + $needed_angeltypes_source = Db::select( + ' SELECT `needed_angel_types`.*, - `angel_types`.`id`, `angel_types`.`name`, `angel_types`.`restricted`, - `angel_types`.`no_self_signup` + `angel_types`.`shift_self_signup` FROM `needed_angel_types` JOIN `angel_types` ON `angel_types`.`id` = `needed_angel_types`.`angel_type_id` - WHERE `shift_id` = ? - ORDER BY `room_id` DESC', - [$shiftId] - ); + WHERE `needed_angel_types`.`shift_id` = ? + ORDER BY `location_id` DESC + ', + [$shift->id] + ); + } + + // Get needed by shift type + if ($shift->schedule && $shift->schedule->needed_from_shift_type) { + $needed_angeltypes_source = Db::select(' + SELECT + `needed_angel_types`.*, + `angel_types`.`name`, + `angel_types`.`restricted`, + `angel_types`.`shift_self_signup` + FROM `needed_angel_types` + JOIN `angel_types` ON `angel_types`.`id` = `needed_angel_types`.`angel_type_id` + WHERE `needed_angel_types`.`shift_type_id` = ? + ORDER BY `location_id` DESC + ', [$shift->shift_type_id]); + } - // Use settings from room - if (count($needed_angeltypes_source) == 0) { + // Load from room + if ($shift->schedule && !$shift->schedule->needed_from_shift_type) { $needed_angeltypes_source = Db::select(' - SELECT `needed_angel_types`.*, `angel_types`.`name`, `angel_types`.`restricted` + SELECT + `needed_angel_types`.*, + `angel_types`.`name`, + `angel_types`.`restricted`, + `angel_types`.`shift_self_signup` FROM `needed_angel_types` JOIN `angel_types` ON `angel_types`.`id` = `needed_angel_types`.`angel_type_id` - JOIN `shifts` ON `shifts`.`room_id` = `needed_angel_types`.`room_id` - WHERE `shifts`.`id` = ? - ORDER BY `room_id` DESC - ', [$shiftId]); + WHERE `needed_angel_types`.`location_id` = ? + ORDER BY `location_id` DESC + ', [$shift->location_id]); } /** @var ShiftEntry[]|Collection $shift_entries */ $shift_entries = ShiftEntry::with('user', 'angelType') - ->where('shift_id', $shiftId) + ->where('shift_id', $shift->id) ->get(); $needed_angeltypes = []; foreach ($needed_angeltypes_source as $angeltype) { diff --git a/includes/model/ShiftEntry_model.php b/includes/model/ShiftEntry_model.php index 21ec09210..3f8dd3b57 100644 --- a/includes/model/ShiftEntry_model.php +++ b/includes/model/ShiftEntry_model.php @@ -15,7 +15,7 @@ function ShiftEntry_onCreate(ShiftEntry $shiftEntry): void 'User ' . User_Nick_render($shiftEntry->user, true) . ' signed up for shift ' . $shiftEntry->shift->title . ' (' . $shift->shiftType->name . ')' - . ' at ' . $shift->room->name + . ' at ' . $shift->location->name . ' from ' . $shift->start->format('Y-m-d H:i') . ' to ' . $shift->end->format('Y-m-d H:i') . ' as ' . $shiftEntry->angelType->name @@ -33,14 +33,14 @@ function ShiftEntry_onDelete(ShiftEntry $shiftEntry) $signout_user = $shiftEntry->user; $shift = Shift($shiftEntry->shift); $shifttype = $shift->shiftType; - $room = $shift->room; + $location = $shift->location; $angeltype = $shiftEntry->angelType; engelsystem_log( 'Shift signout: ' . User_Nick_render($signout_user, true) . ' from shift ' . $shift->title . ' (' . $shifttype->name . ')' - . ' at ' . $room->name + . ' at ' . $location->name . ' from ' . $shift->start->format('Y-m-d H:i') . ' to ' . $shift->end->format('Y-m-d H:i') . ' as ' . $angeltype->name diff --git a/includes/model/ShiftsFilter.php b/includes/model/ShiftsFilter.php index bb75ed7e3..59fc56aa0 100644 --- a/includes/model/ShiftsFilter.php +++ b/includes/model/ShiftsFilter.php @@ -44,10 +44,10 @@ class ShiftsFilter * ShiftsFilter constructor. * * @param bool $user_shifts_admin - * @param int[] $rooms + * @param int[] $locations * @param int[] $angelTypes */ - public function __construct($user_shifts_admin = false, private $rooms = [], $angelTypes = []) + public function __construct($user_shifts_admin = false, private $locations = [], $angelTypes = []) { $this->types = $angelTypes; @@ -68,7 +68,7 @@ public function sessionExport() return [ 'userShiftsAdmin' => $this->userShiftsAdmin, 'filled' => $this->filled, - 'rooms' => $this->rooms, + 'locations' => $this->locations, 'types' => $this->types, 'startTime' => $this->startTime, 'endTime' => $this->endTime, @@ -80,12 +80,12 @@ public function sessionExport() */ public function sessionImport($data) { - $this->userShiftsAdmin = $data['userShiftsAdmin']; - $this->filled = $data['filled']; - $this->rooms = $data['rooms']; - $this->types = $data['types']; - $this->startTime = $data['startTime']; - $this->endTime = $data['endTime']; + $this->userShiftsAdmin = $data['userShiftsAdmin'] ?? false; + $this->filled = $data['filled'] ?? []; + $this->locations = $data['locations'] ?? []; + $this->types = $data['types'] ?? []; + $this->startTime = $data['startTime'] ?? null; + $this->endTime = $data['endTime'] ?? null; } /** @@ -163,28 +163,20 @@ public function setTypes($types) /** * @return int[] */ - public function getRooms() + public function getLocations() { - if (count($this->rooms) == 0) { + if (count($this->locations) == 0) { return [0]; } - return $this->rooms; + return $this->locations; } /** - * @param int[] $rooms + * @param int[] $locations */ - public function setRooms($rooms) + public function setLocations($locations) { - $this->rooms = $rooms; - } - - /** - * @return bool - */ - public function isUserShiftsAdmin() - { - return $this->userShiftsAdmin; + $this->locations = $locations; } /** diff --git a/includes/model/Shifts_model.php b/includes/model/Shifts_model.php index 3925460a9..ff2c9dd25 100644 --- a/includes/model/Shifts_model.php +++ b/includes/model/Shifts_model.php @@ -28,12 +28,26 @@ function Shifts_by_angeltype(AngelType $angeltype) UNION + /* By shift type */ SELECT DISTINCT `shifts`.* FROM `shifts` - JOIN `needed_angel_types` ON `needed_angel_types`.`room_id` = `shifts`.`room_id` + JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id` = `shifts`.`shift_type_id` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id WHERE `needed_angel_types`.`angel_type_id` = ? AND NOT s.shift_id IS NULL - ', [$angeltype->id, $angeltype->id]); + AND se.needed_from_shift_type = TRUE + + UNION + + /* By location */ + SELECT DISTINCT `shifts`.* FROM `shifts` + JOIN `needed_angel_types` ON `needed_angel_types`.`location_id` = `shifts`.`location_id` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `needed_angel_types`.`angel_type_id` = ? + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = FALSE + ', [$angeltype->id, $angeltype->id, $angeltype->id]); } /** @@ -53,25 +67,42 @@ function Shifts_free($start, $end, ShiftsFilter $filter = null) $shifts = Db::select(' SELECT * FROM ( - SELECT id, start + SELECT shifts.id, start FROM `shifts` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id WHERE (`end` > ? AND `start` < ?) AND (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`shift_id`=`shifts`.`id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') > (SELECT COUNT(*) FROM `shift_entries` WHERE `shift_entries`.`shift_id`=`shifts`.`id` AND shift_entries.`freeloaded`=0' . ($filter ? ' AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') AND s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' + + UNION + + /* By shift type */ + SELECT shifts.id, start + FROM `shifts` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE (`end` > ? AND `start` < ?) + AND (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') + > (SELECT COUNT(*) FROM `shift_entries` WHERE `shift_entries`.`shift_id`=`shifts`.`id` AND shift_entries.`freeloaded`=0' . ($filter ? ' AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = TRUE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' UNION - SELECT id, start + /* By location */ + SELECT shifts.id, start FROM `shifts` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id WHERE (`end` > ? AND `start` < ?) - AND (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`room_id`=`shifts`.`room_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') - > (SELECT COUNT(*) FROM `shift_entries` WHERE `shift_entries`.`shift_id`=`shifts`.`id` AND `freeloaded`=0' . ($filter ? ' AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') + AND (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`location_id`=`shifts`.`location_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') + > (SELECT COUNT(*) FROM `shift_entries` WHERE `shift_entries`.`shift_id`=`shifts`.`id` AND shift_entries.`freeloaded`=0' . ($filter ? ' AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') AND NOT s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + AND se.needed_from_shift_type = FALSE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' ) AS `tmp` ORDER BY `tmp`.`start` ', [ @@ -79,12 +110,15 @@ function Shifts_free($start, $end, ShiftsFilter $filter = null) $end, $start, $end, + $start, + $end, ]); $shifts = collect($shifts); - return Shift::query() + return Shift::with(['location', 'shiftType']) ->whereIn('id', $shifts->pluck('id')->toArray()) + ->orderBy('shifts.start') ->get(); } @@ -96,32 +130,51 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter) { $sql = ' SELECT * FROM ( - SELECT DISTINCT `shifts`.*, `shift_types`.`name`, `rooms`.`name` AS `room_name` + SELECT DISTINCT `shifts`.*, `shift_types`.`name`, `locations`.`name` AS `location_name` FROM `shifts` - JOIN `rooms` ON `shifts`.`room_id` = `rooms`.`id` + JOIN `locations` ON `shifts`.`location_id` = `locations`.`id` JOIN `shift_types` ON `shift_types`.`id` = `shifts`.`shift_type_id` JOIN `needed_angel_types` ON `needed_angel_types`.`shift_id` = `shifts`.`id` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE `shifts`.`room_id` IN (' . implode(',', $shiftsFilter->getRooms()) . ') + WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ') AND `start` BETWEEN ? AND ? AND `needed_angel_types`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ') AND s.shift_id IS NULL UNION - SELECT DISTINCT `shifts`.*, `shift_types`.`name`, `rooms`.`name` AS `room_name` + /* By shift type */ + SELECT DISTINCT `shifts`.*, `shift_types`.`name`, `locations`.`name` AS `location_name` + FROM `shifts` + JOIN `locations` ON `shifts`.`location_id` = `locations`.`id` + JOIN `shift_types` ON `shift_types`.`id` = `shifts`.`shift_type_id` + JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ') + AND `start` BETWEEN ? AND ? + AND `needed_angel_types`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ') + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = TRUE + + UNION + + /* By location */ + SELECT DISTINCT `shifts`.*, `shift_types`.`name`, `locations`.`name` AS `location_name` FROM `shifts` - JOIN `rooms` ON `shifts`.`room_id` = `rooms`.`id` + JOIN `locations` ON `shifts`.`location_id` = `locations`.`id` JOIN `shift_types` ON `shift_types`.`id` = `shifts`.`shift_type_id` - JOIN `needed_angel_types` ON `needed_angel_types`.`room_id`=`shifts`.`room_id` + JOIN `needed_angel_types` ON `needed_angel_types`.`location_id`=`shifts`.`location_id` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE `shifts`.`room_id` IN (' . implode(',', $shiftsFilter->getRooms()) . ') + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ') AND `start` BETWEEN ? AND ? AND `needed_angel_types`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ') AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = FALSE ) AS tmp_shifts - ORDER BY `room_name`, `start` + ORDER BY `location_name`, `start` '; $shiftsData = Db::select( @@ -131,15 +184,19 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter) $shiftsFilter->getEnd(), $shiftsFilter->getStart(), $shiftsFilter->getEnd(), + $shiftsFilter->getStart(), + $shiftsFilter->getEnd(), ] ); - $shifts = []; + $shifts = new Collection(); foreach ($shiftsData as $shift) { $shifts[] = (new Shift())->forceFill($shift); } - return collect($shifts); + $shifts->load(['location', 'shiftType']); + + return $shifts; } /** @@ -155,31 +212,54 @@ function NeededAngeltypes_by_ShiftsFilter(ShiftsFilter $shiftsFilter) `angel_types`.`id`, `angel_types`.`name`, `angel_types`.`restricted`, - `angel_types`.`no_self_signup` + `angel_types`.`shift_self_signup` FROM `shifts` JOIN `needed_angel_types` ON `needed_angel_types`.`shift_id`=`shifts`.`id` JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE `shifts`.`room_id` IN (' . implode(',', $shiftsFilter->getRooms()) . ') + WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ') AND shifts.`start` BETWEEN ? AND ? AND s.shift_id IS NULL UNION + /* By shift type */ SELECT `needed_angel_types`.*, `shifts`.`id` AS shift_id, `angel_types`.`id`, `angel_types`.`name`, `angel_types`.`restricted`, - `angel_types`.`no_self_signup` + `angel_types`.`shift_self_signup` FROM `shifts` - JOIN `needed_angel_types` ON `needed_angel_types`.`room_id`=`shifts`.`room_id` + JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id` JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE `shifts`.`room_id` IN (' . implode(',', $shiftsFilter->getRooms()) . ') + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ') AND shifts.`start` BETWEEN ? AND ? AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = TRUE + + UNION + + /* By location */ + SELECT + `needed_angel_types`.*, + `shifts`.`id` AS shift_id, + `angel_types`.`id`, + `angel_types`.`name`, + `angel_types`.`restricted`, + `angel_types`.`shift_self_signup` + FROM `shifts` + JOIN `needed_angel_types` ON `needed_angel_types`.`location_id`=`shifts`.`location_id` + JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ') + AND shifts.`start` BETWEEN ? AND ? + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = FALSE '; return Db::select( @@ -189,6 +269,8 @@ function NeededAngeltypes_by_ShiftsFilter(ShiftsFilter $shiftsFilter) $shiftsFilter->getEnd(), $shiftsFilter->getStart(), $shiftsFilter->getEnd(), + $shiftsFilter->getStart(), + $shiftsFilter->getEnd(), ] ); } @@ -208,7 +290,7 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty `angel_types`.`id`, `angel_types`.`name`, `angel_types`.`restricted`, - `angel_types`.`no_self_signup` + `angel_types`.`shift_self_signup` FROM `shifts` JOIN `needed_angel_types` ON `needed_angel_types`.`shift_id`=`shifts`.`id` JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id` @@ -219,26 +301,51 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty UNION + /* By shift type */ SELECT `needed_angel_types`.*, `shifts`.`id` AS shift_id, `angel_types`.`id`, `angel_types`.`name`, `angel_types`.`restricted`, - `angel_types`.`no_self_signup` + `angel_types`.`shift_self_signup` FROM `shifts` - JOIN `needed_angel_types` ON `needed_angel_types`.`room_id`=`shifts`.`room_id` + JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id` JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id WHERE `shifts`.`id`=? AND `angel_types`.`id`=? AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = TRUE + + UNION + + /* By location */ + SELECT + `needed_angel_types`.*, + `shifts`.`id` AS shift_id, + `angel_types`.`id`, + `angel_types`.`name`, + `angel_types`.`restricted`, + `angel_types`.`shift_self_signup` + FROM `shifts` + JOIN `needed_angel_types` ON `needed_angel_types`.`location_id`=`shifts`.`location_id` + JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `shifts`.`id`=? + AND `angel_types`.`id`=? + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = FALSE ', [ $shift->id, $angeltype->id, $shift->id, $angeltype->id, + $shift->id, + $angeltype->id, ] ); } @@ -249,9 +356,9 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty */ function ShiftEntries_by_ShiftsFilter(ShiftsFilter $shiftsFilter) { - return ShiftEntry::with('user') + return ShiftEntry::with('user', 'user.state') ->join('shifts', 'shifts.id', 'shift_entries.shift_id') - ->whereIn('shifts.room_id', $shiftsFilter->getRooms()) + ->whereIn('shifts.location_id', $shiftsFilter->getLocations()) ->whereBetween('start', [$shiftsFilter->getStart(), $shiftsFilter->getEnd()]) ->get(); } @@ -367,12 +474,12 @@ function Shift_signup_allowed_angel( if ( empty($user_angeltype) - || $angeltype->no_self_signup == 1 - || ($angeltype->restricted == 1 && !isset($user_angeltype['confirm_user_id'])) + || !$angeltype->shift_self_signup + || ($angeltype->restricted && !isset($user_angeltype['confirm_user_id'])) ) { // you cannot join if user is not of this angel type + // you cannot join if angeltype has shift self signup disabled // you cannot join if you are not confirmed - // you cannot join if angeltype has no self signup return new ShiftSignupState(ShiftSignupStatus::ANGELTYPE, $free_entries); } @@ -509,8 +616,8 @@ function Shifts_by_user($userId, $include_freeloaded_comments = false) $shiftsData = Db::select( ' SELECT - `rooms`.*, - `rooms`.name AS Name, + `locations`.*, + `locations`.name AS Name, `shift_types`.`id` AS `shifttype_id`, `shift_types`.`name`, `shift_entries`.`id` as shift_entry_id, @@ -524,7 +631,7 @@ function Shifts_by_user($userId, $include_freeloaded_comments = false) FROM `shift_entries` JOIN `shifts` ON (`shift_entries`.`shift_id` = `shifts`.`id`) JOIN `shift_types` ON (`shift_types`.`id` = `shifts`.`shift_type_id`) - JOIN `rooms` ON (`shifts`.`room_id` = `rooms`.`id`) + JOIN `locations` ON (`shifts`.`location_id` = `locations`.`id`) WHERE shift_entries.`user_id` = ? ORDER BY `start` ', @@ -533,12 +640,13 @@ function Shifts_by_user($userId, $include_freeloaded_comments = false) ] ); - $shifts = []; + $shifts = new Collection(); foreach ($shiftsData as $data) { $shifts[] = (new Shift())->forceFill($data); } + $shifts->load(['shiftType', 'location']); - return collect($shifts); + return $shifts; } /** @@ -558,7 +666,7 @@ function Shift($shift) } $neededAngels = []; - $angelTypes = NeededAngelTypes_by_shift($shift->id); + $angelTypes = NeededAngelTypes_by_shift($shift); foreach ($angelTypes as $type) { $neededAngels[] = [ 'angel_type_id' => $type['angel_type_id'], diff --git a/includes/model/Stats.php b/includes/model/Stats.php index adc4af32d..6dc04d513 100644 --- a/includes/model/Stats.php +++ b/includes/model/Stats.php @@ -1,6 +1,7 @@ = NOW() AND `start` <= NOW()) - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') ); return $result['count'] ?: '-'; @@ -46,20 +47,37 @@ function stats_hours_to_work(ShiftsFilter $filter = null) * TIMESTAMPDIFF(MINUTE, `shifts`.`start`, `shifts`.`end`) / 60 AS `count` FROM `shifts` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE `shifts`.`end` >= NOW() + WHERE shifts.`end` >= NOW() AND s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' UNION ALL + /* By shift type */ SELECT - (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`room_id`=`shifts`.`room_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') + (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') * TIMESTAMPDIFF(MINUTE, `shifts`.`start`, `shifts`.`end`) / 60 AS `count` FROM `shifts` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE shifts.`end` >= NOW() + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE shifts.`end` >= NOW() AND NOT s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + AND se.needed_from_shift_type = TRUE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' + + UNION ALL + + /* By location */ + SELECT + (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`location_id`=`shifts`.`location_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ') + * TIMESTAMPDIFF(MINUTE, `shifts`.`start`, `shifts`.`end`) / 60 AS `count` + FROM `shifts` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE shifts.`end` >= NOW() + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = FALSE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' ) AS `tmp` ' ); @@ -76,7 +94,7 @@ function stats_hours_to_work(ShiftsFilter $filter = null) */ function stats_angels_needed_three_hours(ShiftsFilter $filter = null) { - $in3hours = time() + 3 * 60 * 60; + $in3hours = Carbon::now()->addHours(3)->toDateTimeString(); $result = Db::selectOne(' SELECT SUM(`count`) AS `count` FROM ( SELECT @@ -89,7 +107,8 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null) AND `needed_angel_types`.`shift_id`=`shifts`.`id` ' . ($filter ? 'AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' ) - ( - SELECT COUNT(*) FROM `shift_entries` + SELECT COUNT(*) + FROM `shift_entries` JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id` WHERE `angel_types`.`show_on_dashboard`=TRUE AND `shift_entries`.`shift_id`=`shifts`.`id` @@ -102,10 +121,11 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null) LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id WHERE shifts.`end` > NOW() AND shifts.`start` < ? AND s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' UNION ALL + /* By shift type */ SELECT GREATEST(0, ( @@ -113,7 +133,7 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null) FROM `needed_angel_types` JOIN `angel_types` ON `angel_types`.`id`=`needed_angel_types`.`angel_type_id` WHERE `angel_types`.`show_on_dashboard`=TRUE - AND `needed_angel_types`.`room_id`=`shifts`.`room_id` + AND `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id` ' . ($filter ? 'AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' ) - ( SELECT COUNT(*) @@ -128,12 +148,46 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null) AS `count` FROM `shifts` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE `end` > NOW() AND `start` < ? + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE shifts.`end` > NOW() AND shifts.`start` < ? AND NOT s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + AND se.needed_from_shift_type = TRUE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' + + UNION ALL + + /* By location */ + SELECT + GREATEST(0, + ( + SELECT SUM(needed_angel_types.`count`) + FROM `needed_angel_types` + JOIN `angel_types` ON `angel_types`.`id`=`needed_angel_types`.`angel_type_id` + WHERE `angel_types`.`show_on_dashboard`=TRUE + AND `needed_angel_types`.`location_id`=`shifts`.`location_id` + ' . ($filter ? 'AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' + ) - ( + SELECT COUNT(*) + FROM `shift_entries` + JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id` + WHERE `angel_types`.`show_on_dashboard`=TRUE + AND `shift_entries`.`shift_id`=`shifts`.`id` + AND `freeloaded`=0 + ' . ($filter ? 'AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' + ) + ) + AS `count` + FROM `shifts` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE shifts.`end` > NOW() AND shifts.`start` < ? + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = FALSE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' ) AS `tmp`', [ $in3hours, $in3hours, + $in3hours, ]); return $result['count'] ?: '-'; @@ -157,6 +211,8 @@ function stats_angels_needed_for_nightshifts(ShiftsFilter $filter = null) date('Y-m-d', time() + 12 * 60 * 60) . ' ' . $nightStartTime . ':00' ); $night_end = $night_start + ($nightEndTime - $nightStartTime) * 60 * 60; + $night_start = Carbon::createFromTimestamp($night_start)->toDateTimeString(); + $night_end = Carbon::createFromTimestamp($night_end)->toDateTimeString(); $result = Db::selectOne(' SELECT SUM(`count`) AS `count` FROM ( SELECT @@ -182,10 +238,11 @@ function stats_angels_needed_for_nightshifts(ShiftsFilter $filter = null) LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id WHERE shifts.`end` > ? AND shifts.`start` < ? AND s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' UNION ALL + /* By shift type */ SELECT GREATEST(0, ( @@ -193,28 +250,62 @@ function stats_angels_needed_for_nightshifts(ShiftsFilter $filter = null) FROM `needed_angel_types` JOIN `angel_types` ON `angel_types`.`id`=`needed_angel_types`.`angel_type_id` WHERE `angel_types`.`show_on_dashboard`=TRUE - AND `needed_angel_types`.`room_id`=`shifts`.`room_id` + AND `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id` ' . ($filter ? 'AND angel_types.id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' ) - ( SELECT COUNT(*) FROM `shift_entries` JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id` WHERE `angel_types`.`show_on_dashboard`=TRUE AND `shift_entries`.`shift_id`=`shifts`.`id` - AND `freeloaded`=0 + AND shift_entries.`freeloaded`=0 + ' . ($filter ? 'AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' + ) + ) + AS `count` + FROM `shifts` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE shifts.`end` > ? AND shifts.`start` < ? + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = TRUE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' + + UNION ALL + + /* By location */ + SELECT + GREATEST(0, + ( + SELECT SUM(needed_angel_types.`count`) + FROM `needed_angel_types` + JOIN `angel_types` ON `angel_types`.`id`=`needed_angel_types`.`angel_type_id` + WHERE `angel_types`.`show_on_dashboard`=TRUE + AND `needed_angel_types`.`location_id`=`shifts`.`location_id` + ' . ($filter ? 'AND angel_types.id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' + ) - ( + SELECT COUNT(*) FROM `shift_entries` + JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id` + WHERE `angel_types`.`show_on_dashboard`=TRUE + AND `shift_entries`.`shift_id`=`shifts`.`id` + AND shift_entries.`freeloaded`=0 ' . ($filter ? 'AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ' ) ) AS `count` FROM `shifts` LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id - WHERE `end` > ? AND `start` < ? + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE shifts.`end` > ? AND shifts.`start` < ? AND NOT s.shift_id IS NULL - ' . ($filter ? 'AND shifts.room_id IN (' . implode(',', $filter->getRooms()) . ')' : '') . ' + AND se.needed_from_shift_type = FALSE + ' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . ' ) AS `tmp`', [ $night_start, $night_end, $night_start, $night_end, + $night_start, + $night_end, ]); return $result['count'] ?: '-'; diff --git a/includes/model/UserWorkLog_model.php b/includes/model/UserWorkLog_model.php deleted file mode 100644 index a15c7969c..000000000 --- a/includes/model/UserWorkLog_model.php +++ /dev/null @@ -1,23 +0,0 @@ -whereDate('worked_at', '>=', $sinceTime); - } - - return $worklogs->get(); -} diff --git a/includes/model/User_model.php b/includes/model/User_model.php index b57059d32..560ab1715 100644 --- a/includes/model/User_model.php +++ b/includes/model/User_model.php @@ -5,7 +5,6 @@ use Engelsystem\Models\AngelType; use Engelsystem\Models\User\User; use Engelsystem\Models\Worklog; -use Engelsystem\ValidationResult; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; @@ -67,113 +66,6 @@ function Users_by_angeltype_inverted(AngelType $angeltype) ->get(); } -/** - * Strip unwanted characters from a users nick. Allowed are letters, numbers, connecting punctuation and simple space. - * Nick is trimmed. - * - * @param string $nick - * @return ValidationResult - */ -function User_validate_Nick($nick) -{ - $nick = trim($nick); - - if (strlen($nick) == 0 || strlen($nick) > 24) { - return new ValidationResult(false, $nick); - } - if (preg_match(config('username_regex', '/([^\p{L}\p{N}\-_. ]+)/ui'), $nick)) { - return new ValidationResult(false, $nick); - } - - return new ValidationResult(true, $nick); -} - -/** - * Validate the planned arrival date - * - * @param int $planned_arrival_date Unix timestamp - * @return ValidationResult - */ -function User_validate_planned_arrival_date($planned_arrival_date) -{ - if (is_null($planned_arrival_date)) { - // null is not okay - return new ValidationResult(false, time()); - } - - $config = config(); - $buildup = $config->get('buildup_start'); - $teardown = $config->get('teardown_end'); - - /** @var Carbon $buildup */ - if (!empty($buildup) && Carbon::createFromTimestamp($planned_arrival_date)->lessThan($buildup->setTime(0, 0))) { - // Planned arrival can not be before buildup start date - return new ValidationResult(false, $buildup->getTimestamp()); - } - - /** @var Carbon $teardown */ - if (!empty($teardown) && Carbon::createFromTimestamp($planned_arrival_date)->greaterThanOrEqualTo($teardown->addDay()->setTime(0, 0))) { - // Planned arrival can not be after teardown end date - return new ValidationResult(false, $teardown->getTimestamp()); - } - - return new ValidationResult(true, $planned_arrival_date); -} - -/** - * Validate the planned departure date - * - * @param int $planned_arrival_date Unix timestamp - * @param int $planned_departure_date Unix timestamp - * @return ValidationResult - */ -function User_validate_planned_departure_date($planned_arrival_date, $planned_departure_date) -{ - if (is_null($planned_departure_date)) { - // null is okay - return new ValidationResult(true, null); - } - - if ($planned_arrival_date > $planned_departure_date) { - // departure cannot be before arrival - return new ValidationResult(false, $planned_arrival_date); - } - - $config = config(); - $buildup = $config->get('buildup_start'); - $teardown = $config->get('teardown_end'); - - /** @var Carbon $buildup */ - if (!empty($buildup) && Carbon::createFromTimestamp($planned_departure_date)->lessThan($buildup->setTime(0, 0))) { - // Planned departure can not be before buildup start date - return new ValidationResult(false, $buildup->getTimestamp()); - } - - /** @var Carbon $teardown */ - if (!empty($teardown) && Carbon::createFromTimestamp($planned_departure_date)->greaterThanOrEqualTo($teardown->addDay()->setTime(0, 0))) { - // Planned departure can not be after teardown end date - return new ValidationResult(false, $teardown->getTimestamp()); - } - - return new ValidationResult(true, $planned_departure_date); -} - -/** - * Generates a new api key for given user. - * - * @param User $user - * @param bool $log - */ -function User_reset_api_key($user, $log = true) -{ - $user->api_key = bin2hex(random_bytes(32)); - $user->save(); - - if ($log) { - engelsystem_log(sprintf('API key resetted (%s).', User_Nick_render($user, true))); - } -} - /** * @param User $user * @return float @@ -186,7 +78,10 @@ function User_get_eligable_voucher_count($user) : null; $shiftEntries = ShiftEntries_finished_by_user($user, $start); - $worklog = UserWorkLogsForUser($user->id, $start); + $worklog = $user->worklogs() + ->whereDate('worked_at', '>=', $start ?: 0) + ->with(['user', 'creator']) + ->get(); $shifts_done = count($shiftEntries) + $worklog->count(); diff --git a/includes/pages/admin_active.php b/includes/pages/admin_active.php index 4685e76b3..2b4aacd10 100644 --- a/includes/pages/admin_active.php +++ b/includes/pages/admin_active.php @@ -3,6 +3,7 @@ use Engelsystem\Helpers\Carbon; use Engelsystem\Models\User\State; use Engelsystem\Models\User\User; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; use Engelsystem\Config\GoodieType; @@ -47,11 +48,11 @@ function admin_active() __('At least %s angels are forced to be active. The number has to be greater.'), $forced_count )); - throw_redirect(page_link_to('admin_active')); + throw_redirect(url('/admin-active')); } } else { $msg .= error(__('Please enter a number of angels to be marked as active.')); - throw_redirect(page_link_to('admin_active')); + throw_redirect(url('/admin-active')); } if ($request->hasPostData('ack')) { @@ -78,6 +79,11 @@ function admin_active() ->leftJoin('shifts', 'shift_entries.shift_id', '=', 'shifts.id') ->leftJoin('users_state', 'users.id', '=', 'users_state.user_id') ->where('users_state.arrived', '=', true) + ->orWhere(function (EloquentBuilder $userinfo) { + $userinfo->where('users_state.arrived', '=', false) + ->whereNotNull('users_state.user_info') + ->whereNot('users_state.user_info', ''); + }) ->groupBy('users.id') ->orderByDesc('force_active') ->orderByDesc('shift_length') @@ -97,9 +103,9 @@ function admin_active() $msg = success(__('Marked angels.'), true); } else { $set_active = form([ - button(page_link_to('admin_active', ['search' => $search]), '« ' . __('back')), - form_submit('ack', '» ' . __('apply')), - ], page_link_to('admin_active', ['search' => $search, 'count' => $count, 'set_active' => 1])); + button(url('/admin-active', ['search' => $search]), '« ' . __('general.back')), + form_submit('ack', '» ' . __('Apply')), + ], url('/admin-active', ['search' => $search, 'count' => $count, 'set_active' => 1])); } } @@ -133,7 +139,7 @@ function admin_active() $user_source->state->got_shirt = true; $user_source->state->save(); engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has tshirt now.'); - $msg = success(($goodie_tshirt ? __('Angel has got a t-shirt.') : __('Angel has got a goodie.')), true); + $msg = success(($goodie_tshirt ? __('Angel has got a T-shirt.') : __('Angel has got a goodie.')), true); } else { $msg = error('Angel not found.', true); } @@ -144,14 +150,14 @@ function admin_active() $user_source->state->got_shirt = false; $user_source->state->save(); engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has NO tshirt.'); - $msg = success(($goodie_tshirt ? __('Angel has got no t-shirt.') : __('Angel has got no goodie.')), true); + $msg = success(($goodie_tshirt ? __('Angel has got no T-shirt.') : __('Angel has got no goodie.')), true); } else { $msg = error(__('Angel not found.'), true); } } } - $query = User::with('personalData') + $query = User::with(['personalData', 'state']) ->selectRaw( sprintf( ' @@ -180,6 +186,11 @@ function admin_active() }) ->leftJoin('users_state', 'users.id', '=', 'users_state.user_id') ->where('users_state.arrived', '=', true) + ->orWhere(function (EloquentBuilder $userinfo) { + $userinfo->where('users_state.arrived', '=', false) + ->whereNotNull('users_state.user_info') + ->whereNot('users_state.user_info', ''); + }) ->groupBy('users.id') ->orderByDesc('force_active') ->orderByDesc('shift_length') @@ -215,7 +226,7 @@ function admin_active() $shirtSize = $usr->personalData->shirt_size; $userData = []; $userData['no'] = count($matched_users) + 1; - $userData['nick'] = User_Nick_render($usr) . User_Pronoun_render($usr); + $userData['nick'] = User_Nick_render($usr) . User_Pronoun_render($usr) . user_info_icon($usr); if ($goodie_tshirt) { $userData['shirt_size'] = (isset($tshirt_sizes[$shirtSize]) ? $tshirt_sizes[$shirtSize] : ''); } @@ -236,8 +247,8 @@ function admin_active() $parameters['show_all_shifts'] = 1; } $actions[] = form( - [form_submit('submit', __('set active'), 'btn-sm', false, 'secondary')], - page_link_to('admin_active', $parameters), + [form_submit('submit', icon('plus-lg') . __('set active'), 'btn-sm', false, 'secondary')], + url('/admin-active', $parameters), false, true ); @@ -251,8 +262,8 @@ function admin_active() $parametersRemove['show_all_shifts'] = 1; } $actions[] = form( - [form_submit('submit', __('remove active'), 'btn-sm', false, 'secondary')], - page_link_to('admin_active', $parametersRemove), + [form_submit('submit', icon('dash-lg') . __('Remove active'), 'btn-sm', false, 'secondary')], + url('/admin-active', $parametersRemove), false, true ); @@ -268,8 +279,8 @@ function admin_active() if ($goodie_enabled) { $actions[] = form( - [form_submit('submit', ($goodie_tshirt ? __('got t-shirt') : __('got goodie')), 'btn-sm', false, 'secondary')], - page_link_to('admin_active', $parametersShirt), + [form_submit('submit', icon('person') . ($goodie_tshirt ? __('Got T-shirt') : __('Got goodie')), 'btn-sm', false, 'secondary')], + url('/admin-active', $parametersShirt), false, true ); @@ -286,8 +297,8 @@ function admin_active() if ($goodie_enabled) { $actions[] = form( - [form_submit('submit', ($goodie_tshirt ? __('remove t-shirt') : __('remove goodie')), 'btn-sm', false, 'secondary')], - page_link_to('admin_active', $parameters), + [form_submit('submit', icon('person') . ($goodie_tshirt ? __('Remove T-shirt') : __('Remove goodie')), 'btn-sm', false, 'secondary')], + url('/admin-active', $parameters), false, true ); @@ -295,7 +306,7 @@ function admin_active() } if ($goodie_tshirt) { - $actions[] = button(url('/admin/user/' . $usr->id . '/goodie'), __('form.edit'), 'btn-secondary btn-sm'); + $actions[] = button(url('/admin/user/' . $usr->id . '/goodie'), icon('pencil') . __('form.edit'), 'btn-secondary btn-sm'); } $userData['actions'] = buttons($actions); @@ -328,18 +339,18 @@ function admin_active() form([ form_text('search', __('Search angel:'), $search), form_checkbox('show_all_shifts', __('Show all shifts'), $show_all_shifts), - form_submit('submit', __('Search')), - ], page_link_to('admin_active')), + form_submit('submit', icon('search') . __('form.search')), + ], url('/admin-active')), $set_active == '' ? form([ form_text('count', __('How much angels should be active?'), $count ?: $forced_count), - form_submit('set_active', __('Preview')), + form_submit('set_active', icon('eye') . __('form.preview'), 'btn-info'), ]) : $set_active, $msg . msg(), table( array_merge( [ 'no' => __('No.'), - 'nick' => __('Name'), + 'nick' => __('general.name'), ], ($goodie_tshirt ? ['shirt_size' => __('Size')] : []), [ @@ -350,15 +361,15 @@ function admin_active() ], ($goodie_enabled ? ['tshirt' => ($goodie_tshirt ? __('T-shirt?') : __('Goodie?'))] : []), [ - 'actions' => '', + 'actions' => __('general.actions'), ] ), $matched_users ), - $goodie_enabled ? '

' . ($goodie_tshirt ? __('Shirt statistic') : __('Goodie statistic')) . '

' : '', + $goodie_enabled ? '

' . ($goodie_tshirt ? __('T-shirt statistic') : __('Goodie statistic')) . '

' : '', $goodie_enabled ? table(array_merge( ($goodie_tshirt ? ['size' => __('Size')] : []), - ['given' => $goodie_tshirt ? __('Given shirts') : __('Given goodies') ] + ['given' => $goodie_tshirt ? __('Given T-shirts') : __('Given goodies') ] ), $goodie_statistics) : '', ]); } diff --git a/includes/pages/admin_arrive.php b/includes/pages/admin_arrive.php index 31a66707e..51a78d785 100644 --- a/includes/pages/admin_arrive.php +++ b/includes/pages/admin_arrive.php @@ -65,7 +65,7 @@ function admin_arrive() } /** @var User[] $users */ - $users = User::with('personalData')->orderBy('name')->get(); + $users = User::with(['personalData', 'state'])->orderBy('name')->get(); $arrival_count_at_day = []; $planned_arrival_count_at_day = []; $planned_departure_count_at_day = []; @@ -78,7 +78,13 @@ function admin_arrive() foreach ($users as $usr) { if (count($tokens) > 0) { $match = false; - $index = join(' ', $usr->attributesToArray()); + $data = collect($usr->toArray())->flatten()->filter(function ($value) { + // Remove empty values + return !empty($value) && + // Skip datetime + !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}Z$/', (string) $value); + }); + $index = join(' ', $data->toArray()); foreach ($tokens as $token) { $token = trim($token); if (!empty($token) && stristr($index, $token)) { @@ -92,50 +98,66 @@ function admin_arrive() } } - $usr->name = User_Nick_render($usr) . User_Pronoun_render($usr); + $usr->name = User_Nick_render($usr) + . User_Pronoun_render($usr) + . user_info_icon($usr); $plannedDepartureDate = $usr->personalData->planned_departure_date; $arrivalDate = $usr->state->arrival_date; $plannedArrivalDate = $usr->personalData->planned_arrival_date; $usr['rendered_planned_departure_date'] = $plannedDepartureDate - ? $plannedDepartureDate->format(__('Y-m-d')) + ? $plannedDepartureDate->format(__('general.date')) : '-'; - $usr['rendered_planned_arrival_date'] = $plannedArrivalDate ? $plannedArrivalDate->format(__('Y-m-d')) : '-'; - $usr['rendered_arrival_date'] = $arrivalDate ? $arrivalDate->format(__('Y-m-d')) : '-'; + $usr['rendered_planned_arrival_date'] = $plannedArrivalDate ? $plannedArrivalDate->format(__('general.date')) : '-'; + $usr['rendered_arrival_date'] = $arrivalDate ? $arrivalDate->format(__('general.date')) : '-'; $usr['arrived'] = icon_bool($usr->state->arrived); $usr['actions'] = form([ form_hidden('action', $usr->state->arrived ? 'reset' : 'arrived'), form_hidden('user', $usr->id), form_submit( 'submit', - $usr->state->arrived ? __('reset') : __('arrived'), + $usr->state->arrived + ? icon('arrow-counterclockwise') + : icon('house'), 'btn-sm', true, - $usr->state->arrived ? 'secondary' : 'primary' + $usr->state->arrived ? 'secondary' : 'primary', + $usr->state->arrived + ? __('Reset') + : __('user.arrive') ), ]); if ($usr->state->arrival_date) { - $day = $usr->state->arrival_date->format(__('Y-m-d')); + $day = $usr->state->arrival_date->format('Y-m-d'); if (!isset($arrival_count_at_day[$day])) { - $arrival_count_at_day[$day] = 0; + $arrival_count_at_day[$day] = [ + 'day' => $usr->state->arrival_date, + 'count' => 0, + ]; } - $arrival_count_at_day[$day]++; + $arrival_count_at_day[$day]['count']++; } if ($usr->personalData->planned_arrival_date) { - $day = $usr->personalData->planned_arrival_date->format(__('Y-m-d')); + $day = $usr->personalData->planned_arrival_date->format('Y-m-d'); if (!isset($planned_arrival_count_at_day[$day])) { - $planned_arrival_count_at_day[$day] = 0; + $planned_arrival_count_at_day[$day] = [ + 'day' => $usr->personalData->planned_arrival_date, + 'count' => 0, + ]; } - $planned_arrival_count_at_day[$day]++; + $planned_arrival_count_at_day[$day]['count']++; } if ($usr->personalData->planned_departure_date && $usr->state->arrived) { - $day = $usr->personalData->planned_departure_date->format(__('Y-m-d')); + $day = $usr->personalData->planned_departure_date->format('Y-m-d'); if (!isset($planned_departure_count_at_day[$day])) { - $planned_departure_count_at_day[$day] = 0; + $planned_departure_count_at_day[$day] = [ + 'day' => $usr->personalData->planned_departure_date, + 'count' => 0, + ]; } - $planned_departure_count_at_day[$day]++; + $planned_departure_count_at_day[$day]['count']++; } $users_matched[] = $usr; @@ -147,33 +169,33 @@ function admin_arrive() $arrival_at_day = []; $arrival_sum = 0; - foreach ($arrival_count_at_day as $day => $count) { - $arrival_sum += $count; + foreach ($arrival_count_at_day as $day => $entry) { + $arrival_sum += $entry['count']; $arrival_at_day[$day] = [ - 'day' => $day, - 'count' => $count, + 'day' => $entry['day']->format(__('general.date')), + 'count' => $entry['count'], 'sum' => $arrival_sum, ]; } $planned_arrival_at_day = []; $planned_arrival_sum = 0; - foreach ($planned_arrival_count_at_day as $day => $count) { - $planned_arrival_sum += $count; + foreach ($planned_arrival_count_at_day as $day => $entry) { + $planned_arrival_sum += $entry['count']; $planned_arrival_at_day[$day] = [ - 'day' => $day, - 'count' => $count, + 'day' => $entry['day']->format(__('general.date')), + 'count' => $entry['count'], 'sum' => $planned_arrival_sum, ]; } $planned_departure_at_day = []; $planned_departure_sum = 0; - foreach ($planned_departure_count_at_day as $day => $count) { - $planned_departure_sum += $count; + foreach ($planned_departure_count_at_day as $day => $entry) { + $planned_departure_sum += $entry['count']; $planned_departure_at_day[$day] = [ - 'day' => $day, - 'count' => $count, + 'day' => $entry['day']->format(__('general.date')), + 'count' => $entry['count'], 'sum' => $planned_departure_sum, ]; } @@ -181,60 +203,60 @@ function admin_arrive() return page_with_title(admin_arrive_title(), [ $msg . msg(), form([ - form_text('search', __('Search'), $search), - form_submit('submit', __('Search')), - ], page_link_to('admin_arrive')), + form_text('search', __('form.search'), $search), + form_submit('submit', icon('search') . __('form.search')), + ], url('/admin-arrive')), table([ - 'name' => __('Name'), + 'name' => __('general.name'), 'rendered_planned_arrival_date' => __('Planned arrival'), 'arrived' => __('Arrived?'), 'rendered_arrival_date' => __('Arrival date'), 'rendered_planned_departure_date' => __('Planned departure'), - 'actions' => '', + 'actions' => __('general.actions'), ], $users_matched), div('row', [ div('col-md-4', [ heading(__('Planned arrival statistics'), 3), BarChart::render([ - 'count' => __('arrived'), + 'count' => __('user.arrived'), 'sum' => __('arrived sum'), ], [ 'count' => '#090', 'sum' => '#888', ], $planned_arrival_at_day), table([ - 'day' => __('Date'), - 'count' => __('Count'), + 'day' => __('title.date'), + 'count' => __('general.count'), 'sum' => __('Sum'), ], $planned_arrival_at_day), ]), div('col-md-4', [ heading(__('Arrival statistics'), 3), BarChart::render([ - 'count' => __('arrived'), + 'count' => __('user.arrived'), 'sum' => __('arrived sum'), ], [ 'count' => '#090', 'sum' => '#888', ], $arrival_at_day), table([ - 'day' => __('Date'), - 'count' => __('Count'), + 'day' => __('title.date'), + 'count' => __('general.count'), 'sum' => __('Sum'), ], $arrival_at_day), ]), div('col-md-4', [ heading(__('Planned departure statistics'), 3), BarChart::render([ - 'count' => __('arrived'), + 'count' => __('user.arrived'), 'sum' => __('arrived sum'), ], [ 'count' => '#090', 'sum' => '#888', ], $planned_departure_at_day), table([ - 'day' => __('Date'), - 'count' => __('Count'), + 'day' => __('title.date'), + 'count' => __('general.count'), 'sum' => __('Sum'), ], $planned_departure_at_day), ]), diff --git a/includes/pages/admin_free.php b/includes/pages/admin_free.php index 7fd17faae..d5b5db669 100644 --- a/includes/pages/admin_free.php +++ b/includes/pages/admin_free.php @@ -40,7 +40,7 @@ function admin_free() /** @var User[] $users */ $users = []; if ($request->has('submit')) { - $query = User::with('personalData') + $query = User::with(['personalData', 'contact', 'state']) ->select('users.*') ->leftJoin('shift_entries', 'users.id', 'shift_entries.user_id') ->leftJoin('users_state', 'users.id', 'users_state.user_id') @@ -97,16 +97,18 @@ function admin_free() $email = $usr->contact->email ?: $usr->email; $free_users_table[] = [ - 'name' => User_Nick_render($usr) . User_Pronoun_render($usr), + 'name' => User_Nick_render($usr) + . User_Pronoun_render($usr) + . user_info_icon($usr), 'shift_state' => User_shift_state_render($usr), 'last_shift' => User_last_shift_render($usr), - 'dect' => sprintf('%1$s', $usr->contact->dect), + 'dect' => sprintf('%1$s', htmlspecialchars((string) $usr->contact->dect)), 'email' => $usr->settings->email_human - ? sprintf('%1$s', $email) + ? sprintf('%1$s', htmlspecialchars((string) $email)) : icon('eye-slash'), 'actions' => auth()->can('admin_user') - ? button(page_link_to('admin_user', ['id' => $usr->id]), icon('pencil') . __('edit'), 'btn-sm') + ? button(url('/admin-user', ['id' => $usr->id]), icon('pencil'), 'btn-sm', '', __('form.edit')) : '', ]; } @@ -115,19 +117,19 @@ function admin_free() div('row', [ div('col-md-12 form-inline', [ div('row', [ - form_text('search', __('Search'), $search, null, null, null, 'col'), + form_text('search', __('form.search'), $search, null, null, null, 'col'), form_select('angeltype', __('Angeltype'), $angel_types, $angelType, '', 'col'), - form_submit('submit', __('Search')), + form_submit('submit', icon('search') . __('form.search')), ]), ]), ]), ]), table([ - 'name' => __('Name'), - 'shift_state' => __('Next shift'), + 'name' => __('general.name'), + 'shift_state' => __('shift.next'), 'last_shift' => __('Last shift'), - 'dect' => __('DECT'), - 'email' => __('E-Mail'), + 'dect' => __('general.dect'), + 'email' => __('general.email'), 'actions' => '', ], $free_users_table), ]); diff --git a/includes/pages/admin_groups.php b/includes/pages/admin_groups.php index 5e46d5ba0..62ea115a5 100644 --- a/includes/pages/admin_groups.php +++ b/includes/pages/admin_groups.php @@ -21,28 +21,31 @@ function admin_groups() $html = ''; $request = request(); /** @var Group[]|Collection $groups */ - $groups = Group::query()->orderBy('name')->get(); + $groups = Group::with('privileges')->orderBy('name')->get(); if (!$request->has('action')) { $groups_table = []; foreach ($groups as $group) { /** @var Privilege[]|Collection $privileges */ - $privileges = $group->privileges()->orderBy('name')->get(); + $privileges = $group->privileges->sortBy('name'); $privileges_html = []; foreach ($privileges as $privilege) { - $privileges_html[] = $privilege['name']; + $privileges_html[] = htmlspecialchars($privilege['name']); } $groups_table[] = [ - 'name' => $group->name, + 'name' => htmlspecialchars($group->name), 'privileges' => join(', ', $privileges_html), 'actions' => button( - page_link_to( - 'admin_groups', + url( + '/admin-groups', ['action' => 'edit', 'id' => $group->id] ), - icon('pencil') . __('edit'), + icon('pencil'), + '', + '', + __('form.edit'), 'btn-sm' ), ]; @@ -50,7 +53,7 @@ function admin_groups() return page_with_title(admin_groups_title(), [ table([ - 'name' => __('Name'), + 'name' => __('general.name'), 'privileges' => __('Privileges'), 'actions' => '', ], $groups_table), @@ -72,18 +75,18 @@ function admin_groups() foreach ($privileges as $privilege) { $privileges_form[] = form_checkbox( 'privileges[]', - $privilege->description . ' (' . $privilege->name . ')', + htmlspecialchars($privilege->description . ' (' . $privilege->name . ')'), $privilege->selected != '', $privilege->id, - 'privilege-' . $privilege->name + 'privilege-' . htmlspecialchars($privilege->name) ); } - $privileges_form[] = form_submit('submit', __('Save')); - $html .= page_with_title(__('Edit group') . ' ' . $group->name, [ + $privileges_form[] = form_submit('submit', icon('save') . __('form.save')); + $html .= page_with_title(__('Edit group') . ' ' . htmlspecialchars($group->name), [ form( $privileges_form, - page_link_to('admin_groups', ['action' => 'save', 'id' => $group->id]) + url('/admin-groups', ['action' => 'save', 'id' => $group->id]) ), ]); } else { @@ -118,7 +121,7 @@ function admin_groups() 'Group privileges of group ' . $group->name . ' edited: ' . join(', ', $privilege_names) ); - throw_redirect(page_link_to('admin_groups')); + throw_redirect(url('/admin-groups')); } else { return error('No Group found.', true); } diff --git a/includes/pages/admin_rooms.php b/includes/pages/admin_rooms.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/includes/pages/admin_shifts.php b/includes/pages/admin_shifts.php index c4fc29bc7..f435cec2f 100644 --- a/includes/pages/admin_shifts.php +++ b/includes/pages/admin_shifts.php @@ -1,15 +1,11 @@ get(); - $room_array = $rooms->pluck('name', 'id')->toArray(); + $locations = Location::orderBy('name')->get(); + $location_array = $locations->pluck('name', 'id')->toArray(); // Load angeltypes /** @var AngelType[] $types */ @@ -85,14 +81,14 @@ function admin_shifts() // Auswahl der sichtbaren Locations fΓΌr die Schichten if ( - $request->has('rid') - && preg_match('/^\d+$/', $request->input('rid')) - && isset($room_array[$request->input('rid')]) + $request->has('lid') + && preg_match('/^\d+$/', $request->input('lid')) + && isset($location_array[$request->input('lid')]) ) { - $rid = $request->input('rid'); + $lid = $request->input('lid'); } else { $valid = false; - $rid = $rooms->first()->id; + $lid = $locations->first()?->id ?? 0; error(__('Please select a location.')); } @@ -139,6 +135,29 @@ function admin_shifts() 'trim', explode(',', $request->input('change_hours')) ); + // Fehlende Minutenangaben ergΓ€nzen, 24 Uhr -> 00 Uhr + array_walk($change_hours, function (&$value) use ($valid) { + // Add minutes + if (!preg_match('/^(\d{1,2}):\d{2}$/', $value)) { + $value .= ':00'; + } + // Add 0 before low hours + if (preg_match('/^\d:\d{2}$/', $value)) { + $value = '0' . $value; + } + // Fix 24:00 + if ($value == '24:00') { + $value = '00:00'; + } + }); + // Ensure valid time in change hours + foreach ($change_hours as $change_hour) { + if (!preg_match('/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/', $change_hour)) { + $valid = false; + error(sprintf(__('Please validate the change hour %s. It should be between 00:00 and 24:00.'), $change_hour)); + } + } + $change_hours = array_unique($change_hours); } else { $valid = false; error(__('Please split the shift-change hours by colons.')); @@ -152,7 +171,9 @@ function admin_shifts() } if ($request->has('angelmode')) { - if ($request->input('angelmode') == 'location') { + if ($request->input('angelmode') == 'shift_type') { + $angelmode = 'shift_type'; + } elseif ($request->input('angelmode') == 'location') { $angelmode = 'location'; } elseif ($request->input('angelmode') == 'manually') { foreach ($types as $type) { @@ -163,15 +184,23 @@ function admin_shifts() error(sprintf(__('Please check the needed angels for team %s.'), $type->name)); } } - - if (array_sum($needed_angel_types) == 0) { - $valid = false; - error(__('There are 0 angels needed. Please enter the amounts of needed angels.')); - } } else { $valid = false; error(__('Please select a mode for needed angels.')); } + + if ( + $angelmode == 'manually' && array_sum($needed_angel_types) == 0 + || $angelmode == 'location' && !NeededAngelType::whereLocationId($lid) + ->where('count', '>', '0') + ->count() + || $angelmode == 'shift_type' && !NeededAngelType::whereShiftTypeId($shifttype_id) + ->where('count', '>', '0') + ->count() + ) { + $valid = false; + error(__('There are 0 angels needed. Please enter the amounts of needed angels.')); + } } else { $valid = false; error(__('Please select needed angels.')); @@ -184,8 +213,12 @@ function admin_shifts() // Alle Eingaben in Ordnung if ($valid) { - if ($angelmode == 'location') { - $needed_angel_types = NeededAngelType::whereRoomId($rid) + if ($angelmode == 'shift_type') { + $needed_angel_types = NeededAngelType::whereShiftTypeId($shifttype_id) + ->pluck('count', 'angel_type_id') + ->toArray() + $needed_angel_types; + } elseif ($angelmode == 'location') { + $needed_angel_types = NeededAngelType::whereLocationId($lid) ->pluck('count', 'angel_type_id') ->toArray() + $needed_angel_types; } @@ -195,7 +228,7 @@ function admin_shifts() $shifts[] = [ 'start' => $start, 'end' => $end, - 'room_id' => $rid, + 'location_id' => $lid, 'title' => $title, 'shift_type_id' => $shifttype_id, 'description' => $description, @@ -215,7 +248,7 @@ function admin_shifts() $shifts[] = [ 'start' => $shift_start, 'end' => $shift_end, - 'room_id' => $rid, + 'location_id' => $lid, 'title' => $title, 'shift_type_id' => $shifttype_id, 'description' => $description, @@ -224,13 +257,6 @@ function admin_shifts() $shift_start = $shift_end; } while ($shift_end < $end); } elseif ($mode == 'variable') { - // Fehlende Minutenangaben ergΓ€nzen - array_walk($change_hours, function (&$value) { - if (!preg_match('/^\d{1,2}:\d{2}$/', $value)) { - $value .= ':00'; - } - }); - // Alle Tage durchgehen $end_day = Carbon::createFromDatetime($end->format('Y-m-d') . ' 00:00'); $day = Carbon::createFromDatetime($start->format('Y-m-d') . ' 00:00'); @@ -238,10 +264,10 @@ function admin_shifts() // Alle Schichtwechselstunden durchgehen for ($i = 0; $i < count($change_hours); $i++) { $start_hour = $change_hours[$i]; - if ($i < count($change_hours) - 1) { + if (isset($change_hours[$i + 1])) { // Normales Intervall zwischen zwei Schichtwechselstunden $end_hour = $change_hours[$i + 1]; - } elseif ($shift_over_midnight) { + } elseif ($shift_over_midnight && $day != $end_day) { // Letzte Schichtwechselstunde: Wenn eine 24h Abdeckung gewΓΌnscht ist, // hier die erste Schichtwechselstunde als Ende einsetzen $end_hour = $change_hours[0]; @@ -283,7 +309,7 @@ function admin_shifts() $shifts[] = [ 'start' => $interval_start, 'end' => $interval_end, - 'room_id' => $rid, + 'location_id' => $lid, 'title' => $title, 'shift_type_id' => $shifttype_id, 'description' => $description, @@ -300,17 +326,27 @@ function admin_shifts() $shifts_table = []; foreach ($shifts as $shift) { + $shiftType = $shifttypes_source->find($shift['shift_type_id']); + $location = $locations->find($shift['location_id']); + + /** @var Carbon $start */ + $start = $shift['start']; + /** @var Carbon $end */ + $end = $shift['end']; $shifts_table_entry = [ 'timeslot' => icon('clock-history') . ' ' - . $shift['start']->format(__('Y-m-d H:i')) + . $start->format(__('general.datetime')) . ' - ' - . $shift['end']->format(__('H:i')) - . '
' - . Room_name_render(Room::find($shift['room_id'])), + . '' + . $end->format(__('H:i')) + . '' + . ', ' . round($end->copy()->diffInMinutes($start) / 60, 2) . 'h' + . '
' + . location_name_render($location), 'title' => - ShiftType_name_render(ShiftType::find($shifttype_id)) - . ($shift['title'] ? '
' . $shift['title'] : ''), + htmlspecialchars($shiftType->name) + . ($shift['title'] ? '
' . htmlspecialchars($shift['title']) : ''), 'needed_angels' => '', ]; foreach ($types as $type) { @@ -326,9 +362,9 @@ function admin_shifts() $session->set('admin_shifts_shifts', $shifts); $session->set('admin_shifts_types', $needed_angel_types); - $hidden_types = ''; + $previousEntries = []; foreach ($needed_angel_types as $type_id => $count) { - $hidden_types .= form_hidden('angeltype_count_' . $type_id, $count); + $previousEntries['angeltype_count_' . $type_id] = $count; } // Number of Shifts that will be created (if over 100 its danger-red) @@ -338,28 +374,38 @@ function admin_shifts() $shiftsCreationHint = '' . $shiftsCreationHint . ''; } - return page_with_title(__('Preview'), [ + // Save as previous state to be able to reuse it + $previousEntries += [ + 'shifttype_id' => $shifttype_id, + 'description' => $description, + 'title' => $title, + 'lid' => $lid, + 'start' => $request->input('start'), + 'end' => $request->input('end'), + 'mode' => $mode, + 'length' => $length, + 'change_hours' => implode(', ', $change_hours), + 'angelmode' => $angelmode, + 'shift_over_midnight' => $shift_over_midnight ? 'true' : 'false', + ]; + $session->set('admin_shifts_previous', $previousEntries); + + $hidden_types = ''; + foreach ($previousEntries as $name => $value) { + $hidden_types .= form_hidden($name, $value); + } + + return page_with_title(__('form.preview'), [ form([ $hidden_types, - form_hidden('shifttype_id', $shifttype_id), - form_hidden('description', $description), - form_hidden('title', $title), - form_hidden('rid', $rid), - form_hidden('start', $start->format('Y-m-d H:i')), - form_hidden('end', $end->format('Y-m-d H:i')), - form_hidden('mode', $mode), - form_hidden('length', $length), - form_hidden('change_hours', implode(', ', $change_hours)), - form_hidden('angelmode', $angelmode), - form_hidden('shift_over_midnight', $shift_over_midnight ? 'true' : 'false'), - form_submit('back', icon('chevron-left') . __('back')), + form_submit('back', icon('chevron-left') . __('general.back')), $shiftsCreationHint, table([ 'timeslot' => __('Time and location'), 'title' => __('Type and title'), 'needed_angels' => __('Needed angels'), ], $shifts_table), - form_submit('submit', icon('save') . __('Save')), + form_submit('submit', icon('save') . __('form.save')), ]), ]); } @@ -368,7 +414,7 @@ function admin_shifts() !is_array($session->get('admin_shifts_shifts')) || !is_array($session->get('admin_shifts_types')) ) { - throw_redirect(page_link_to('admin_shifts')); + throw_redirect(url('/admin-shifts')); } $transactionId = Str::uuid(); @@ -379,15 +425,6 @@ function admin_shifts() $shift->createdBy()->associate(auth()->user()); $shift->save(); - engelsystem_log( - 'Shift created: ' . $shifttypes[$shift->shift_type_id] - . ' with title ' . $shift->title - . ' with description ' . $shift->description - . ' from ' . $shift->start->format('Y-m-d H:i') - . ' to ' . $shift->end->format('Y-m-d H:i') - . ', transaction: ' . $transactionId - ); - $needed_angel_types_info = []; foreach ($session->get('admin_shifts_types', []) as $type_id => $count) { $angel_type_source = AngelType::find($type_id); @@ -401,26 +438,37 @@ function admin_shifts() $needed_angel_types_info[] = $angel_type_source->name . ': ' . $count; } } - engelsystem_log('Shift needs following angel types: ' . join(', ', $needed_angel_types_info)); + + engelsystem_log( + 'Shift created: ' . $shifttypes[$shift->shift_type_id] + . ' (' . $shift->id . ')' + . ' with title ' . $shift->title + . ' and description ' . $shift->description + . ' from ' . $shift->start->format('Y-m-d H:i') + . ' to ' . $shift->end->format('Y-m-d H:i') + . ' in ' . $shift->location->name + . ' with angel types: ' . join(', ', $needed_angel_types_info) + . ', transaction: ' . $transactionId + ); } success('Shifts created.'); - throw_redirect(page_link_to('admin_shifts')); + throw_redirect(url('/admin-shifts')); } else { $session->remove('admin_shifts_shifts'); $session->remove('admin_shifts_types'); } - $rid = null; - if ($request->has('rid')) { - $rid = $request->input('rid'); + $lid = null; + if ($request->has('lid')) { + $lid = $request->input('lid'); } $angel_types = ''; foreach ($types as $type) { $angel_types .= '
' . form_spinner( 'angeltype_count_' . $type->id, - $type->name, + htmlspecialchars($type->name), $needed_angel_types[$type->id], [ 'radio-name' => 'angelmode', @@ -430,20 +478,36 @@ function admin_shifts() . '
'; } + $link = button(url('/user-shifts'), icon('chevron-left'), 'btn-sm', '', __('general.back')); + $reset = ''; + if ($session->has('admin_shifts_previous')) { + $reset = form_submit( + 'back', + icon('arrow-counterclockwise'), + '', + false, + 'link', + __('Reset to previous state') + ); + foreach ($session->get('admin_shifts_previous', []) as $name => $value) { + $reset .= form_hidden($name, $value); + } + } + return page_with_title( - admin_shifts_title() . ' ' . sprintf( + $link . ' ' . admin_shifts_title() . ' ' . sprintf( '%s', - page_link_to('admin_shifts_history'), + url('/admin/shifts/history'), icon('clock-history') - ), + ) . form([$reset], '', 'display:inline'), [ msg(), form([ div('row', [ div('col-md-6 col-xl-5', [ form_select('shifttype_id', __('Shifttype'), $shifttypes, $shifttype_id), - form_text('title', __('Title'), $title), - form_select('rid', __('Room'), $room_array, $rid), + form_text('title', __('title.title'), $title), + form_select('lid', __('Location'), $location_array, $lid), ]), div('col-md-6 col-xl-7', [ form_textarea('description', __('Additional description'), $description), @@ -454,10 +518,22 @@ function admin_shifts() div('col-md-6 col-xl-5', [ div('row', [ div('col-lg-6', [ - form_datetime('start', __('Start'), $start), + form_datetime( + 'start', + __('shifts.start'), + $request->has('start') + ? Carbon::createFromDatetime($request->input('start')) + : $start + ), ]), div('col-lg-6', [ - form_datetime('end', __('End'), $end), + form_datetime( + 'end', + __('shifts.end'), + $request->has('end') + ? Carbon::createFromDatetime($request->input('end')) + : $end + ), ]), ]), form_info(__('Mode')), @@ -488,7 +564,7 @@ function admin_shifts() 'change_hours', __('Shift change hours'), $request->has('change_hours') - ? $request->input('change_hours') + ? ($change_hours ? implode(', ', $change_hours) : $request->input('change_hours')) : '00, 04, 08, 10, 12, 14, 16, 18, 20, 22', false, null, @@ -509,7 +585,13 @@ function admin_shifts() form_info(__('Needed angels')), form_radio( 'angelmode', - __('Take needed angels from room settings'), + __('Copy needed angels from shift type settings'), + $angelmode == 'shift_type', + 'shift_type' + ), + form_radio( + 'angelmode', + __('Copy needed angels from location settings'), $angelmode == 'location', 'location' ), @@ -524,104 +606,8 @@ function admin_shifts() ]), ]), ]), - form_submit('preview', icon('search') . __('Preview')), + form_submit('preview', icon('eye') . __('form.preview'), 'btn-info'), ]), ] ); } - -function admin_shifts_history_title(): string -{ - return __('Shifts history'); -} - -/** - * Display shifts transaction history - * - * @return string - */ -function admin_shifts_history(): string -{ - if (!auth()->can('admin_shifts')) { - throw new HttpForbidden(); - } - - $request = request(); - $transactionId = $request->postData('transaction_id'); - if ($request->hasPostData('delete') && $transactionId) { - $shifts = Shift::whereTransactionId($transactionId)->get(); - - engelsystem_log('Deleting ' . count($shifts) . ' shifts (transaction id ' . $transactionId . ')'); - - foreach ($shifts as $shift) { - $shift = Shift($shift); - foreach ($shift->shiftEntries as $entry) { - event('shift.entry.deleting', [ - 'user' => $entry->user, - 'start' => $shift->start, - 'end' => $shift->end, - 'name' => $shift->shiftType->name, - 'title' => $shift->title, - 'type' => $entry->angelType->name, - 'room' => $shift->room, - 'freeloaded' => $entry->freeloaded, - ]); - } - - $shift->delete(); - - engelsystem_log( - 'Deleted shift ' . $shift->title . ' / ' . $shift->shiftType->name - . ' from ' . $shift->start->format('Y-m-d H:i') - . ' to ' . $shift->end->format('Y-m-d H:i') - ); - } - - success(sprintf(__('%s shifts deleted.'), count($shifts))); - throw_redirect(page_link_to('admin_shifts_history')); - } - - $schedules = Schedule::all()->pluck('name', 'id')->toArray(); - $shiftsData = Db::select(' - SELECT - s.transaction_id, - s.title, - schedule_shift.schedule_id, - COUNT(s.id) AS count, - MIN(s.start) AS start, - MAX(s.end) AS end, - s.created_by AS user_id, - MAX(s.created_at) AS created_at - FROM shifts AS s - LEFT JOIN schedule_shift on schedule_shift.shift_id = s.id - WHERE s.transaction_id IS NOT NULL - GROUP BY s.transaction_id - ORDER BY created_at DESC - '); - - foreach ($shiftsData as &$shiftData) { - $shiftData['title'] = $shiftData['schedule_id'] ? __('shifts_history.schedule', [$schedules[$shiftData['schedule_id']]]) : $shiftData['title']; - $shiftData['user'] = User_Nick_render(User::find($shiftData['user_id'])); - $shiftData['start'] = Carbon::make($shiftData['start'])->format(__('Y-m-d H:i')); - $shiftData['end'] = Carbon::make($shiftData['end'])->format(__('Y-m-d H:i')); - $shiftData['created_at'] = Carbon::make($shiftData['created_at'])->format(__('Y-m-d H:i')); - $shiftData['actions'] = form([ - form_hidden('transaction_id', $shiftData['transaction_id']), - form_submit('delete', icon('trash') . __('delete all'), 'btn-sm', true, 'danger'), - ]); - } - - return page_with_title(admin_shifts_history_title(), [ - msg(), - table([ - 'transaction_id' => __('ID'), - 'title' => __('Title'), - 'count' => __('Count'), - 'start' => __('Start'), - 'end' => __('End'), - 'user' => __('User'), - 'created_at' => __('Created'), - 'actions' => '', - ], $shiftsData), - ], true); -} diff --git a/includes/pages/admin_user.php b/includes/pages/admin_user.php index 93ac2f099..5ea82bb40 100644 --- a/includes/pages/admin_user.php +++ b/includes/pages/admin_user.php @@ -1,6 +1,7 @@ can('user.info.edit'); + $user_edit_shirt = auth()->can('user.edit.shirt'); + $user_edit = auth()->can('user.edit'); + $admin_arrive = auth()->can('admin_arrive'); if (!$request->has('id')) { throw_redirect(users_link()); @@ -40,77 +45,110 @@ function admin_user() } $html .= __('Here you can change the user entry. Under the item \'Arrived\' the angel is marked as present, a yes at Active means that the angel was active.'); - if ($goodie_enabled) { + if ($goodie_enabled && $user_edit_shirt) { if ($goodie_tshirt) { $html .= ' ' . __('If the angel is active, it can claim a T-shirt. If T-shirt is set to \'Yes\', the angel already got their T-shirt.'); } else { $html .= ' ' . __('If the angel is active, it can claim a goodie. If goodie is set to \'Yes\', the angel already got their goodie.'); } } - $html .= '

'; + $html .= '

'; $html .= '
' . "\n"; $html .= form_csrf(); $html .= '' . "\n"; $html .= '' . "\n"; $html .= ''; $html .= '' . "\n"; - $html .= '
' . "\n"; $html .= '' . "\n"; - $html .= ' ' . "\n"; + $html .= ' ' . "\n"; $html .= ' ' . "\n"; if (config('enable_user_name')) { - $html .= ' ' . "\n"; - $html .= ' ' . "\n"; + $html .= ' ' . "\n"; + $html .= ' ' . "\n"; } - $html .= ' ' . "\n"; + $html .= ' ' . "\n"; if (config('enable_dect')) { - $html .= ' ' . "\n"; + $html .= ' ' . "\n"; } if ($user_source->settings->email_human) { - $html .= ' ' . "\n"; + $html .= ' ' . "\n"; } - if ($goodie_tshirt) { + if ($goodie_tshirt && $user_edit_shirt) { $html .= ' ' . "\n"; } + // User info + if ($user_info_edit) { + $html .= ' ' . "\n"; + } + $options = [ '1' => __('Yes'), '0' => __('No'), ]; - // Gekommen? - $html .= ' ' . "\n"; } else { - $html .= __('No'); + $html .= ($user_source->state->arrived ? __('Yes') : __('No')); + $html .= '' . "\n"; } - $html .= '' . "\n"; - // Aktiv? - $html .= ' ' . "\n"; + // Active? + if ($user_edit_shirt) { + $html .= ' ' . "\n"; + } else { + $html .= ' ' . "\n"; + } - // Aktiv erzwingen + // Forced active? if (auth()->can('admin_active')) { $html .= ' ' . "\n"; } - if ($goodie_enabled) { + if ($goodie_enabled && $user_edit_shirt) { // T-Shirt bekommen? if ($goodie_tshirt) { - $html .= '
' . __('Nickname') . '' . '
' . __('general.nick') . '' + . '' + . '
' . __('Last login') . '

' - . ($user_source->last_login_at ? $user_source->last_login_at->format(__('Y-m-d H:i')) : '-') + . ($user_source->last_login_at ? $user_source->last_login_at->format(__('general.datetime')) : '-') . '

' . __('Prename') . '' . '
' . __('Last name') . '' . '
' . __('settings.profile.firstname') . '' + . '' + . '
' . __('settings.profile.lastname') . '' + . '' + . '
' . __('Mobile') . '' . '
' . __('settings.profile.mobile') . '' + . '' + . '
' . __('DECT') . '' . '
' . __('general.dect') . '' + . '' + . '
' . __('settings.profile.email') . '' . '
' . __('general.email') . '' + . '' + . '
' . __('user.shirt_size') . '' . html_select_key( 'size', 'eSize', $tshirt_sizes, $user_source->personalData->shirt_size, - __('Please select...') + __('form.select_placeholder') ) . '
' + . __('user.info') + . ' ' + . '' + . '' + . '
' . __('Arrived') . '' . "\n"; - if ($user_source->state->arrived) { - $html .= __('Yes'); + // Arrived? + $html .= '
' . __('user.arrived') . '' . "\n"; + if ($admin_arrive) { + $html .= html_options('arrive', $options, $user_source->state->arrived) . '
' . __('user.active') . '' . "\n"; - $html .= html_options('eAktiv', $options, $user_source->state->active) . '
' . __('user.active') . '' . "\n"; + $html .= html_options('eAktiv', $options, $user_source->state->active) . '
' . __('user.active') . '' . "\n"; + $html .= ($user_source->state->active ? __('Yes') : __('No')); + $html .= '
' . __('Force active') . '' . "\n"; $html .= html_options('force_active', $options, $user_source->state->force_active) . '
' . __('T-Shirt') . '' . "\n"; + $html .= '
' . __('T-shirt') . '' . "\n"; } else { $html .= '
' . __('Goodie') . '' . "\n"; } @@ -119,27 +157,36 @@ function admin_user() $html .= '
' . "\n" . '
' . "\n" . '
' . "\n"; - $html .= '' . "\n"; + $html .= '' . "\n" . '
' . "\n"; + $html .= '' . "\n"; $html .= '
'; - $html .= '
'; + $html .= '
'; - $html .= form_info('', __('Please visit the angeltypes page or the users profile to manage the users angeltypes.')); + $html .= __('Here you can reset the password of this angel:'); - $html .= ' ' . __('Here you can reset the password of this angel:') . '
'change_pw', 'id' => $user_id]) . '" method="post">' . "\n"; $html .= form_csrf(); $html .= '' . "\n"; - $html .= ' ' . "\n"; - $html .= ' ' . "\n"; - - $html .= '
' . __('Password') . '' . '
' . __('Confirm password') . '' . '
' . "\n" . '
' . "\n"; - $html .= '' . "\n"; + $html .= ' ' . __('settings.password') + . ' ' + . '' + . '' + . '' . "\n"; + $html .= ' ' . __('password.reset.confirm') . '' + . '' + . '' . "\n"; + + $html .= '' . "\n" . '
' . "\n"; + $html .= '' . "\n"; $html .= '
'; - $html .= '
'; + $html .= '
'; /** @var Group $my_highest_group */ $my_highest_group = $user->groups()->orderByDesc('id')->first(); @@ -157,7 +204,7 @@ function admin_user() && ($my_highest_group >= $angel_highest_group || is_null($angel_highest_group)) ) { $html .= __('Here you can define the user groups of the angel:') . '
' . "\n"; $html .= form_csrf(); $html .= '
'; @@ -167,19 +214,22 @@ function admin_user() $html .= '
' . 'selected ? ' checked="checked"' : '') - . ' />
'; + . ' />
'; } $html .= '
'; - $html .= '' . "\n"; + $html .= '' . "\n"; $html .= '
'; - $html .= '
'; + $html .= '
'; } $html .= buttons([ - button(user_delete_link($user_source->id), icon('trash') . __('delete'), 'btn-danger'), + button(user_delete_link($user_source->id), icon('trash') . __('form.delete'), 'btn-danger'), ]); $html .= '
'; @@ -236,17 +286,30 @@ function admin_user() break; case 'save': - $force_active = $user->state->force_active; $user_source = User::find($user_id); - if (auth()->can('admin_active')) { - $force_active = $request->input('force_active'); + + $changed_email = false; + $email = $request->postData('eemail'); + if (($user_source->email !== $email) && User::whereEmail($email)->exists()) { + $html .= error(__('settings.profile.email.already-taken') . "\n", true); + break; } if ($user_source->settings->email_human) { - $user_source->email = $request->postData('eemail'); + $changed_email = $user_source->email !== $email; + $user_source->email = $email; } - $nickValidation = User_validate_Nick($request->postData('eNick')); - if ($nickValidation->isValid()) { - $user_source->name = $nickValidation->getValue(); + + $changed_nick = false; + $nick = trim((string) $request->get('eNick')); + $nickValid = (new Username())->validate($nick); + if (($user_source->name !== $nick) && User::whereName($nick)->exists()) { + $html .= error(__('settings.profile.nick.already-taken') . "\n", true); + break; + } + $old_nick = $user_source->name; + if ($nickValid && $user_edit) { + $changed_nick = ($user_source->name !== $nick) || User::whereName($nick)->exists(); + $user_source->name = $nick; } $user_source->save(); @@ -254,30 +317,49 @@ function admin_user() $user_source->personalData->first_name = $request->postData('eVorname'); $user_source->personalData->last_name = $request->postData('eName'); } - if ($goodie_tshirt) { + if ($goodie_tshirt && $user_edit_shirt) { $user_source->personalData->shirt_size = $request->postData('eSize'); } $user_source->personalData->save(); $user_source->contact->mobile = $request->postData('eHandy'); - $user_source->contact->dect = $request->postData('eDECT'); + if (config('enable_dect')) { + $user_source->contact->dect = $request->postData('eDECT'); + } $user_source->contact->save(); - if ($goodie_enabled) { + if ($goodie_enabled && $user_edit_shirt) { $user_source->state->got_shirt = $request->postData('eTshirt'); } - $user_source->state->active = $request->postData('eAktiv'); - $user_source->state->force_active = $force_active; + if ($user_info_edit) { + $user_source->state->user_info = $request->postData('userInfo'); + } + if ($admin_arrive) { + $user_source->state->arrived = $request->postData('arrive'); + } + + if ($user_edit_shirt) { + $user_source->state->active = $request->postData('eAktiv'); + } + if (auth()->can('admin_active')) { + $user_source->state->force_active = $request->input('force_active'); + } $user_source->state->save(); engelsystem_log( - 'Updated user: ' . $user_source->name . ' (' . $user_source->id . ')' - . ($goodie_tshirt ? ', t-shirt: ' : '' . $user_source->personalData->shirt_size) + 'Updated user: ' . ($changed_nick + ? ('nick modified form ' . $old_nick . ' to ' . $user_source->name) + : $user_source->name) + . ' (' . $user_source->id . ')' + . ($changed_email ? ', email modified' : '') + . ($goodie_tshirt ? ', t-shirt-size: ' . $user_source->personalData->shirt_size : '') + . ', arrived: ' . $user_source->state->arrived . ', active: ' . $user_source->state->active . ', force-active: ' . $user_source->state->force_active - . ($goodie_tshirt ? ', tshirt: ' : ', goodie: ' . $user_source->state->got_shirt) + . ($goodie_tshirt ? ', t-shirt: ' : ', goodie: ' . $user_source->state->got_shirt) + . ($user_info_edit ? ', user-info: ' . $user_source->state->user_info : '') ); - $html .= success(__('Changes where saved.') . "\n", true); + $html .= success(__('Changes were saved.') . "\n", true); break; case 'change_pw': @@ -299,9 +381,13 @@ function admin_user() } } - return page_with_title(__('Edit user'), [ + $link = button(url('/users', ['action' => 'view', 'user_id' => $user_id]), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title( + $link . ' ' . __('Edit user'), + [ $html, - ]); + ] + ); } /** diff --git a/includes/pages/guest_login.php b/includes/pages/guest_login.php deleted file mode 100644 index 9353b4c04..000000000 --- a/includes/pages/guest_login.php +++ /dev/null @@ -1,542 +0,0 @@ -user(); - $tshirt_sizes = config('tshirt_sizes'); - $goodie = GoodieType::from(config('goodie_type')); - $goodie_enabled = $goodie !== GoodieType::None; - $goodie_tshirt = $goodie === GoodieType::Tshirt; - $enable_user_name = config('enable_user_name'); - $enable_dect = config('enable_dect'); - $enable_planned_arrival = config('enable_planned_arrival'); - $min_password_length = config('min_password_length'); - $enable_password = config('enable_password'); - $enable_pronoun = config('enable_pronoun'); - $enable_mobile_show = config('enable_mobile_show'); - $config = config(); - $request = request(); - $session = session(); - /** @var Connection $db */ - $db = app(Database::class)->getConnection(); - $is_oauth = $session->has('oauth2_connect_provider'); - - $msg = ''; - $nick = ''; - $lastName = ''; - $preName = ''; - $dect = ''; - $mobile = ''; - $mobile_show = false; - $email = ''; - $pronoun = ''; - $email_shiftinfo = false; - $email_by_human_allowed = false; - $email_messages = false; - $email_news = false; - $email_goody = false; - $tshirt_size = ''; - $password_hash = ''; - $selected_angel_types = []; - $planned_arrival_date = null; - - /** @var AngelType[]|Collection $angel_types_source */ - $angel_types_source = AngelType::all(); - $angel_types = []; - if (!empty($session->get('oauth2_groups'))) { - /** @var OAuth2 $oauth */ - $oauth = app()->get(OAuth2::class); - $ssoTeams = $oauth->getSsoTeams($session->get('oauth2_connect_provider')); - foreach ($ssoTeams as $name => $team) { - if (in_array($name, $session->get('oauth2_groups'))) { - $selected_angel_types[] = $team['id']; - } - } - } - foreach ($angel_types_source as $angel_type) { - if ($angel_type->hide_register) { - continue; - } - $angel_types[$angel_type->id] = $angel_type->name - . ($angel_type->restricted ? ' (' . __('Requires introduction') . ')' : ''); - if (!$angel_type->restricted) { - $selected_angel_types[] = $angel_type->id; - } - } - - $oauth_enable_password = $session->get('oauth2_enable_password'); - if (!is_null($oauth_enable_password)) { - $enable_password = $oauth_enable_password; - } - - if ( - !auth()->can('register') // No registration permission - // Not authenticated and - || (!$authUser && !config('registration_enabled') && !$session->get('oauth2_allow_registration')) // Registration disabled - || (!$authUser && !$enable_password && !$is_oauth) // Password disabled and not oauth - ) { - error(__('Registration is disabled.')); - - return page_with_title(register_title(), [ - msg(), - ]); - } - - if ($request->hasPostData('submit')) { - $valid = true; - - if ($request->has('username')) { - $nickValidation = User_validate_Nick($request->input('username')); - $nick = $nickValidation->getValue(); - - if (!$nickValidation->isValid()) { - $valid = false; - $msg .= error(sprintf( - __('Please enter a valid nick.') . ' ' . __('Use up to 24 letters, numbers, connecting punctuations or spaces for your nickname.'), - $nick - ), true); - } - if (User::whereName($nick)->count() > 0) { - $valid = false; - $msg .= error(sprintf(__('Your nick "%s" already exists.'), htmlspecialchars($nick)), true); - } - } else { - $valid = false; - $msg .= error(__('Please enter a nickname.'), true); - } - - if ($request->has('mobile_show') && $enable_mobile_show) { - $mobile_show = true; - } - - if ($request->has('email') && strlen(strip_request_item('email')) > 0) { - $email = strip_request_item('email'); - if (!check_email($email)) { - $valid = false; - $msg .= error(__('E-mail address is not correct.'), true); - } - if (User::whereEmail($email)->first()) { - $valid = false; - $msg .= error(__('E-mail address is already used by another user.'), true); - } - } else { - $valid = false; - $msg .= error(__('Please enter your e-mail.'), true); - } - - if ($request->has('email_shiftinfo')) { - $email_shiftinfo = true; - } - - if ($request->has('email_by_human_allowed')) { - $email_by_human_allowed = true; - } - - if ($request->has('email_messages')) { - $email_messages = true; - } - - if ($request->has('email_news')) { - $email_news = true; - } - - if ($request->has('email_goody')) { - $email_goody = true; - } - - if ($goodie_tshirt) { - if ($request->has('tshirt_size') && isset($tshirt_sizes[$request->input('tshirt_size')])) { - $tshirt_size = $request->input('tshirt_size'); - } else { - $valid = false; - $msg .= error(__('Please select your shirt size.'), true); - } - } - - if ($enable_password && $request->has('password') && strlen($request->postData('password')) >= $min_password_length) { - if ($request->postData('password') != $request->postData('password2')) { - $valid = false; - $msg .= error(__('Your passwords don\'t match.'), true); - } - } elseif ($enable_password) { - $valid = false; - $msg .= error(sprintf( - __('Your password is too short (please use at least %s characters).'), - $min_password_length - ), true); - } - - if ($request->has('planned_arrival_date') && $enable_planned_arrival) { - $tmp = parse_date('Y-m-d H:i', $request->input('planned_arrival_date') . ' 00:00'); - $result = User_validate_planned_arrival_date($tmp); - $planned_arrival_date = $result->getValue(); - if (!$result->isValid()) { - $valid = false; - error(__('Please enter your planned date of arrival. It should be after the buildup start date and before teardown end date.')); - } - } elseif ($enable_planned_arrival) { - $valid = false; - error(__('Please enter your planned date of arrival. It should be after the buildup start date and before teardown end date.')); - } - - $selected_angel_types = []; - foreach (array_keys($angel_types) as $angel_type_id) { - if ($request->has('angel_types_' . $angel_type_id)) { - $selected_angel_types[] = $angel_type_id; - } - } - - // Trivia - if ($enable_user_name && $request->has('lastname')) { - $lastName = strip_request_item('lastname'); - } - if ($enable_user_name && $request->has('prename')) { - $preName = strip_request_item('prename'); - } - if ($enable_pronoun && $request->has('pronoun')) { - $pronoun = strip_request_item('pronoun'); - } - if ($enable_dect && $request->has('dect')) { - if (strlen(strip_request_item('dect')) <= 40) { - $dect = strip_request_item('dect'); - } else { - $valid = false; - error(__('For dect numbers are only 40 digits allowed.')); - } - } - if ($request->has('mobile')) { - $mobile = strip_request_item('mobile'); - } - - if ($valid) { - // Safeguard against partially created user data - $db->beginTransaction(); - - $user = new User([ - 'name' => $nick, - 'password' => $password_hash, - 'email' => $email, - 'api_key' => '', - 'last_login_at' => null, - ]); - $user->save(); - - $contact = new Contact([ - 'dect' => $dect, - 'mobile' => $mobile, - ]); - $contact->user() - ->associate($user) - ->save(); - - $personalData = new PersonalData([ - 'first_name' => $preName, - 'last_name' => $lastName, - 'pronoun' => $pronoun, - 'shirt_size' => $tshirt_size, - 'planned_arrival_date' => $enable_planned_arrival ? Carbon::createFromTimestamp($planned_arrival_date) : null, - ]); - $personalData->user() - ->associate($user) - ->save(); - - $settings = new Settings([ - 'language' => $session->get('locale'), - 'theme' => config('theme'), - 'email_human' => $email_by_human_allowed, - 'email_messages' => $email_messages, - 'email_goody' => $email_goody, - 'email_shiftinfo' => $email_shiftinfo, - 'email_news' => $email_news, - 'mobile_show' => $mobile_show, - ]); - $settings->user() - ->associate($user) - ->save(); - - $state = new State([]); - if (config('autoarrive')) { - $state->arrived = true; - $state->arrival_date = new Carbon(); - } - $state->user() - ->associate($user) - ->save(); - - if ($session->has('oauth2_connect_provider') && $session->has('oauth2_user_id')) { - $oauth = new OAuth([ - 'provider' => $session->get('oauth2_connect_provider'), - 'identifier' => $session->get('oauth2_user_id'), - 'access_token' => $session->get('oauth2_access_token'), - 'refresh_token' => $session->get('oauth2_refresh_token'), - 'expires_at' => $session->get('oauth2_expires_at'), - ]); - $oauth->user() - ->associate($user) - ->save(); - - $session->remove('oauth2_connect_provider'); - $session->remove('oauth2_user_id'); - $session->remove('oauth2_access_token'); - $session->remove('oauth2_refresh_token'); - $session->remove('oauth2_expires_at'); - } - - // Assign user-group and set password - $defaultGroup = Group::find(auth()->getDefaultRole()); - $user->groups()->attach($defaultGroup); - if ($enable_password) { - auth()->setPassword($user, $request->postData('password')); - } - - // Assign angel-types - $user_angel_types_info = []; - foreach ($selected_angel_types as $selected_angel_type_id) { - $angelType = AngelType::findOrFail($selected_angel_type_id); - $user->userAngelTypes()->attach($angelType); - $user_angel_types_info[] = $angelType->name; - } - - // Commit complete user data - $db->commit(); - - engelsystem_log( - 'User ' . User_Nick_render($user, true) - . ' signed up as: ' . join(', ', $user_angel_types_info) - ); - success(__('Angel registration successful!')); - - // User is already logged in - that means a supporter has registered an angel. Return to register page. - if ($authUser) { - throw_redirect(page_link_to('register')); - } - - // If a welcome message is present, display it on the next page - if ($config->get('welcome_msg')) { - $session->set('show_welcome', true); - } - - // Login the user - if ($user->oauth->count()) { - /** @var OAuth $provider */ - $provider = $user->oauth->first(); - throw_redirect(url('/oauth/' . $provider->provider)); - } - - throw_redirect(page_link_to('/')); - } - } - - $buildup_start_date = time(); - $teardown_end_date = null; - if ($buildup = $config->get('buildup_start')) { - /** @var Carbon $buildup */ - $buildup_start_date = $buildup->getTimestamp(); - } - - if ($teardown = $config->get('teardown_end')) { - /** @var Carbon $teardown */ - $teardown_end_date = $teardown->getTimestamp(); - } - - $form_data = $session->get('form_data'); - $session->remove('form_data'); - if (!$nick && !empty($form_data['name'])) { - $nick = $form_data['name']; - } - - if (!$email && !empty($form_data['email'])) { - $email = $form_data['email']; - } - - if (!$preName && !empty($form_data['first_name'])) { - $preName = $form_data['first_name']; - } - - if (!$lastName && !empty($form_data['last_name'])) { - $lastName = $form_data['last_name']; - } - - return page_with_title(register_title(), [ - __('By completing this form you\'re registering as a Chaos-Angel. This script will create you an account in the angel task scheduler.'), - form_info(entry_required() . ' = ' . __('Entry required!')), - $msg, - msg(), - form([ - div('row', [ - div('col', [ - form_text( - 'username', - __('Nick') . ' ' . entry_required(), - $nick, - false, - 24, - 'nickname' - ), - form_info( - '', - __('Use up to 24 letters, numbers, connecting punctuations or spaces for your nickname.') - ), - ]), - - $enable_pronoun ? div('col', [ - form_text('pronoun', __('Pronoun'), $pronoun, false, 15), - ]) : '', - ]), - - $enable_user_name ? div('row', [ - div('col', [ - form_text('prename', __('First name'), $preName, false, 64, 'given-name'), - ]), - div('col', [ - form_text('lastname', __('Last name'), $lastName, false, 64, 'family-name'), - ]), - ]) : '', - - div('row', [ - div('col', [ - form_email( - 'email', - __('E-Mail') . ' ' . entry_required(), - $email, - false, - 'email', - 254 - ), - form_checkbox( - 'email_shiftinfo', - __( - 'The %s is allowed to send me an email (e.g. when my shifts change)', - [config('app_name')] - ), - $email_shiftinfo - ), - form_checkbox( - 'email_news', - __('Notify me of new news'), - $email_news - ), - form_checkbox( - 'email_messages', - __('settings.profile.email_messages'), - $email_messages - ), - form_checkbox( - 'email_by_human_allowed', - __('Allow heaven angels to contact you by e-mail.'), - $email_by_human_allowed - ), - $goodie_enabled ? - form_checkbox( - 'email_goody', - __('To receive vouchers, give consent that nick, email address, worked hours and shirt size will be stored until the next similar event.') - . (config('privacy_email') ? ' ' . __('To withdraw your approval, send an email to %1$s.', [config('privacy_email')]) : ''), - $email_goody - ) : '', - ]), - - $enable_dect ? div('col', [ - form_text('dect', __('DECT'), $dect, false, 40, 'tel-local'), - ]) : '', - - div('col', [ - form_text('mobile', __('Mobile'), $mobile, false, 40, 'tel-national'), - $enable_mobile_show ? form_checkbox( - 'mobile_show', - __('Show mobile number to other users to contact me'), - $mobile_show - ) : '', - ]), - ]), - - div('row', [ - $enable_password ? div('col', [ - form_password('password', __('Password') . ' ' . entry_required(), 'new-password'), - ]) : '', - - $enable_planned_arrival ? div('col', [ - form_date( - 'planned_arrival_date', - __('Planned date of arrival') . ' ' . entry_required(), - $planned_arrival_date, - $buildup_start_date, - $teardown_end_date - ), - ]) : '', - ]), - - div('row', [ - $enable_password ? div('col', [ - form_password('password2', __('Confirm password') . ' ' . entry_required(), 'new-password'), - ]) : '', - - div('col', [ - $goodie_tshirt ? form_select( - 'tshirt_size', - __('Shirt size') . ' ' . entry_required(), - $tshirt_sizes, - $tshirt_size, - __('Please select...') - ) : '', - ]), - ]), - - div('row', [ - div('col', [ - form_checkboxes( - 'angel_types', - __('What do you want to do?') . sprintf( - ' (%s)', - url('/angeltypes/about'), - __('Description of job types') - ), - $angel_types, - $selected_angel_types - ), - form_info( - '', - __('Some angel types have to be confirmed later by a supporter at an introduction meeting. You can change your selection in the options section.') - ), - ]), - ]), - - form_submit('submit', __('Register')), - ]), - ]); -} - -/** - * @return string - */ -function entry_required() -{ - return icon('exclamation-triangle', 'text-info'); -} diff --git a/includes/pages/schedule/ImportSchedule.php b/includes/pages/schedule/ImportSchedule.php index e1a940cea..fed44373c 100644 --- a/includes/pages/schedule/ImportSchedule.php +++ b/includes/pages/schedule/ImportSchedule.php @@ -16,7 +16,7 @@ use Engelsystem\Helpers\Uuid; use Engelsystem\Http\Request; use Engelsystem\Http\Response; -use Engelsystem\Models\Room as RoomModel; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\Schedule as ScheduleUrl; use Engelsystem\Models\Shifts\ScheduleShift; use Engelsystem\Models\Shifts\Shift; @@ -91,14 +91,14 @@ public function index(): Response public function edit(Request $request): Response { $scheduleId = $request->getAttribute('schedule_id'); // optional - $schedule = ScheduleUrl::find($scheduleId); return $this->response->withView( 'admin/schedule/edit.twig', [ 'schedule' => $schedule, - 'shift_types' => ShiftType::all()->pluck('name', 'id'), + 'shift_types' => ShiftType::all()->sortBy('name')->pluck('name', 'id'), + 'locations' => Location::all()->sortBy('name')->pluck('name', 'id'), ] ); } @@ -110,13 +110,24 @@ public function save(Request $request): Response /** @var ScheduleUrl $schedule */ $schedule = ScheduleUrl::findOrNew($scheduleId); + if ($request->request->has('delete')) { + return $this->delete($schedule); + } + + $locationsList = Location::all()->pluck('id'); + $locationsValidation = []; + foreach ($locationsList as $id) { + $locationsValidation['location_' . $id] = 'optional|checked'; + } + $data = $this->validate($request, [ 'name' => 'required', 'url' => 'required', 'shift_type' => 'required|int', + 'needed_from_shift_type' => 'optional|checked', 'minutes_before' => 'int', 'minutes_after' => 'int', - ]); + ] + $locationsValidation); if (!ShiftType::find($data['shift_type'])) { throw new ErrorException('schedule.import.invalid-shift-type'); @@ -125,19 +136,35 @@ public function save(Request $request): Response $schedule->name = $data['name']; $schedule->url = $data['url']; $schedule->shift_type = $data['shift_type']; + $schedule->needed_from_shift_type = (bool) $data['needed_from_shift_type']; $schedule->minutes_before = $data['minutes_before']; $schedule->minutes_after = $data['minutes_after']; $schedule->save(); + $schedule->activeLocations()->detach(); + + $for = new Collection(); + foreach ($locationsList as $id) { + if (!$data['location_' . $id]) { + continue; + } + + $location = Location::find($id); + $schedule->activeLocations()->attach($location); + $for[] = $location->name; + } $this->log->info( - 'Schedule {name}: Url {url}, Shift Type {shift_type}, minutes before/after {before}/{after}', + 'Schedule {name}: Url {url}, Shift Type {shift_type}, ({need}), ' + . 'minutes before/after {before}/{after}, for: {locations}', [ 'name' => $schedule->name, 'url' => $schedule->name, 'shift_type' => $schedule->shift_type, + 'need' => $schedule->needed_from_shift_type ? 'from shift type' : 'from room', 'before' => $schedule->minutes_before, 'after' => $schedule->minutes_after, + 'locations' => $for->implode(', '), ] ); @@ -146,6 +173,33 @@ public function save(Request $request): Response return redirect('/admin/schedule/load/' . $schedule->id); } + protected function delete(ScheduleUrl $schedule): Response + { + foreach ($schedule->scheduleShifts as $scheduleShift) { + // Only guid is needed here + $event = new Event( + $scheduleShift->guid, + 0, + new Room(''), + '', + '', + '', + Carbon::now(), + '', + '', + '', + '', + '' + ); + + $this->deleteEvent($event, $schedule); + } + $schedule->delete(); + + $this->addNotification('schedule.delete.success'); + return redirect('/admin/schedule'); + } + public function loadSchedule(Request $request): Response { try { @@ -179,7 +233,7 @@ public function loadSchedule(Request $request): Response [ 'schedule_id' => $scheduleUrl->id, 'schedule' => $schedule, - 'rooms' => [ + 'locations' => [ 'add' => $newRooms, ], 'shifts' => [ @@ -218,15 +272,15 @@ public function importSchedule(Request $request): Response $this->log('Started schedule "{name}" import', ['name' => $scheduleUrl->name]); foreach ($newRooms as $room) { - $this->createRoom($room); + $this->createLocation($room); } - $rooms = $this->getAllRooms(); + $locations = $this->getAllLocations(); foreach ($newEvents as $event) { $this->createEvent( $event, $shiftType, - $rooms + $locations ->where('name', $event->getRoom()->getName()) ->first(), $scheduleUrl @@ -237,15 +291,15 @@ public function importSchedule(Request $request): Response $this->updateEvent( $event, $shiftType, - $rooms + $locations ->where('name', $event->getRoom()->getName()) - ->first() + ->first(), + $scheduleUrl ); } foreach ($deleteEvents as $event) { - $this->fireDeleteShiftEntryEvents($event); - $this->deleteEvent($event); + $this->deleteEvent($event, $scheduleUrl); } $scheduleUrl->touch(); @@ -255,29 +309,30 @@ public function importSchedule(Request $request): Response return redirect($this->url, 303); } - protected function createRoom(Room $room): void + protected function createLocation(Room $room): void { - $roomModel = new RoomModel(); - $roomModel->name = $room->getName(); - $roomModel->save(); + $location = new Location(); + $location->name = $room->getName(); + $location->save(); - $this->log('Created schedule room "{room}"', ['room' => $room->getName()]); + $this->log('Created schedule location "{location}"', ['location' => $room->getName()]); } - protected function fireDeleteShiftEntryEvents(Event $event): void + protected function fireDeleteShiftEntryEvents(Event $event, ScheduleUrl $schedule): void { $shiftEntries = $this->db ->table('shift_entries') ->select([ - 'shift_types.name', 'shifts.title', 'angel_types.name AS type', 'rooms.id AS room_id', + 'shift_types.name', 'shifts.title', 'angel_types.name AS type', 'locations.id AS location_id', 'shifts.start', 'shifts.end', 'shift_entries.user_id', 'shift_entries.freeloaded', ]) ->join('shifts', 'shifts.id', 'shift_entries.shift_id') ->join('schedule_shift', 'shifts.id', 'schedule_shift.shift_id') - ->join('rooms', 'rooms.id', 'shifts.room_id') + ->join('locations', 'locations.id', 'shifts.location_id') ->join('angel_types', 'angel_types.id', 'shift_entries.angel_type_id') ->join('shift_types', 'shift_types.id', 'shifts.shift_type_id') ->where('schedule_shift.guid', $event->getGuid()) + ->where('schedule_shift.schedule_id', $schedule->id) ->get(); foreach ($shiftEntries as $shiftEntry) { @@ -288,13 +343,13 @@ protected function fireDeleteShiftEntryEvents(Event $event): void 'name' => $shiftEntry->name, 'title' => $shiftEntry->title, 'type' => $shiftEntry->type, - 'room' => RoomModel::find($shiftEntry->room_id), + 'location' => Location::find($shiftEntry->location_id), 'freeloaded' => $shiftEntry->freeloaded, ]); } } - protected function createEvent(Event $event, int $shiftTypeId, RoomModel $room, ScheduleUrl $scheduleUrl): void + protected function createEvent(Event $event, int $shiftTypeId, Location $location, ScheduleUrl $scheduleUrl): void { $user = auth()->user(); $eventTimeZone = Carbon::now()->timezone; @@ -304,7 +359,7 @@ protected function createEvent(Event $event, int $shiftTypeId, RoomModel $room, $shift->shift_type_id = $shiftTypeId; $shift->start = $event->getDate()->copy()->timezone($eventTimeZone); $shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone); - $shift->room()->associate($room); + $shift->location()->associate($location); $shift->url = $event->getUrl() ?? ''; $shift->transaction_id = Uuid::uuidBy($scheduleUrl->id, '5c4ed01e'); $shift->createdBy()->associate($user); @@ -316,68 +371,82 @@ protected function createEvent(Event $event, int $shiftTypeId, RoomModel $room, $scheduleShift->save(); $this->log( - 'Created schedule shift "{shift}" in "{room}" ({from} {to}, {guid})', + 'Created schedule shift "{shift}" in "{location}" ({from} {to}, {guid})', [ - 'shift' => $shift->title, - 'room' => $shift->room->name, - 'from' => $shift->start->format(DateTimeInterface::RFC3339), - 'to' => $shift->end->format(DateTimeInterface::RFC3339), - 'guid' => $scheduleShift->guid, + 'shift' => $shift->title, + 'location' => $shift->location->name, + 'from' => $shift->start->format(DateTimeInterface::RFC3339), + 'to' => $shift->end->format(DateTimeInterface::RFC3339), + 'guid' => $scheduleShift->guid, ] ); } - protected function updateEvent(Event $event, int $shiftTypeId, RoomModel $room): void + protected function updateEvent(Event $event, int $shiftTypeId, Location $location, ScheduleUrl $schedule): void { $user = auth()->user(); $eventTimeZone = Carbon::now()->timezone; /** @var ScheduleShift $scheduleShift */ - $scheduleShift = ScheduleShift::whereGuid($event->getGuid())->first(); + $scheduleShift = ScheduleShift::whereGuid($event->getGuid())->where('schedule_id', $schedule->id)->first(); $shift = $scheduleShift->shift; + $oldShift = Shift::find($shift->id); $shift->title = $event->getTitle(); $shift->shift_type_id = $shiftTypeId; $shift->start = $event->getDate()->copy()->timezone($eventTimeZone); $shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone); - $shift->room()->associate($room); + $shift->location()->associate($location); $shift->url = $event->getUrl() ?? ''; $shift->updatedBy()->associate($user); $shift->save(); + $this->fireUpdateShiftUpdateEvent($oldShift, $shift); + $this->log( - 'Updated schedule shift "{shift}" in "{room}" ({from} {to}, {guid})', + 'Updated schedule shift "{shift}" in "{location}" ({from} {to}, {guid})', [ - 'shift' => $shift->title, - 'room' => $shift->room->name, - 'from' => $shift->start->format(DateTimeInterface::RFC3339), - 'to' => $shift->end->format(DateTimeInterface::RFC3339), - 'guid' => $scheduleShift->guid, + 'shift' => $shift->title, + 'location' => $shift->location->name, + 'from' => $shift->start->format(DateTimeInterface::RFC3339), + 'to' => $shift->end->format(DateTimeInterface::RFC3339), + 'guid' => $scheduleShift->guid, ] ); } - protected function deleteEvent(Event $event): void + protected function deleteEvent(Event $event, ScheduleUrl $schedule): void { /** @var ScheduleShift $scheduleShift */ - $scheduleShift = ScheduleShift::whereGuid($event->getGuid())->first(); + $scheduleShift = ScheduleShift::whereGuid($event->getGuid())->where('schedule_id', $schedule->id)->first(); $shift = $scheduleShift->shift; $shift->delete(); + $scheduleShift->delete(); + + $this->fireDeleteShiftEntryEvents($event, $schedule); $this->log( - 'Deleted schedule shift "{shift}" in {room} ({from} {to}, {guid})', + 'Deleted schedule shift "{shift}" in {location} ({from} {to}, {guid})', [ - 'shift' => $shift->title, - 'room' => $shift->room->name, - 'from' => $shift->start->format(DateTimeInterface::RFC3339), - 'to' => $shift->end->format(DateTimeInterface::RFC3339), - 'guid' => $scheduleShift->guid, + 'shift' => $shift->title, + 'location' => $shift->location->name, + 'from' => $shift->start->format(DateTimeInterface::RFC3339), + 'to' => $shift->end->format(DateTimeInterface::RFC3339), + 'guid' => $scheduleShift->guid, ] ); } + protected function fireUpdateShiftUpdateEvent(Shift $oldShift, Shift $newShift): void + { + event('shift.updating', [ + 'shift' => $newShift, + 'oldShift' => $oldShift, + ]); + } + /** * @param Request $request - * @return Event[]|Room[]|RoomModel[] + * @return Event[]|Room[]|Location[] * @throws ErrorException */ protected function getScheduleData(Request $request) @@ -420,10 +489,10 @@ protected function getScheduleData(Request $request) protected function newRooms(array $scheduleRooms): array { $newRooms = []; - $allRooms = $this->getAllRooms(); + $allLocations = $this->getAllLocations(); foreach ($scheduleRooms as $room) { - if ($allRooms->where('name', $room->getName())->count()) { + if ($allLocations->where('name', $room->getName())->count()) { continue; } @@ -456,11 +525,15 @@ protected function shiftsDiff( $scheduleEvents = []; /** @var Event[] $deleteEvents */ $deleteEvents = []; - $rooms = $this->getAllRooms(); + $locations = $this->getAllLocations(); $eventTimeZone = Carbon::now()->timezone; foreach ($schedule->getDay() as $day) { foreach ($day->getRoom() as $room) { + if (!$scheduleUrl->activeLocations->where('name', $room->getName())->count()) { + continue; + } + foreach ($room->getEvent() as $event) { $scheduleEvents[$event->getGuid()] = $event; @@ -477,19 +550,18 @@ protected function shiftsDiff( $scheduleEventsGuidList = array_keys($scheduleEvents); $existingShifts = $this->getScheduleShiftsByGuid($scheduleUrl, $scheduleEventsGuidList); - foreach ($existingShifts as $shift) { - $guid = $shift->guid; - /** @var Shift $shift */ - $shift = Shift::with('room')->find($shift->shift_id); + foreach ($existingShifts as $scheduleShift) { + $guid = $scheduleShift->guid; + $shift = $scheduleShift->shift; $event = $scheduleEvents[$guid]; - $room = $rooms->where('name', $event->getRoom()->getName())->first(); + $location = $locations->where('name', $event->getRoom()->getName())->first(); if ( $shift->title != $event->getTitle() || $shift->shift_type_id != $shiftType || $shift->start != $event->getDate() || $shift->end != $event->getEndDate() - || $shift->room_id != ($room->id ?? '') + || $shift->location_id != ($location->id ?? '') || $shift->url != ($event->getUrl() ?? '') ) { $changeEvents[$guid] = $event; @@ -503,8 +575,8 @@ protected function shiftsDiff( } $scheduleShifts = $this->getScheduleShiftsWhereNotGuid($scheduleUrl, $scheduleEventsGuidList); - foreach ($scheduleShifts as $shift) { - $event = $this->eventFromScheduleShift($shift); + foreach ($scheduleShifts as $scheduleShift) { + $event = $this->eventFromScheduleShift($scheduleShift); $deleteEvents[$event->getGuid()] = $event; } @@ -513,14 +585,13 @@ protected function shiftsDiff( protected function eventFromScheduleShift(ScheduleShift $scheduleShift): Event { - /** @var Shift $shift */ - $shift = Shift::with('room')->find($scheduleShift->shift_id); + $shift = $scheduleShift->shift; $duration = $shift->start->diff($shift->end); return new Event( $scheduleShift->guid, 0, - new Room($shift->room->name), + new Room($shift->location->name), $shift->title, '', 'n/a', @@ -534,11 +605,11 @@ protected function eventFromScheduleShift(ScheduleShift $scheduleShift): Event } /** - * @return RoomModel[]|Collection + * @return Location[]|Collection */ - protected function getAllRooms(): Collection + protected function getAllLocations(): Collection { - return RoomModel::all(); + return Location::all(); } /** @@ -548,7 +619,7 @@ protected function getAllRooms(): Collection */ protected function getScheduleShiftsByGuid(ScheduleUrl $scheduleUrl, array $events) { - return ScheduleShift::query() + return ScheduleShift::with('shift.location') ->whereIn('guid', $events) ->where('schedule_id', $scheduleUrl->id) ->get(); @@ -561,7 +632,7 @@ protected function getScheduleShiftsByGuid(ScheduleUrl $scheduleUrl, array $even */ protected function getScheduleShiftsWhereNotGuid(ScheduleUrl $scheduleUrl, array $events) { - return ScheduleShift::query() + return ScheduleShift::with('shift.location') ->whereNotIn('guid', $events) ->where('schedule_id', $scheduleUrl->id) ->get(); diff --git a/includes/pages/user_myshifts.php b/includes/pages/user_myshifts.php index 0dbc8823d..06461f387 100644 --- a/includes/pages/user_myshifts.php +++ b/includes/pages/user_myshifts.php @@ -8,7 +8,7 @@ */ function myshifts_title() { - return __('My shifts'); + return __('profile.my-shifts'); } /** @@ -35,23 +35,24 @@ function user_myshifts() $shifts_user = User::find($shift_entry_id); if ($request->has('reset')) { if ($request->input('reset') == 'ack') { - User_reset_api_key($user); + auth()->resetApiKey($user); + engelsystem_log(sprintf('API key resetted (%s).', User_Nick_render($user, true))); success(__('Key changed.')); - throw_redirect(page_link_to('users', ['action' => 'view', 'user_id' => $shifts_user->id])); + throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id])); } return page_with_title(__('Reset API key'), [ error( __('If you reset the key, the url to your iCal- and JSON-export and your atom/rss feed changes! You have to update it in every application using one of these exports.'), true ), - button(page_link_to('user_myshifts', ['reset' => 'ack']), __('Continue'), 'btn-danger'), + button(url('/user-myshifts', ['reset' => 'ack']), __('Continue'), 'btn-danger'), ]); } elseif ($request->has('edit') && preg_match('/^\d+$/', $request->input('edit'))) { $shift_entry_id = $request->input('edit'); /** @var ShiftEntry $shiftEntry */ $shiftEntry = ShiftEntry::where('id', $shift_entry_id) ->where('user_id', $shifts_user->id) - ->with(['shift', 'shift.shiftType', 'shift.room', 'user']) + ->with(['shift', 'shift.shiftType', 'shift.location', 'user']) ->first(); if (!empty($shiftEntry)) { $shift = $shiftEntry->shift; @@ -90,14 +91,14 @@ function user_myshifts() . '. Freeloaded: ' . ($freeloaded ? 'YES Comment: ' . $freeloaded_comment : 'NO') ); success(__('Shift saved.')); - throw_redirect(page_link_to('users', ['action' => 'view', 'user_id' => $shifts_user->id])); + throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id])); } } return ShiftEntry_edit_view( $shifts_user, - $shift->start->format(__('Y-m-d H:i')) . ', ' . shift_length($shift), - $shift->room->name, + $shift->start->format(__('general.datetime')) . ', ' . shift_length($shift), + $shift->location->name, $shift->shiftType->name, $shiftEntry->angelType->name, $shiftEntry->user_comment, @@ -106,10 +107,10 @@ function user_myshifts() auth()->can('user_shifts_admin') ); } else { - throw_redirect(page_link_to('user_myshifts')); + throw_redirect(url('/user-myshifts')); } } - throw_redirect(page_link_to('users', ['action' => 'view', 'user_id' => $shifts_user->id])); + throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id])); return ''; } diff --git a/includes/pages/user_shifts.php b/includes/pages/user_shifts.php index c471dc76a..5f584630e 100644 --- a/includes/pages/user_shifts.php +++ b/includes/pages/user_shifts.php @@ -3,7 +3,9 @@ use Engelsystem\Database\Db; use Engelsystem\Helpers\Carbon; use Engelsystem\Models\AngelType; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; +use Engelsystem\Models\Shifts\NeededAngelType; +use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\UserAngelType; use Engelsystem\ShiftsFilter; use Illuminate\Database\Eloquent\Builder; @@ -32,7 +34,7 @@ function user_shifts() $request = request(); if (auth()->user()->isFreeloader()) { - throw_redirect(page_link_to('user_myshifts')); + throw_redirect(url('/user-myshifts')); } if ($request->has('edit_shift')) { @@ -105,23 +107,42 @@ function update_ShiftsFilter(ShiftsFilter $shiftsFilter, $user_shifts_admin, $da { $shiftsFilter->setUserShiftsAdmin($user_shifts_admin); $shiftsFilter->setFilled(check_request_int_array('filled', $shiftsFilter->getFilled())); - $shiftsFilter->setRooms(check_request_int_array('rooms', $shiftsFilter->getRooms())); + $shiftsFilter->setLocations(check_request_int_array('locations', $shiftsFilter->getLocations())); $shiftsFilter->setTypes(check_request_int_array('types', $shiftsFilter->getTypes())); update_ShiftsFilter_timerange($shiftsFilter, $days); } /** - * @return Room[]|Collection + * @return Location[]|Collection */ -function load_rooms() +function load_locations(bool $onlyWithActiveShifts = false) { - $rooms = Room::orderBy('name')->get(); - if ($rooms->isEmpty()) { - error(__('The administration has not configured any rooms yet.')); - throw_redirect(page_link_to('/')); + $locations = Location::orderBy('name'); + + if ($onlyWithActiveShifts) { + $locationIdsFromAngelType = NeededAngelType::query() + ->whereNotNull('location_id') + ->select('location_id'); + + $locationIdsFromShift = Shift::query() + ->leftJoin('needed_angel_types', 'shifts.id', 'needed_angel_types.shift_id') + ->leftJoin('needed_angel_types AS nast', 'shifts.shift_type_id', 'nast.shift_type_id') + ->whereNotNull('needed_angel_types.id') + ->orWhereNotNull('nast.id') + ->select('shifts.location_id'); + + $locations->whereIn('id', $locationIdsFromAngelType) + ->orWhereIn('id', $locationIdsFromShift); } - return $rooms; + $locations = $locations->get(); + + if ($locations->isEmpty()) { + error(__('The administration has not configured any locations yet.')); + throw_redirect(url('/')); + } + + return $locations; } /** @@ -143,7 +164,7 @@ function load_days() error(__('The administration has not configured any shifts yet.')); // Do not try to redirect to the current page if (config('home_site') != 'user_shifts') { - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } } return $days; @@ -155,10 +176,11 @@ function load_days() function load_types() { $user = auth()->user(); + $isShico = auth()->can('admin_shifts'); if (!AngelType::count()) { error(__('The administration has not configured any angeltypes yet - or you are not subscribed to any angeltype.')); - throw_redirect(page_link_to('/')); + throw_redirect(url('/')); } $types = Db::select( @@ -178,8 +200,11 @@ function load_types() ON ( `user_angel_type`.`angel_type_id`=`angel_types`.`id` AND `user_angel_type`.`user_id`=? - ) - ORDER BY `angel_types`.`name` + )' + . ($isShico ? '' : + 'WHERE angel_types.hide_on_shift_view = 0 + OR user_angel_type.user_id IS NOT NULL ') . + 'ORDER BY `angel_types`.`name` ', [ $user->id, @@ -210,9 +235,9 @@ function view_user_shifts() $session = session(); $days = load_days(); - $rooms = load_rooms(); + $locations = load_locations(true); $types = load_types(); - $ownTypes = []; + $ownAngelTypes = []; /** @var EloquentCollection|UserAngelType[] $userAngelTypes */ $userAngelTypes = UserAngelType::whereUserId($user->id) @@ -223,12 +248,12 @@ function view_user_shifts() }) ->get(); foreach ($userAngelTypes as $type) { - $ownTypes[] = $type->angel_type_id; + $ownAngelTypes[] = $type->angel_type_id; } if (!$session->has('shifts-filter')) { - $room_ids = $rooms->pluck('id')->toArray(); - $shiftsFilter = new ShiftsFilter(auth()->can('user_shifts_admin'), $room_ids, $ownTypes); + $location_ids = $locations->pluck('id')->toArray(); + $shiftsFilter = new ShiftsFilter(auth()->can('user_shifts_admin'), $location_ids, $ownAngelTypes); $session->set('shifts-filter', $shiftsFilter->sessionExport()); } @@ -240,7 +265,7 @@ function view_user_shifts() $shiftCalendarRenderer = shiftCalendarRendererByShiftFilter($shiftsFilter); if (empty($user->api_key)) { - User_reset_api_key($user, false); + auth()->resetApiKey($user); } $filled = [ @@ -258,24 +283,28 @@ function view_user_shifts() $end_day = date('Y-m-d', $shiftsFilter->getEndTime()); $end_time = '23:59'; + $canSignUpForShifts = true; if (config('signup_requires_arrival') && !$user->state->arrived) { + $canSignUpForShifts = false; info(render_user_arrived_hint()); } $formattedDays = collect($days)->map(function ($value) { - return Carbon::make($value)->format(__('Y-m-d')); + return dateWithEventDay(Carbon::make($value)->format('Y-m-d')); })->toArray(); + $link = button(url('/admin-shifts'), icon('plus-lg'), 'add'); + return page([ div('col-md-12', [ - msg(), view(__DIR__ . '/../../resources/views/pages/user-shifts.html', [ 'title' => shifts_title(), - 'room_select' => make_select( - $rooms, - $shiftsFilter->getRooms(), - 'rooms', - icon('pin-map-fill') . __('Rooms') + 'add_link' => auth()->can('admin_shifts') ? $link : '', + 'location_select' => make_select( + $locations, + $shiftsFilter->getLocations(), + 'locations', + icon('pin-map-fill') . __('Locations') ), 'start_select' => html_select_key( 'start_day', @@ -295,14 +324,11 @@ function view_user_shifts() $types, $shiftsFilter->getTypes(), 'types', - icon('person-lines-fill') . __('Angeltypes') . '1', - [ - button( - 'javascript:checkOwnTypes(\'selection_types\', ' . json_encode($ownTypes) . ')', - __('Own'), - 'd-print-none' - ), - ] + icon('person-lines-fill') . __('angeltypes.angeltypes') + . ' ', + $ownAngelTypes ), 'filled_select' => make_select( $filled, @@ -310,12 +336,6 @@ function view_user_shifts() 'filled', icon('person-fill-slash') . __('Occupancy') ), - 'task_notice' => - '1' - . __('The tasks shown here are influenced by the angeltypes you joined already!') - . ' ' - . __('Description of the jobs.') - . '', 'shifts_table' => msg() . $shiftCalendarRenderer->render(), 'ical_text' => div('mt-3', ical_hint()), 'filter' => __('Filter'), @@ -327,7 +347,11 @@ function view_user_shifts() 'set_last_4h' => __('last 4h'), 'set_next_4h' => __('next 4h'), 'set_next_8h' => __('next 8h'), - 'buttons' => button( + 'random' => auth()->can('user_shifts') && $canSignUpForShifts ? button( + url('/shifts/random'), + icon('dice-4-fill') . __('shifts.random') + ) : '', + 'dashboard' => button( public_dashboard_link(), icon('speedometer2') . __('Public Dashboard') ), @@ -351,9 +375,9 @@ function ical_hint() return heading(__('iCal export and API') . ' ' . button_help('user/ical'), 2) . '

' . sprintf( __('Export your own shifts. iCal format or JSON format available (please keep secret, otherwise reset the api key).'), - page_link_to('ical', ['key' => $user->api_key]), - page_link_to('shifts_json_export', ['key' => $user->api_key]), - page_link_to('user_myshifts', ['reset' => 1]) + url('/ical', ['key' => $user->api_key]), + url('/shifts-json-export', ['key' => $user->api_key]), + url('/user-myshifts', ['reset' => 1]) ) . ' - ', $id); } -/** - * Render a bootstrap datepicker - * - * @param string $name Name of the parameter - * @param string $label Label - * @param int|Carbon $value Unix Timestamp - * @param string $start_date Earliest possible date - * @param string $end_date - * @return string HTML - */ -function form_date($name, $label, $value, $start_date = '', $end_date = '') -{ - $dom_id = $name . '-date'; - $value = ($value instanceof Carbon) ? $value->getTimestamp() : $value; - $value = is_numeric($value) ? date('Y-m-d', $value) : ''; - $start_date = is_numeric($start_date) ? date('Y-m-d', $start_date) : ''; - $end_date = is_numeric($end_date) ? date('Y-m-d', $end_date) : ''; - - return form_element( - $label, - '', - $dom_id - ); -} - /** * Render a bootstrap datepicker * @@ -93,24 +69,6 @@ function form_datetime(string $name, string $label, $value) ', $dom_id, $name, htmlspecialchars($value ? $value->format('Y-m-d H:i') : '')), $dom_id); } -/** - * Rendert eine Liste von Checkboxen fΓΌr ein Formular - * - * @param string $name Die Namen der Checkboxen werden aus name_key gebildet - * @param string $label Die Beschriftung der Liste - * @param array $items Array mit den einzelnen Checkboxen - * @param array $selected Array mit den Keys, die ausgewΓ€hlt sind - * @return string - */ -function form_checkboxes($name, $label, $items, $selected) -{ - $html = form_element($label, ''); - foreach ($items as $key => $item) { - $html .= form_checkbox($name . '_' . $key, $item, in_array($key, $selected)); - } - return $html; -} - /** * Rendert eine Checkbox * @@ -128,14 +86,15 @@ function form_checkbox($name, $label, $selected, $value = 'checked', $html_id = } return '

' - . '
'; } /** - * Rendert einen Radio + * Renders a radio button * * @param string $name * @param string $label @@ -184,9 +143,9 @@ function form_info($label, $text = '') * @param string $buttonType * @return string */ -function form_submit($name, $label, $class = '', $wrapForm = true, $buttonType = 'primary') +function form_submit($name, $label, $class = '', $wrapForm = true, $buttonType = 'primary', $title = '') { - $button = ''; @@ -232,67 +191,6 @@ function form_text($name, $label, $value, $disabled = false, $maxlength = null, ); } -/** - * Renders a text input with placeholder instead of label. - * - * @param string $name Input name - * @param string $placeholder Placeholder - * @param string $value The value - * @param boolean $disabled Is the field enabled? - * @return string - */ -function form_text_placeholder($name, $placeholder, $value, $disabled = false) -{ - $disabled = $disabled ? ' disabled="disabled"' : ''; - return form_element( - '', - '' - ); -} - -/** - * Rendert ein Formular-Emailfeld - * - * @param string $name - * @param string $label - * @param string $value - * @param bool $disabled - * @param string|null $autocomplete - * @param int|null $maxlength - * - * @return string - */ -function form_email($name, $label, $value, $disabled = false, $autocomplete = null, $maxlength = null) -{ - $disabled = $disabled ? ' disabled="disabled"' : ''; - $autocomplete = $autocomplete ? ' autocomplete="' . $autocomplete . '"' : ''; - $maxlength = $maxlength ? ' maxlength=' . (int) $maxlength : ''; - return form_element( - $label, - '', - 'form_' . $name - ); -} - -/** - * Rendert ein Formular-Dateifeld - * - * @param string $name - * @param string $label - * @return string - */ -function form_file($name, $label) -{ - return form_element( - $label, - sprintf('', $name), - 'form_' . $name - ); -} - /** * Rendert ein Formular-Passwortfeld * @@ -308,7 +206,7 @@ function form_password($name, $label, $autocomplete, $disabled = false) return form_element( $label, sprintf( - '', + '', $name, config('min_password_length'), $autocomplete, @@ -318,25 +216,6 @@ function form_password($name, $label, $autocomplete, $disabled = false) ); } -/** - * Renders a password input with placeholder instead of label. - * - * @param string $name - * @param string $placeholder - * @param bool $disabled - * @return string - */ -function form_password_placeholder($name, $placeholder, $disabled = false) -{ - $disabled = $disabled ? ' disabled="disabled"' : ''; - return form_element( - '', - '', - 'form_' . $name - ); -} - /** * Rendert ein Formular-Textfeld * @@ -406,14 +285,14 @@ function form_element($label, $input, $for = '', $class = '') * * @param string[] $elements * @param string $action - * @param bool $inline + * @param string $style * @return string */ -function form($elements, $action = '', $inline = false, $btnGroup = false) +function form($elements, $action = '', $style = '', $btnGroup = false) { return '
' + . ($style ? ' style="' . $style . '"' : '') . '>' . join($elements) . form_csrf() . '
'; @@ -463,9 +342,13 @@ function html_select_key($dom_id, $name, $rows, $selected, $selectText = '') } foreach ($rows as $key => $row) { if (($key == $selected) || ($row === $selected)) { - $html .= ''; + $html .= ''; } else { - $html .= ''; + $html .= ''; } } $html .= ''; diff --git a/includes/sys_menu.php b/includes/sys_menu.php index 806694cc5..3ca5a7a2f 100644 --- a/includes/sys_menu.php +++ b/includes/sys_menu.php @@ -1,20 +1,9 @@ addHint(user_angeltypes_unconfirmed_hint()); $hints_renderer->addHint(render_user_departure_date_hint()); $hints_renderer->addHint(user_driver_license_required_hint()); + $hints_renderer->addHint(user_ifsg_certificate_required_hint()); // Important hints: $hints_renderer->addHint(render_user_freeloader_hint(), true); - $hints_renderer->addHint(render_user_arrived_hint(), true); + $hints_renderer->addHint(render_user_arrived_hint(true), true); + $hints_renderer->addHint(render_user_pronoun_hint(), true); + $hints_renderer->addHint(render_user_firstname_hint(), true); + $hints_renderer->addHint(render_user_lastname_hint(), true); $hints_renderer->addHint(render_user_tshirt_hint(), true); $hints_renderer->addHint(render_user_dect_hint(), true); + $hints_renderer->addHint(render_user_mobile_hint(), true); return $hints_renderer->render(); } @@ -62,10 +56,10 @@ function make_navigation() $page = current_page(); $menu = []; $pages = [ - 'news' => __('News'), - 'meetings' => [__('Meetings'), 'user_meetings'], + 'news' => __('news.title'), + 'meetings' => [__('news.title.meetings'), 'user_meetings'], 'user_shifts' => __('Shifts'), - 'angeltypes' => __('Angeltypes'), + 'angeltypes' => __('angeltypes.angeltypes'), 'questions' => [__('Ask the Heaven'), 'question.add'], ]; @@ -75,23 +69,30 @@ function make_navigation() } $title = ((array) $options)[0]; - $menu[] = toolbar_item_link(page_link_to($menu_page), '', $title, $menu_page == $page); + $menu[] = toolbar_item_link( + url(str_replace('_', '-', $menu_page)), + '', + $title, + $menu_page == $page + ); } - $menu = make_room_navigation($menu); + $menu = make_location_navigation($menu); $admin_menu = []; $admin_pages = [ - // path => name - // path => [name, permission] + // Examples: + // path => name, + // path => [name, permission], + 'admin_arrive' => 'Arrive angels', 'admin_active' => 'Active angels', 'users' => ['All Angels', 'admin_user'], 'admin_free' => 'Free angels', 'admin/questions' => ['Answer questions', 'question.edit'], - 'shifttypes' => 'Shifttypes', + 'admin/shifttypes' => ['shifttype.shifttypes', 'shifttypes'], 'admin_shifts' => 'Create shifts', - 'admin/rooms' => ['room.rooms', 'admin_rooms'], + 'admin/locations' => ['location.locations', 'admin_locations'], 'admin_groups' => 'Grouprights', 'admin/schedule' => ['schedule.import', 'schedule.import'], 'admin/logs' => ['log.log', 'admin_log'], @@ -109,8 +110,8 @@ function make_navigation() $title = ((array) $options)[0]; $admin_menu[] = toolbar_dropdown_item( - page_link_to($menu_page), - __($title), + url(str_replace('_', '-', $menu_page)), + htmlspecialchars(__($title)), $menu_page == $page ); } @@ -141,31 +142,36 @@ function menu_is_allowed(string $page, $options) } /** - * Adds room navigation to the given menu. + * Adds location navigation to the given menu. * * @param string[] $menu Rendered menu * @return string[] */ -function make_room_navigation($menu) +function make_location_navigation($menu) { - if (!auth()->can('view_rooms')) { + if (!auth()->can('view_locations')) { return $menu; } - // Get a list of all rooms - $rooms = Room::orderBy('name')->get(); - $room_menu = []; - if (auth()->can('admin_rooms')) { - $room_menu[] = toolbar_dropdown_item(page_link_to('admin/rooms'), __('Manage rooms'), false, 'list'); + // Get a list of all locations + $locations = Location::orderBy('name')->get(); + $location_menu = []; + if (auth()->can('admin_locations')) { + $location_menu[] = toolbar_dropdown_item( + url('/admin/locations'), + __('Manage locations'), + false, + 'list' + ); } - if (count($room_menu) > 0) { - $room_menu[] = toolbar_dropdown_item_divider(); + if (count($location_menu) > 0) { + $location_menu[] = toolbar_dropdown_item_divider(); } - foreach ($rooms as $room) { - $room_menu[] = toolbar_dropdown_item(room_link($room), $room->name, false, 'pin-map-fill'); + foreach ($locations as $location) { + $location_menu[] = toolbar_dropdown_item(location_link($location), $location->name, false, 'pin-map-fill'); } - if (count($room_menu) > 0) { - $menu[] = toolbar_dropdown(__('Rooms'), $room_menu); + if (count($location_menu) > 0) { + $menu[] = toolbar_dropdown(__('Locations'), $location_menu); } return $menu; } @@ -209,7 +215,7 @@ function admin_new_questions() return null; } - return '' + return '' . __('There are unanswered questions!') . ''; } diff --git a/includes/sys_page.php b/includes/sys_page.php index a5eb71421..70aa7f93f 100644 --- a/includes/sys_page.php +++ b/includes/sys_page.php @@ -2,9 +2,7 @@ use Engelsystem\Helpers\Carbon; use Engelsystem\Http\Exceptions\HttpTemporaryRedirect; -use Engelsystem\Models\BaseModel; use Engelsystem\ValidationResult; -use Illuminate\Support\Collection; /** * Provide page/request helper functions @@ -63,41 +61,6 @@ function throw_redirect($url) throw new HttpTemporaryRedirect($url); } -/** - * Echoes given output and dies. - * - * @param string $output String to display - */ -function raw_output($output = '') -{ - echo $output; - die(); -} - -/** - * Helper function for transforming list of entities into array for select boxes. - * - * @param array|Collection $data The data array - * @param string $key_name name of the column to use as id/key - * @param string $value_name name of the column to use as displayed value - * - * @return array|Collection - */ -function select_array($data, $key_name, $value_name) -{ - if ($data instanceof Collection) { - return $data->mapWithKeys(function (BaseModel $model) use ($key_name, $value_name) { - return [$model->{$key_name} => $model->{$value_name}]; - }); - } - - $return = []; - foreach ($data as $value) { - $return[$value[$key_name]] = $value[$value_name]; - } - return $return; -} - /** * Returns an int[] from given request param name. * @@ -185,23 +148,6 @@ function strip_request_item($name, $default_value = null) return $default_value; } -/** - * Returns REQUEST value or default value (null) if not set. - * - * @param string $name - * @param string|null $default_value - * @return mixed|null - */ -function strip_request_tags($name, $default_value = null) -{ - $request = request(); - if ($request->has($name)) { - return strip_tags($request->input($name)); - } - - return $default_value; -} - /** * Testet, ob der angegebene REQUEST Wert ein Integer ist, bzw. * eine ID sein kΓΆnnte. @@ -251,20 +197,3 @@ function strip_item($item) // Only allow letters, symbols, punctuation, separators and numbers without html tags return preg_replace('/([^\p{L}\p{S}\p{P}\p{Z}\p{N}+]+)/ui', '', strip_tags($item)); } - -/** - * Validates an email address with support for IDN domain names. - * - * @param string $email - * @return bool - */ -function check_email($email) -{ - // Convert the domain part from idn to ascii - if (substr_count($email, '@') == 1) { - list($name, $domain) = explode('@', $email); - $domain = idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); - $email = $name . '@' . $domain; - } - return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); -} diff --git a/includes/sys_template.php b/includes/sys_template.php index 6fa452484..362824adf 100644 --- a/includes/sys_template.php +++ b/includes/sys_template.php @@ -1,5 +1,6 @@ ' . $text . ''; -} - /** * Renders a bootstrap label with given content and class. * @@ -188,7 +178,7 @@ function toolbar_item_link($href, $icon, $label, $active = false) return ''; } @@ -196,11 +186,11 @@ function toolbar_item_link($href, $icon, $label, $active = false) function toolbar_dropdown_item(string $href, string $label, bool $active, string $icon = null): string { return strtr( - '
  • {icon} {label}
  • ', + '
  • {icon} {label}
  • ', [ '{href}' => $href, '{icon}' => $icon === null ? '' : '', - '{label}' => $label, + '{label}' => htmlspecialchars($label), '{active}' => $active ? ' active' : '', '{aria}' => $active ? ' aria-current="page"' : '', ] @@ -235,7 +225,7 @@ function toolbar_dropdown($label, $submenu, $active = false): string $template, [ '{class}' => $active ? ' active' : '', - '{label}' => $label, + '{label}' => htmlspecialchars($label), '{submenu}' => join("\n", $submenu), ] ); @@ -338,7 +328,7 @@ function render_table($columns, $rows, $data = true) $html .= ''; $html .= ''; foreach ($rows as $row) { - $html .= ''; + $html .= ''; foreach ($columns as $key => $column) { $value = ' '; if (isset($row[$key])) { @@ -362,7 +352,7 @@ function render_table($columns, $rows, $data = true) * @param string $id * @return string */ -function button($href, $label, $class = '', $id = '') +function button($href, $label, $class = '', $id = '', $title = '') { if (!Str::contains(str_replace(['btn-sm', 'btn-xl'], '', $class), 'btn-')) { $class = 'btn-secondary' . ($class ? ' ' . $class : ''); @@ -370,20 +360,22 @@ function button($href, $label, $class = '', $id = '') $idAttribute = $id ? 'id="' . $id . '"' : ''; - return '' . $label . ''; + return '' . $label . ''; } /** - * Rendert einen Knopf mit JavaScript onclick Handler + * Renders a button to select corresponding checkboxes * - * @param string $javascript + * @param string $name * @param string $label - * @param string $class + * @param string $value * @return string */ -function button_js($javascript, $label, $class = '') +function button_checkbox_selection($name, $label, $value) { - return '' . $label . ''; + return ''; } /** @@ -395,9 +387,9 @@ function button_js($javascript, $label, $class = '') * * @return string */ -function button_icon($href, $icon, $class = '') +function button_icon($href, $icon, $class = '', $title = '') { - return button($href, icon($icon), $class); + return button($href, icon($icon), $class, '', $title); } /** @@ -426,7 +418,20 @@ function buttons($buttons = []) * @param array $buttons * @return string */ -function table_buttons($buttons = []) +function table_buttons($buttons = [], $additionalClass = '') +{ + return '
    ' . join('', $buttons) . '
    '; +} + +function user_info_icon(User $user): string { - return '
    ' . join('', $buttons) . '
    '; + if (!auth()->can('admin_arrive') || !$user->state->user_info) { + return ''; + } + $infoIcon = ' can('user.info.show')) { + $infoIcon .= 'data-bs-toggle="tooltip" title="' . htmlspecialchars($user->state->user_info) . '"'; + } + $infoIcon .= '>'; + return $infoIcon; } diff --git a/includes/view/AngelTypes_view.php b/includes/view/AngelTypes_view.php index 8da72751e..f01c2a6b9 100644 --- a/includes/view/AngelTypes_view.php +++ b/includes/view/AngelTypes_view.php @@ -26,7 +26,7 @@ function AngelType_name_render(AngelType $angeltype, $plain = false) } return '' - . ($angeltype->restricted ? icon('mortarboard-fill') : '') . $angeltype->name + . ($angeltype->restricted ? icon('mortarboard-fill') : '') . htmlspecialchars($angeltype->name) . ''; } @@ -60,12 +60,15 @@ function AngelType_render_membership(AngelType $user_angeltype) */ function AngelType_delete_view(AngelType $angeltype) { - return page_with_title(sprintf(__('Delete angeltype %s'), $angeltype->name), [ + $link = button($angeltype->id + ? url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]) + : url('/angeltypes'), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title($link . ' ' . sprintf(__('Delete angeltype %s'), htmlspecialchars($angeltype->name)), [ info(sprintf(__('Do you want to delete angeltype %s?'), $angeltype->name), true), form([ buttons([ - button(page_link_to('angeltypes'), icon('x-lg') . __('cancel')), - form_submit('delete', icon('trash') . __('delete'), 'btn-danger', false), + button(url('/angeltypes'), icon('x-lg') . __('form.cancel')), + form_submit('delete', icon('trash'), 'btn-danger', false, 'primary', __('form.delete')), ]), ]), ], true); @@ -80,56 +83,96 @@ function AngelType_delete_view(AngelType $angeltype) */ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode) { - return page_with_title(sprintf(__('Edit %s'), $angeltype->name), [ - buttons([ - button(page_link_to('angeltypes'), icon('person-lines-fill') . __('Angeltypes'), 'back'), - ]), - msg(), - form([ - $supporter_mode - ? form_info(__('Name'), $angeltype->name) - : form_text('name', __('Name'), $angeltype->name), - $supporter_mode - ? form_info(__('Requires introduction'), $angeltype->restricted ? __('Yes') : __('No')) - : form_checkbox('restricted', __('Requires introduction'), $angeltype->restricted), - form_info( - '', - __('Angel types which require introduction can only be used by an angel if enabled by a supporter (double opt-in).') - ), - $supporter_mode - ? form_info(__('No Self Sign Up allowed'), $angeltype->no_self_signup ? __('Yes') : __('No')) - : form_checkbox('no_self_signup', __('No Self Sign Up allowed'), $angeltype->no_self_signup), - $supporter_mode ? + $link = button($angeltype->id + ? url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]) + : url('/angeltypes'), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title( + $link . ' ' . ( + $angeltype->id ? + sprintf(__('Edit %s'), htmlspecialchars((string) $angeltype->name)) : + __('Create angeltype') + ), + [ + $angeltype->id ? + buttons([ + button(url('/angeltypes'), icon('person-lines-fill') . __('angeltypes.angeltypes'), 'back'), + ]) : '', + msg(), + form([ + $supporter_mode + ? form_info(__('general.name'), htmlspecialchars($angeltype->name)) + : form_text('name', __('general.name'), $angeltype->name), + $supporter_mode + ? form_info(__('angeltypes.restricted'), $angeltype->restricted ? __('Yes') : __('No')) + : form_checkbox( + 'restricted', + __('angeltypes.restricted') . + ' ', + $angeltype->restricted + ), + $supporter_mode + ? form_info(__('shift.self_signup'), $angeltype->shift_self_signup ? __('Yes') : __('No')) + : form_checkbox( + 'shift_self_signup', + __('shift.self_signup') . + ' ', + $angeltype->shift_self_signup + ), + $supporter_mode ? + form_info( + __('Requires driver license'), + $angeltype->requires_driver_license + ? __('Yes') + : __('No') + ) : + form_checkbox( + 'requires_driver_license', + __('Requires driver license'), + $angeltype->requires_driver_license + ), + $supporter_mode && config('ifsg_enabled') ? + form_info( + __('angeltype.ifsg.required'), + $angeltype->requires_ifsg_certificate + ? __('Yes') + : __('No') + ) : + form_checkbox( + 'requires_ifsg_certificate', + __('angeltype.ifsg.required'), + $angeltype->requires_ifsg_certificate + ), + $supporter_mode + ? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No')) + : form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard), + $supporter_mode + ? form_info(__('Hide at Registration'), $angeltype->hide_register ? __('Yes') : __('No')) + : form_checkbox('hide_register', __('Hide at Registration'), $angeltype->hide_register), + $supporter_mode + ? form_info(__('angeltypes.hide_on_shift_view'), $angeltype->hide_on_shift_view ? __('Yes') : __('No')) + : form_checkbox( + 'hide_on_shift_view', + __('angeltypes.hide_on_shift_view') . + ' ', + $angeltype->hide_on_shift_view + ), + form_textarea('description', __('general.description'), $angeltype->description), + form_info('', __('Please use markdown for the description.')), + heading(__('Contact'), 3), form_info( - __('Requires driver license'), - $angeltype->requires_driver_license - ? __('Yes') - : __('No') - ) : - form_checkbox( - 'requires_driver_license', - __('Requires driver license'), - $angeltype->requires_driver_license + '', + __('Primary contact person/desk for user questions.') ), - $supporter_mode - ? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No')) - : form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard), - $supporter_mode - ? form_info(__('Hide at Registration'), $angeltype->hide_register ? __('Yes') : __('No')) - : form_checkbox('hide_register', __('Hide at Registration'), $angeltype->hide_register), - form_textarea('description', __('Description'), $angeltype->description), - form_info('', __('Please use markdown for the description.')), - heading(__('Contact'), 3), - form_info( - '', - __('Primary contact person/desk for user questions.') - ), - form_text('contact_name', __('Name'), $angeltype->contact_name), - config('enable_dect') ? form_text('contact_dect', __('DECT'), $angeltype->contact_dect) : '', - form_text('contact_email', __('E-Mail'), $angeltype->contact_email), - form_submit('submit', __('Save')), - ]), - ]); + form_text('contact_name', __('general.name'), $angeltype->contact_name), + config('enable_dect') ? form_text('contact_dect', __('general.dect'), $angeltype->contact_dect) : '', + form_text('contact_email', __('general.email'), $angeltype->contact_email), + form_submit('submit', icon('save') . __('form.save')), + ]), + ] + ); } /** @@ -151,28 +194,40 @@ function AngelType_view_buttons( $user_driver_license, $user ) { - $buttons = [ - button(page_link_to('angeltypes'), icon('person-lines-fill') . __('Angeltypes'), 'back'), - ]; - if ($angeltype->requires_driver_license) { $buttons[] = button( - user_driver_license_edit_link($user), + url('/settings/certificates'), icon('person-vcard') . __('my driving license') ); } + if (config('isfg_enabled') && $angeltype->requires_ifsg_certificate) { + $buttons[] = button( + url('/settings/certificates'), + icon('card-checklist') . __('angeltype.ifsg.own') + ); + } if (is_null($user_angeltype)) { $buttons[] = button( - page_link_to('user_angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]), - icon('box-arrow-in-right') . __('join'), - 'add' + url('/user-angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]), + icon('box-arrow-in-right') . ($admin_angeltypes ? '' : __('Join')), + 'add', + '', + ($admin_angeltypes ? 'Join' : ''), ); } else { if ($angeltype->requires_driver_license && !$user_driver_license->wantsToDrive()) { error(__('This angeltype requires a driver license. Please enter your driver license information!')); } + if ( + config('ifsg_enabled') && $angeltype->requires_ifsg_certificate && !( + $user->license->ifsg_certificate_light || $user->license->ifsg_certificate + ) + ) { + error(__('angeltype.ifsg.required.info')); + } + if ($angeltype->restricted && !$user_angeltype->confirm_user_id) { error(sprintf( __('You are unconfirmed for this angeltype. Please go to the introduction for %s to get confirmed.'), @@ -180,21 +235,30 @@ function AngelType_view_buttons( )); } $buttons[] = button( - page_link_to('user_angeltypes', ['action' => 'delete', 'user_angeltype_id' => $user_angeltype->id]), - icon('box-arrow-right') . __('leave') + url('/user-angeltypes', ['action' => 'delete', 'user_angeltype_id' => $user_angeltype->id]), + icon('box-arrow-right') . ($admin_angeltypes ? '' : __('Leave')), + '', + '', + ($admin_angeltypes ? __('Leave') : ''), ); } if ($admin_angeltypes || $supporter) { $buttons[] = button( - page_link_to('angeltypes', ['action' => 'edit', 'angeltype_id' => $angeltype->id]), - icon('pencil') . __('edit') + url('/angeltypes', ['action' => 'edit', 'angeltype_id' => $angeltype->id]), + icon('pencil'), + '', + '', + __('form.edit') ); } if ($admin_angeltypes) { $buttons[] = button( - page_link_to('angeltypes', ['action' => 'delete', 'angeltype_id' => $angeltype->id]), - icon('trash') . __('delete') + url('/angeltypes', ['action' => 'delete', 'angeltype_id' => $angeltype->id]), + icon('trash'), + 'btn-danger', + '', + __('form.delete') ); } @@ -218,7 +282,7 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange foreach ($members as $member) { $member->name = User_Nick_render($member) . User_Pronoun_render($member); if (config('enable_dect')) { - $member['dect'] = $member->contact->dect; + $member['dect'] = htmlspecialchars((string) $member->contact->dect); } if ($angeltype->requires_driver_license) { $member['wants_to_drive'] = icon_bool($member->license->wantsToDrive()); @@ -229,23 +293,29 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange $member['has_license_12t_truck'] = icon_bool($member->license->drive_12t); $member['has_license_forklift'] = icon_bool($member->license->drive_forklift); } + if ($angeltype->requires_ifsg_certificate && config('ifsg_enabled')) { + $member['ifsg_certificate'] = icon_bool($member->license->ifsg_certificate); + if (config('ifsg_light_enabled')) { + $member['ifsg_certificate_light'] = icon_bool($member->license->ifsg_certificate_light); + } + } if ($angeltype->restricted && empty($member->pivot->confirm_user_id)) { $member['actions'] = table_buttons([ button( - page_link_to( - 'user_angeltypes', + url( + '/user-angeltypes', ['action' => 'confirm', 'user_angeltype_id' => $member->pivot->id] ), - __('confirm'), + __('Confirm'), 'btn-sm' ), button( - page_link_to( - 'user_angeltypes', + url( + '/user-angeltypes', ['action' => 'delete', 'user_angeltype_id' => $member->pivot->id] ), - __('deny'), + __('Deny'), 'btn-sm' ), ]); @@ -254,13 +324,15 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange if ($admin_angeltypes) { $member['actions'] = table_buttons([ button( - page_link_to('user_angeltypes', [ + url('/user-angeltypes', [ 'action' => 'update', 'user_angeltype_id' => $member->pivot->id, 'supporter' => 0, ]), - icon('person-fill-down') . __('Remove supporter rights'), - 'btn-sm' + icon('person-fill-down'), + 'btn-sm', + '', + __('Remove supporter rights'), ), ]); } else { @@ -272,22 +344,26 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange $member['actions'] = table_buttons([ $admin_angeltypes ? button( - page_link_to('user_angeltypes', [ + url('/user-angeltypes', [ 'action' => 'update', 'user_angeltype_id' => $member->pivot->id, 'supporter' => 1, ]), - icon('person-fill-up') . __('Add supporter rights'), - 'btn-sm' + icon('person-fill-up'), + 'btn-sm', + '', + __('Add supporter rights'), ) : '', button( - page_link_to('user_angeltypes', [ + url('/user-angeltypes', [ 'action' => 'delete', 'user_angeltype_id' => $member->pivot->id, ]), - icon('trash') . __('remove'), - 'btn-sm' + icon('trash'), + 'btn-sm btn-danger', + '', + __('Remove'), ), ]); } @@ -313,27 +389,34 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_angeltypes) { $headers = [ - 'name' => __('Nick'), - 'dect' => __('DECT'), - 'actions' => '', + 'name' => __('general.nick'), ]; + + if (config('enable_dect')) { + $headers['dect'] = __('general.dect'); + } + if ($angeltype->requires_driver_license && ($supporter || $admin_angeltypes)) { - $headers = [ - 'name' => __('Nick'), - 'dect' => __('DECT'), + $headers = array_merge($headers, [ 'wants_to_drive' => __('Driver'), 'has_car' => __('Has car'), - 'has_license_car' => __('Car'), - 'has_license_3_5t_transporter' => __('3,5t Transporter'), - 'has_license_7_5t_truck' => __('7,5t Truck'), - 'has_license_12t_truck' => __('12t Truck'), - 'has_license_forklift' => __('Forklift'), - 'actions' => '', - ]; + 'has_license_car' => __('settings.certificates.drive_car'), + 'has_license_3_5t_transporter' => __('settings.certificates.drive_3_5t'), + 'has_license_7_5t_truck' => __('settings.certificates.drive_7_5t'), + 'has_license_12t_truck' => __('settings.certificates.drive_12t'), + 'has_license_forklift' => __('settings.certificates.drive_forklift'), + ]); } - if (!config('enable_dect')) { - unset($headers['dect']); + + if (config('ifsg_enabled') && $angeltype->requires_ifsg_certificate && ($supporter || $admin_angeltypes)) { + if (config('ifsg_light_enabled')) { + $headers['ifsg_certificate_light'] = __('ifsg.certificate_light'); + } + $headers['ifsg_certificate'] = __('ifsg.certificate'); } + + $headers['actions'] = ''; + return $headers; } @@ -366,24 +449,29 @@ function AngelType_view( ShiftCalendarRenderer $shiftCalendarRenderer, $tab ) { - return page_with_title(sprintf(__('Team %s'), $angeltype->name), [ - AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_driver_license, $user), - msg(), - tabs([ - __('Info') => AngelType_view_info( - $angeltype, - $members, - $admin_user_angeltypes, - $admin_angeltypes, - $supporter - ), - __('Shifts') => AngelType_view_shifts( - $angeltype, - $shiftsFilterRenderer, - $shiftCalendarRenderer - ), - ], $tab), - ], true); + $link = button(url('/angeltypes'), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title( + $link . ' ' . sprintf(__('Team %s'), htmlspecialchars($angeltype->name)), + [ + AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_driver_license, $user), + msg(), + tabs([ + __('Info') => AngelType_view_info( + $angeltype, + $members, + $admin_user_angeltypes, + $admin_angeltypes, + $supporter + ), + __('Shifts') => AngelType_view_shifts( + $angeltype, + $shiftsFilterRenderer, + $shiftCalendarRenderer + ), + ], $tab), + ], + true + ); } /** @@ -394,7 +482,7 @@ function AngelType_view( */ function AngelType_view_shifts(AngelType $angeltype, $shiftsFilterRenderer, $shiftCalendarRenderer) { - $shifts = $shiftsFilterRenderer->render(page_link_to('angeltypes', [ + $shifts = $shiftsFilterRenderer->render(url('/angeltypes', [ 'action' => 'view', 'angeltype_id' => $angeltype->id, ]), ['type' => $angeltype->id]); @@ -423,10 +511,10 @@ function AngelType_view_info( $info[] = AngelTypes_render_contact_info($angeltype); } - $info[] = '

    ' . __('Description') . '

    '; + $info[] = '

    ' . __('general.description') . '

    '; $parsedown = new Parsedown(); if ($angeltype->description != '') { - $info[] = $parsedown->parse($angeltype->description); + $info[] = $parsedown->parse(htmlspecialchars($angeltype->description)); } list($supporters, $members_confirmed, $members_unconfirmed) = AngelType_view_members( @@ -462,11 +550,11 @@ function AngelType_view_info( if ($admin_user_angeltypes) { $info[] = buttons([ button( - page_link_to( - 'user_angeltypes', + url( + '/user-angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id] ), - __('Add'), + icon('plus-lg') . __('Add'), 'add' ), ]); @@ -477,12 +565,12 @@ function AngelType_view_info( $info[] = '

    ' . __('Unconfirmed') . '

    '; $info[] = buttons([ button( - page_link_to('user_angeltypes', ['action' => 'confirm_all', 'angeltype_id' => $angeltype->id]), - icon('check-lg') . __('confirm all') + url('/user-angeltypes', ['action' => 'confirm_all', 'angeltype_id' => $angeltype->id]), + icon('check-lg') . __('Confirm all') ), button( - page_link_to('user_angeltypes', ['action' => 'delete_all', 'angeltype_id' => $angeltype->id]), - icon('trash') . __('deny all') + url('/user-angeltypes', ['action' => 'delete_all', 'angeltype_id' => $angeltype->id]), + icon('trash') . __('Deny all') ), ]); $info[] = table($table_headers, $members_unconfirmed); @@ -500,9 +588,20 @@ function AngelType_view_info( function AngelTypes_render_contact_info(AngelType $angeltype) { $info = [ - __('Name') => [$angeltype->contact_name, $angeltype->contact_name], - __('DECT') => config('enable_dect') ? [sprintf('%1$s', $angeltype->contact_dect), $angeltype->contact_dect] : null, - __('E-Mail') => [sprintf('%1$s', $angeltype->contact_email), $angeltype->contact_email], + __('general.name') => [ + htmlspecialchars($angeltype->contact_name), + htmlspecialchars($angeltype->contact_name), + ], + __('general.dect') => config('enable_dect') + ? [ + sprintf('%1$s', htmlspecialchars($angeltype->contact_dect)), + htmlspecialchars($angeltype->contact_dect), + ] + : null, + __('general.email') => [ + sprintf('%1$s', htmlspecialchars($angeltype->contact_email)), + htmlspecialchars($angeltype->contact_email), + ], ]; $contactInfo = []; foreach ($info as $name => $data) { @@ -523,20 +622,22 @@ function AngelTypes_render_contact_info(AngelType $angeltype) */ function AngelTypes_list_view($angeltypes, bool $admin_angeltypes) { - return page_with_title(angeltypes_title(), [ - msg(), - buttons([ - $admin_angeltypes - ? button(page_link_to('angeltypes', ['action' => 'edit']), __('New angeltype'), 'add') - : '', - button(url('/angeltypes/about'), __('angeltypes.about')), - ]), - table([ - 'name' => __('Name'), - 'is_restricted' => icon('mortarboard-fill') . __('Requires introduction'), - 'no_self_signup_allowed' => icon('pencil-square') . __('Self Sign Up Allowed'), - 'membership' => __('Membership'), - 'actions' => '', - ], $angeltypes), - ], true); + $link = button(url('/angeltypes', ['action' => 'edit']), icon('plus-lg'), 'add'); + return page_with_title( + angeltypes_title() . ' ' . ($admin_angeltypes ? $link : ''), + [ + msg(), + buttons([ + button(url('/angeltypes/about'), __('angeltypes.about')), + ]), + table([ + 'name' => __('general.name'), + 'is_restricted' => icon('mortarboard-fill') . __('angeltypes.restricted'), + 'shift_self_signup_allowed' => icon('pencil-square') . __('shift.self_signup.allowed'), + 'membership' => __('Membership'), + 'actions' => '', + ], $angeltypes), + ], + true, + ); } diff --git a/includes/view/EventConfig_view.php b/includes/view/EventConfig_view.php index 296c0e0a3..7a9c026de 100644 --- a/includes/view/EventConfig_view.php +++ b/includes/view/EventConfig_view.php @@ -42,7 +42,7 @@ function EventConfig_edit_view( ]), div('row', [ div('col-md-6', [ - form_submit('submit', __('Save')), + form_submit('submit', icon('save') . __('form.save')), ]), ]), ]), diff --git a/includes/view/Locations_view.php b/includes/view/Locations_view.php new file mode 100644 index 000000000..6e9b3536a --- /dev/null +++ b/includes/view/Locations_view.php @@ -0,0 +1,117 @@ +user(); + + $assignNotice = ''; + if (config('signup_requires_arrival') && !$user->state->arrived) { + $assignNotice = info(render_user_arrived_hint(), true); + } + + $description = ''; + if ($location->description) { + $description = '

    ' . __('general.description') . '

    '; + $parsedown = new Parsedown(); + $description .= $parsedown->parse(htmlspecialchars($location->description)); + } + + $neededAngelTypes = ''; + if (auth()->can('admin_shifts') && $location->neededAngelTypes->isNotEmpty()) { + $neededAngelTypes .= '

    ' . __('location.required_angels') . '

    '; + } + + $dect = ''; + if (config('enable_dect') && $location->dect) { + $dect = heading(__('Contact'), 3) + . description([__('general.dect') => sprintf( + '%1$s', + htmlspecialchars($location->dect) + )]); + } + + $tabs = []; + if ($location->map_url) { + $tabs[__('location.map_url')] = sprintf( + '
    ' + . '' + . '
    ', + htmlspecialchars($location->map_url) + ); + } + + $tabs[__('Shifts')] = div('first', [ + $shiftsFilterRenderer->render(url('/locations', [ + 'action' => 'view', + 'location_id' => $location->id, + ]), ['locations' => [$location->id]]), + $shiftCalendarRenderer->render(), + ]); + + $selected_tab = 0; + $request = request(); + if ($request->has('shifts_filter_day')) { + $selected_tab = count($tabs) - 1; + } + + $link = button(url('/admin/locations'), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title( + (auth()->can('admin_locations') ? $link . ' ' : '') . + icon('pin-map-fill') . htmlspecialchars($location->name), + [ + $assignNotice, + auth()->can('admin_locations') ? buttons([ + button( + url('/admin/locations/edit/' . $location->id), + icon('pencil'), + '', + '', + __('form.edit') + ), + ]) : '', + $dect, + $description, + $neededAngelTypes, + tabs($tabs, $selected_tab), + ], + true + ); +} + +/** + * + * @param Location $location + * @return string + */ +function location_name_render(Location $location) +{ + if (auth()->can('view_locations')) { + return '' + . icon('pin-map-fill') . htmlspecialchars($location->name) + . ''; + } + + return icon('pin-map-fill') . htmlspecialchars($location->name); +} diff --git a/includes/view/PublicDashboard_view.php b/includes/view/PublicDashboard_view.php index 29837176a..c40448f46 100644 --- a/includes/view/PublicDashboard_view.php +++ b/includes/view/PublicDashboard_view.php @@ -1,5 +1,6 @@ isNotEmpty()) { - $first_news = $important_news->first(); + if ($highlighted_news->isNotEmpty()) { + $first_news = $highlighted_news->first(); $news = div('alert alert-warning text-center', [ - '' . $first_news->title . '', + '' + . '' . htmlspecialchars($first_news->title) . '' + . '', ]); } @@ -43,17 +46,25 @@ function public_dashboard_view($stats, $free_shifts, $important_news) ]); } + $stats = [ + stats(__('Angels needed in the next 3 hrs'), $stats['needed-3-hours']), + stats(__('Angels needed for nightshifts'), $stats['needed-night']), + stats(__('Angels currently working'), $stats['angels-working'], 'default'), + stats(__('Hours to be worked'), $stats['hours-to-work'], 'default'), + ]; + + $dayOfEvent = DayOfEvent::get(); + + if (config('enable_show_day_of_event') && $dayOfEvent !== null) { + $stats[] = stats(__('dashboard.day'), $dayOfEvent, 'default'); + } + $isFiltered = request()->get('filtered'); - $filter = collect(session()->get('shifts-filter'))->only(['rooms', 'types'])->toArray(); + $filter = collect(session()->get('shifts-filter'))->only(['locations', 'types'])->toArray(); return page([ div('wrapper', [ div('public-dashboard', [ - div('first row', [ - stats(__('Angels needed in the next 3 hrs'), $stats['needed-3-hours']), - stats(__('Angels needed for nightshifts'), $stats['needed-night']), - stats(__('Angels currently working'), $stats['angels-working'], 'default'), - stats(__('Hours to be worked'), $stats['hours-to-work'], 'default'), - ], 'statistics'), + div('first row', $stats, 'statistics'), $news, $needed_angels, ], 'public-dashboard'), @@ -84,17 +95,17 @@ function public_dashboard_shift_render($shift) $panel_body = icon('clock-history') . $shift['start'] . ' - ' . $shift['end']; $panel_body .= ' (' . $shift['duration'] . ' h)'; - $panel_body .= '
    ' . icon('list-task') . $shift['shifttype_name']; + $panel_body .= '
    ' . icon('list-task') . htmlspecialchars($shift['shifttype_name']); if (!empty($shift['title'])) { - $panel_body .= ' (' . $shift['title'] . ')'; + $panel_body .= ' (' . htmlspecialchars($shift['title']) . ')'; } - $panel_body .= '
    ' . icon('pin-map-fill') . $shift['room_name']; + $panel_body .= '
    ' . icon('pin-map-fill') . htmlspecialchars($shift['location_name']); foreach ($shift['needed_angels'] as $needed_angels) { $panel_body .= '
    ' . icon('person') . '' - . $needed_angels['need'] . ' × ' . $needed_angels['angeltype_name'] + . $needed_angels['need'] . ' × ' . htmlspecialchars($needed_angels['angeltype_name']) . ''; } diff --git a/includes/view/Rooms_view.php b/includes/view/Rooms_view.php deleted file mode 100644 index f1ba4c4c5..000000000 --- a/includes/view/Rooms_view.php +++ /dev/null @@ -1,86 +0,0 @@ -user(); - - $assignNotice = ''; - if (config('signup_requires_arrival') && !$user->state->arrived) { - $assignNotice = info(render_user_arrived_hint(), true); - } - - $description = ''; - if ($room->description) { - $description = '

    ' . __('Description') . '

    '; - $parsedown = new Parsedown(); - $description .= $parsedown->parse($room->description); - } - - $dect = ''; - if (config('enable_dect') && $room->dect) { - $dect = heading(__('Contact'), 3) - . description([__('DECT') => sprintf('%1$s', $room->dect)]); - } - - $tabs = []; - if ($room->map_url) { - $tabs[__('Map')] = sprintf( - '
    ' - . '' - . '
    ', - $room->map_url - ); - } - - $tabs[__('Shifts')] = div('first', [ - $shiftsFilterRenderer->render(page_link_to('rooms', [ - 'action' => 'view', - 'room_id' => $room->id, - ]), ['rooms' => [$room->id]]), - $shiftCalendarRenderer->render(), - ]); - - $selected_tab = 0; - $request = request(); - if ($request->has('shifts_filter_day')) { - $selected_tab = count($tabs) - 1; - } - - return page_with_title(icon('pin-map-fill') . $room->name, [ - $assignNotice, - auth()->can('admin_rooms') ? buttons([ - button( - page_link_to('admin/rooms/edit/' . $room->id), - icon('pencil') . __('edit') - ), - ]) : '', - $dect, - $description, - tabs($tabs, $selected_tab), - ], true); -} - -/** - * - * @param Room $room - * @return string - */ -function Room_name_render(Room $room) -{ - if (auth()->can('view_rooms')) { - return '' . icon('pin-map-fill') . $room->name . ''; - } - - return icon('pin-map-fill') . $room->name; -} diff --git a/includes/view/ShiftCalendarRenderer.php b/includes/view/ShiftCalendarRenderer.php index d910bed31..c62051de4 100644 --- a/includes/view/ShiftCalendarRenderer.php +++ b/includes/view/ShiftCalendarRenderer.php @@ -2,6 +2,7 @@ namespace Engelsystem; +use Engelsystem\Helpers\Carbon; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; use Illuminate\Support\Collection; @@ -61,28 +62,28 @@ public function __construct($shifts, private $needed_angeltypes, private $shift_ } /** - * Assigns the shifts to different lanes per room if they collide + * Assigns the shifts to different lanes per location if they collide * * @param Shift[] $shifts The shifts to assign - * @return array Returns an array that assigns a room_id to an array of ShiftCalendarLane containing the shifts + * @return array Returns an array that assigns a location_id to an array of ShiftCalendarLane containing the shifts */ private function assignShiftsToLanes($shifts) { - // array that assigns a room id to a list of lanes (per room) + // array that assigns a location id to a list of lanes (per location) $lanes = []; foreach ($shifts as $shift) { - $room = $shift->room; - $header = Room_name_render($room); - if (!isset($lanes[$room->id])) { - // initialize room with one lane - $lanes[$room->id] = [ + $location = $shift->location; + $header = location_name_render($location); + if (!isset($lanes[$location->id])) { + // initialize location with one lane + $lanes[$location->id] = [ new ShiftCalendarLane($header), ]; } - // Try to add the shift to the existing lanes for this room + // Try to add the shift to the existing lanes for this location $shift_added = false; - foreach ($lanes[$room->id] as $lane) { + foreach ($lanes[$location->id] as $lane) { /** @var ShiftCalendarLane $lane */ if ($lane->shiftFits($shift)) { $lane->addShift($shift); @@ -90,11 +91,11 @@ private function assignShiftsToLanes($shifts) break; } } - // If all lanes for this room are busy, create a new lane and add shift to it + // If all lanes for this location are busy, create a new lane and add shift to it if (!$shift_added) { $newLane = new ShiftCalendarLane($header); $newLane->addShift($shift); - $lanes[$room->id][] = $newLane; + $lanes[$location->id][] = $newLane; } } @@ -153,8 +154,8 @@ public function render() private function renderShiftLanes() { $html = ''; - foreach ($this->lanes as $room_lanes) { - foreach ($room_lanes as $lane) { + foreach ($this->lanes as $location_lanes) { + foreach ($location_lanes as $lane) { $html .= $this->renderLane($lane); } } @@ -212,20 +213,21 @@ private function renderLane(ShiftCalendarLane $lane) */ private function renderTick($time, $label = false) { + $time = Carbon::createFromTimestamp($time); $class = $label ? 'tick bg-' . theme_type() : 'tick '; - if ($time % (24 * 60 * 60) == 23 * 60 * 60) { + if ($time->isStartOfDay()) { if (!$label) { return div($class . ' day'); } return div($class . ' day', [ - date(__('m-d'), $time) . '
    ' . date(__('H:i'), $time), + $time->format(__('m-d')) . '
    ' . $time->format(__('H:i')), ]); - } elseif ($time % (60 * 60) == 0) { + } elseif ($time->isStartOfHour()) { if (!$label) { return div($class . ' hour'); } return div($class . ' hour', [ - date(__('m-d'), $time) . '
    ' . date(__('H:i'), $time), + $time->format(__('m-d')) . '
    ' . $time->format(__('H:i')), ]); } return div($class); @@ -242,7 +244,7 @@ private function renderTimeLane() $time_slot = [ div('header ' . $bg, [ - __('Time'), + __('log.time'), ]), ]; for ($block = 0; $block < $this->getBlocksPerSlot(); $block++) { @@ -312,7 +314,7 @@ private function renderLegend() badge(__('Help needed'), 'danger'), badge(__('Other angeltype needed / collides with my shifts'), 'warning'), badge(__('Shift is full'), 'success'), - badge(__('Shift running/ended or user not arrived/allowed'), 'secondary'), + badge(__('Shift is running/ended or you have not arrived'), 'secondary'), ]); } } diff --git a/includes/view/ShiftCalendarShiftRenderer.php b/includes/view/ShiftCalendarShiftRenderer.php index 3ab89147c..4a20e7224 100644 --- a/includes/view/ShiftCalendarShiftRenderer.php +++ b/includes/view/ShiftCalendarShiftRenderer.php @@ -29,7 +29,7 @@ public function render(Shift $shift, $needed_angeltypes, $shift_entries, $user) { $info_text = ''; if ($shift->title != '') { - $info_text = icon('info-circle') . $shift->title . '
    '; + $info_text = icon('info-circle') . htmlspecialchars($shift->title) . '
    '; } list($shift_signup_state, $shifts_row) = $this->renderShiftNeededAngeltypes( $shift, @@ -43,8 +43,6 @@ public function render(Shift $shift, $needed_angeltypes, $shift_entries, $user) $blocks = ceil(($shift->end->timestamp - $shift->start->timestamp) / ShiftCalendarRenderer::SECONDS_PER_ROW); $blocks = max(1, $blocks); - $room = $shift->room; - return [ $blocks, div( @@ -57,7 +55,7 @@ public function render(Shift $shift, $needed_angeltypes, $shift_entries, $user) $this->renderShiftHead($shift, $class, $shift_signup_state->getFreeEntries()), div('card-body ' . $this->classBg(), [ $info_text, - Room_name_render($room), + location_name_render($shift->location), ]), $shifts_row, ] @@ -192,14 +190,14 @@ private function renderShiftNeededAngeltype(Shift $shift, $shift_entries, $angel // No link and add a text hint, when the shift ended ShiftSignupStatus::NOT_ARRIVED => $inner_text . ' (' . __('please arrive for signup') . ')', ShiftSignupStatus::NOT_YET => $inner_text . ' (' . __('not yet') . ')', - ShiftSignupStatus::ANGELTYPE => $angeltype->restricted - // User has to be confirmed on the angeltype first + ShiftSignupStatus::ANGELTYPE => $angeltype->restricted || !$angeltype->shift_self_signup + // User has to be confirmed on the angeltype first or can't sign up by themselves ? $inner_text . icon('mortarboard-fill') // Add link to join the angeltype first : $inner_text . '
    ' . button( - page_link_to('user_angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]), - sprintf(__('Become %s'), $angeltype->name), + url('/user-angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]), + sprintf(__('Become %s'), htmlspecialchars($angeltype->name)), 'btn-sm' ), // Shift collides or user is already signed up: No signup allowed @@ -249,20 +247,24 @@ private function renderShiftHead(Shift $shift, $class, $needed_angeltypes_count) if (auth()->can('admin_shifts')) { $header_buttons = '
    ' . table_buttons([ button( - page_link_to('user_shifts', ['edit_shift' => $shift->id]), + url('/user-shifts', ['edit_shift' => $shift->id]), icon('pencil'), - 'btn-' . $class . ' btn-sm border-light text-white' + 'btn-' . $class . ' btn-sm border-light text-white', + '', + __('form.edit') ), button( - page_link_to('user_shifts', ['delete_shift' => $shift->id]), + url('/user-shifts', ['delete_shift' => $shift->id]), icon('trash'), - 'btn-' . $class . ' btn-sm border-light text-white' + 'btn-' . $class . ' btn-sm border-light text-white', + '', + __('form.delete') ), ]) . '
    '; } $shift_heading = $shift->start->format('H:i') . ' ‐ ' . $shift->end->format('H:i') . ' — ' - . $shift->shiftType->name; + . htmlspecialchars($shift->shiftType->name); if ($needed_angeltypes_count > 0) { $shift_heading = '' . $needed_angeltypes_count . ' ' . $shift_heading; diff --git a/includes/view/ShiftEntry_view.php b/includes/view/ShiftEntry_view.php index 31242b46c..4420b414b 100644 --- a/includes/view/ShiftEntry_view.php +++ b/includes/view/ShiftEntry_view.php @@ -1,7 +1,7 @@ displayName, $shift->shiftType->name, - $shift->start->format(__('Y-m-d H:i')), - $shift->end->format(__('Y-m-d H:i')), + $shift->start->format(__('general.datetime')), + $shift->end->format(__('general.datetime')), $angeltype->name ), true), form([ buttons([ - button(user_link($signoff_user->id), icon('x-lg') . __('cancel')), - form_submit('delete', icon('trash') . __('sign off'), 'btn-danger', false), + button(user_link($signoff_user->id), icon('x-lg') . __('form.cancel')), + form_submit('delete', icon('trash'), 'btn-danger', false, 'primary', __('Sign off')), ]), ]), ]); @@ -49,15 +49,22 @@ function ShiftEntry_delete_view(Shift $shift, AngelType $angeltype, User $signof info(sprintf( __('Do you want to sign off from your shift %s from %s to %s as %s?'), $shift->shiftType->name, - $shift->start->format(__('Y-m-d H:i')), - $shift->end->format(__('Y-m-d H:i')), + $shift->start->format(__('general.datetime')), + $shift->end->format(__('general.datetime')), $angeltype->name ), true), form([ buttons([ - button(user_link($signoff_user->id), icon('x-lg') . __('cancel')), - form_submit('delete', icon('trash') . __('delete'), 'btn-danger', false), + button(user_link($signoff_user->id), icon('x-lg') . __('form.cancel')), + form_submit( + 'delete', + icon('trash'), + 'btn-danger', + false, + 'danger', + __('Sign off') + ), ]), ]), ]); @@ -75,7 +82,7 @@ function ShiftEntry_delete_title() * Admin puts user into shift. * * @param Shift $shift - * @param Room $room + * @param Location $location * @param AngelType $angeltype * @param array $angeltypes_select * @param User $signup_user @@ -84,23 +91,23 @@ function ShiftEntry_delete_title() */ function ShiftEntry_create_view_admin( Shift $shift, - Room $room, + Location $location, AngelType $angeltype, $angeltypes_select, $signup_user, $users_select ) { - $start = $shift->start->format(__('Y-m-d H:i')); + $start = $shift->start->format(__('general.datetime')); return page_with_title( - ShiftEntry_create_title() . ': ' . $shift->shiftType->name + ShiftEntry_create_title() . ': ' . htmlspecialchars($shift->shiftType->name) . ' %c', [ - Shift_view_header($shift, $room), + Shift_view_header($shift, $location), info(__('Do you want to sign up the following user for this shift?'), true), form([ form_select('angeltype_id', __('Angeltype'), $angeltypes_select, $angeltype->id), - form_select('user_id', __('User'), $users_select, $signup_user->id), - form_submit('submit', icon('check-lg') . __('Save')), + form_select('user_id', __('general.user'), $users_select, $signup_user->id), + form_submit('submit', icon('save') . __('form.save')), ]), ] ); @@ -110,27 +117,32 @@ function ShiftEntry_create_view_admin( * Supporter puts user into shift. * * @param Shift $shift - * @param Room $room + * @param Location $location * @param AngelType $angeltype * @param User $signup_user * @param array $users_select * @return string */ -function ShiftEntry_create_view_supporter(Shift $shift, Room $room, AngelType $angeltype, $signup_user, $users_select) -{ - $start = $shift->start->format(__('Y-m-d H:i')); +function ShiftEntry_create_view_supporter( + Shift $shift, + Location $location, + AngelType $angeltype, + $signup_user, + $users_select +) { + $start = $shift->start->format(__('general.datetime')); return page_with_title( - ShiftEntry_create_title() . ': ' . $shift->shiftType->name + ShiftEntry_create_title() . ': ' . htmlspecialchars($shift->shiftType->name) . ' %c', [ - Shift_view_header($shift, $room), + Shift_view_header($shift, $location), info(sprintf( __('Do you want to sign up the following user for this shift as %s?'), $angeltype->name ), true), form([ - form_select('user_id', __('User'), $users_select, $signup_user->id), - form_submit('submit', icon('check-lg') . __('Save')), + form_select('user_id', __('general.user'), $users_select, $signup_user->id), + form_submit('submit', icon('save') . __('form.save')), ]), ] ); @@ -140,23 +152,23 @@ function ShiftEntry_create_view_supporter(Shift $shift, Room $room, AngelType $a * User joining a shift. * * @param Shift $shift - * @param Room $room + * @param Location $location * @param AngelType $angeltype * @param string $comment * @return string */ -function ShiftEntry_create_view_user(Shift $shift, Room $room, AngelType $angeltype, $comment) +function ShiftEntry_create_view_user(Shift $shift, Location $location, AngelType $angeltype, $comment) { - $start = $shift->start->format(__('Y-m-d H:i')); + $start = $shift->start->format(__('general.datetime')); return page_with_title( - ShiftEntry_create_title() . ': ' . $shift->shiftType->name + ShiftEntry_create_title() . ': ' . htmlspecialchars($shift->shiftType->name) . ' %c', [ - Shift_view_header($shift, $room), + Shift_view_header($shift, $location), info(sprintf(__('Do you want to sign up for this shift as %s?'), $angeltype->name), true), form([ form_textarea('comment', __('Comment (for your eyes only):'), $comment), - form_submit('submit', icon('check-lg') . __('Save')), + form_submit('submit', icon('save') . __('form.save')), ]), ] ); @@ -213,17 +225,27 @@ function ShiftEntry_edit_view( $comment = ''; } - return page_with_title(__('Edit shift entry'), [ - msg(), - form([ - form_info(__('Angel:'), User_Nick_render($angel)), - form_info(__('Date, Duration:'), $date), - form_info(__('Location:'), $location), - form_info(__('Title:'), $title), - form_info(__('Type:'), $type), - $comment, - join('', $freeload_form), - form_submit('submit', __('Save')), - ]), - ]); + $link = button( + url('/users', ['action' => 'view', 'user_id' => $angel->id]), + icon('chevron-left'), + 'btn-sm', + '', + __('general.back'), + ); + return page_with_title( + $link . ' ' . __('Edit shift entry'), + [ + msg(), + form([ + form_info(__('Angel:'), User_Nick_render($angel)), + form_info(__('Date, Duration:'), $date), + form_info(__('Location:'), htmlspecialchars($location)), + form_info(__('Title:'), htmlspecialchars($title)), + form_info(__('Type:'), htmlspecialchars($type)), + $comment, + join('', $freeload_form), + form_submit('submit', icon('save') . __('form.save')), + ]), + ] + ); } diff --git a/includes/view/ShiftTypes_view.php b/includes/view/ShiftTypes_view.php deleted file mode 100644 index 4f2a7b071..000000000 --- a/includes/view/ShiftTypes_view.php +++ /dev/null @@ -1,127 +0,0 @@ -can('shifttypes')) { - return '' . $shifttype->name . ''; - } - return $shifttype->name; -} - -/** - * @param ShiftType $shifttype - * @return string - */ -function ShiftType_delete_view(ShiftType $shifttype) -{ - return page_with_title(sprintf(__('Delete shifttype %s'), $shifttype->name), [ - info(sprintf(__('Do you want to delete shifttype %s?'), $shifttype->name), true), - form([ - buttons([ - button(page_link_to('shifttypes'), icon('x-lg') . __('cancel')), - form_submit( - 'delete', - icon('trash') . __('delete'), - 'btn-danger', - false - ), - ]), - ]), - ], true); -} - -/** - * @param string $name - * @param string $description - * @param int $shifttype_id - * @return string - */ -function ShiftType_edit_view($name, $description, $shifttype_id) -{ - return page_with_title($shifttype_id ? __('Edit shifttype') : __('Create shifttype'), [ - msg(), - buttons([ - button(page_link_to('shifttypes'), shifttypes_title(), 'back'), - ]), - form([ - form_text('name', __('Name'), $name), - form_textarea('description', __('Description'), $description), - form_info('', __('Please use markdown for the description.')), - form_submit('submit', __('Save')), - ]), - ], true); -} - -/** - * @param ShiftType $shifttype - * @return string - */ -function ShiftType_view(ShiftType $shifttype) -{ - $parsedown = new Parsedown(); - $title = $shifttype->name; - return page_with_title($title, [ - msg(), - buttons([ - button(page_link_to('shifttypes'), shifttypes_title(), 'back'), - button( - page_link_to('shifttypes', ['action' => 'edit', 'shifttype_id' => $shifttype->id]), - icon('pencil') . __('edit') - ), - button( - page_link_to('shifttypes', ['action' => 'delete', 'shifttype_id' => $shifttype->id]), - icon('trash') . __('delete'), - ), - ]), - heading(__('Description'), 2), - $parsedown->parse($shifttype->description), - ], true); -} - -/** - * @param ShiftType[]|array[]|Collection $shifttypes - * @return string - */ -function ShiftTypes_list_view($shifttypes) -{ - foreach ($shifttypes as $shifttype) { - $shifttype->name = '' - . $shifttype->name - . ''; - $shifttype->actions = table_buttons([ - button( - page_link_to( - 'shifttypes', - ['action' => 'edit', 'shifttype_id' => $shifttype->id] - ), - icon('pencil') . __('edit'), - 'btn-sm' - ), - button( - page_link_to('shifttypes', ['action' => 'delete', 'shifttype_id' => $shifttype->id]), - icon('trash') . __('delete'), - 'btn-sm' - ), - ]); - } - - return page_with_title(shifttypes_title(), [ - msg(), - buttons([ - button(page_link_to('shifttypes', ['action' => 'edit']), __('New shifttype'), 'add'), - ]), - table([ - 'name' => __('Name'), - 'actions' => '', - ], $shifttypes), - ], true); -} diff --git a/includes/view/ShiftsFilterRenderer.php b/includes/view/ShiftsFilterRenderer.php index 531580033..ac08917c4 100644 --- a/includes/view/ShiftsFilterRenderer.php +++ b/includes/view/ShiftsFilterRenderer.php @@ -49,7 +49,7 @@ public function render($page_link, $dashboardFilter = []) $toolbar = []; if ($this->daySelectionEnabled && !empty($this->days)) { $selected_day = date('Y-m-d', $this->shiftsFilter->getStartTime()); - $selected_day_formatted = date(__('Y-m-d'), $this->shiftsFilter->getStartTime()); + $selected_day_formatted = dateWithEventDay($selected_day); $day_dropdown_items = []; foreach ($this->days as $value => $day) { $link = $page_link . '&shifts_filter_day=' . $value; @@ -87,14 +87,4 @@ public function enableDaySelection($days) $this->daySelectionEnabled = true; $this->days = $days; } - - /** - * Should the filter display a day selection. - * - * @return bool - */ - public function isDaySelectionEnabled() - { - return $this->daySelectionEnabled; - } } diff --git a/includes/view/Shifts_view.php b/includes/view/Shifts_view.php index 4da951672..dc76ada7b 100644 --- a/includes/view/Shifts_view.php +++ b/includes/view/Shifts_view.php @@ -1,7 +1,7 @@ ' . __('Title') . '', + '

    ' . __('title.title') . '

    ', '

    ' . ($shift->url != '' - ? '' . $shift->title . '' - : $shift->title) + ? '' . htmlspecialchars($shift->title) . '' + : htmlspecialchars($shift->title)) . '

    ', ]), div('col-sm-3 col-xs-6', [ - '

    ' . __('Start') . '

    ', + '

    ' . __('shifts.start') . '

    ', '

    ', - icon('calendar-event') . $shift->start->format(__('Y-m-d')), + icon('calendar-event') . $shift->start->format(__('general.date')), '
    ', icon('clock') . $shift->start->format('H:i'), '

    ', ]), div('col-sm-3 col-xs-6', [ - '

    ' . __('End') . '

    ', + '

    ' . __('shifts.end') . '

    ', '

    ', - icon('calendar-event') . $shift->end->format(__('Y-m-d')), + icon('calendar-event') . $shift->end->format(__('general.date')), '
    ', icon('clock') . $shift->end->format('H:i'), '

    ', ]), div('col-sm-3 col-xs-6', [ '

    ' . __('Location') . '

    ', - '

    ' . Room_name_render($room) . '

    ', + '

    ' . location_name_render($location) . '

    ', ]), ]); } @@ -61,17 +61,23 @@ function Shift_editor_info_render(Shift $shift) if (!empty($shift->created_by)) { $info[] = sprintf( icon('plus-lg') . __('created at %s by %s'), - $shift->created_at->format(__('Y-m-d H:i')), + $shift->created_at->format(__('general.datetime')), User_Nick_render($shift->createdBy) ); } if (!empty($shift->updated_by)) { $info[] = sprintf( icon('pencil') . __('edited at %s by %s'), - $shift->updated_at->format(__('Y-m-d H:i')), + $shift->updated_at->format(__('general.datetime')), User_Nick_render($shift->updatedBy) ); } + if ($shift->transaction_id) { + $info[] = sprintf( + icon('clock-history') . __('History ID: %s'), + $shift->transaction_id + ); + } return join('
    ', $info); } @@ -88,20 +94,17 @@ function Shift_signup_button_render(Shift $shift, AngelType $angeltype) ->first(); if ( - isset($angeltype->shift_signup_state) - && ( - $angeltype->shift_signup_state->isSignupAllowed() - || auth()->user()->isAngelTypeSupporter($angeltype) - || auth()->can('admin_user_angeltypes') - ) + $angeltype->shift_signup_state?->isSignupAllowed() + || auth()->user()->isAngelTypeSupporter($angeltype) + || auth()->can('admin_user_angeltypes') ) { return button(shift_entry_create_link($shift, $angeltype), __('Sign up')); } elseif (empty($user_angeltype)) { return button( - page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), + url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), sprintf( __('Become %s'), - $angeltype->name + htmlspecialchars($angeltype->name) ) ); } @@ -112,16 +115,21 @@ function Shift_signup_button_render(Shift $shift, AngelType $angeltype) /** * @param Shift $shift * @param ShiftType $shifttype - * @param Room $room + * @param Location $location * @param AngelType[]|Collection $angeltypes_source * @param ShiftSignupState $shift_signup_state * @return string */ -function Shift_view(Shift $shift, ShiftType $shifttype, Room $room, $angeltypes_source, ShiftSignupState $shift_signup_state) -{ +function Shift_view( + Shift $shift, + ShiftType $shifttype, + Location $location, + $angeltypes_source, + ShiftSignupState $shift_signup_state +) { $shift_admin = auth()->can('admin_shifts'); $user_shift_admin = auth()->can('user_shifts_admin'); - $admin_rooms = auth()->can('admin_rooms'); + $admin_locations = auth()->can('admin_locations'); $admin_shifttypes = auth()->can('shifttypes'); $parsedown = new Parsedown(); @@ -164,32 +172,42 @@ function Shift_view(Shift $shift, ShiftType $shifttype, Room $room, $angeltypes_ if (config('signup_advance_hours') && $shift->start->timestamp > time() + config('signup_advance_hours') * 3600) { $content[] = info(sprintf( __('This shift is in the far future and becomes available for signup at %s.'), - date(__('Y-m-d H:i'), $shift->start->timestamp - config('signup_advance_hours') * 3600) + date(__('general.datetime'), $shift->start->timestamp - config('signup_advance_hours') * 3600) ), true); } $buttons = []; - if ($shift_admin || $admin_shifttypes || $admin_rooms) { + if ($shift_admin || $admin_shifttypes || $admin_locations) { $buttons = [ - $shift_admin ? button(shift_edit_link($shift), icon('pencil') . __('edit')) : '', - $shift_admin ? button(shift_delete_link($shift), icon('trash') . __('delete')) : '', - $admin_shifttypes ? button(shifttype_link($shifttype), $shifttype->name) : '', - $admin_rooms ? button(room_link($room), icon('pin-map-fill') . $room->name) : '', + $shift_admin ? button(shift_edit_link($shift), icon('pencil'), '', '', __('form.edit')) : '', + $shift_admin ? button(shift_delete_link($shift), icon('trash'), 'btn-danger', '', __('form.delete')) : '', + $admin_shifttypes + ? button(url('/admin/shifttypes/' . $shifttype->id), htmlspecialchars($shifttype->name)) + : '', + $admin_locations + ? button( + location_link($location), + icon('pin-map-fill') . htmlspecialchars($location->name) + ) + : '', ]; } - $buttons[] = button(user_link(auth()->user()->id), ' ' . __('My shifts')); + $buttons[] = button( + user_link(auth()->user()->id), + ' ' . __('profile.my-shifts') + ); $content[] = buttons($buttons); - $content[] = Shift_view_header($shift, $room); + $content[] = Shift_view_header($shift, $location); $content[] = div('row', [ div('col-sm-6', [ '

    ' . __('Needed angels') . '

    ', '
    ' . $needed_angels . '
    ', ]), div('col-sm-6', [ - '

    ' . __('Description') . '

    ', - $parsedown->parse($shifttype->description), - $parsedown->parse($shift->description), + '

    ' . __('general.description') . '

    ', + $parsedown->parse(htmlspecialchars($shifttype->description)), + $parsedown->parse(htmlspecialchars($shift->description)), ]), ]); @@ -197,10 +215,13 @@ function Shift_view(Shift $shift, ShiftType $shifttype, Room $room, $angeltypes_ $content[] = Shift_editor_info_render($shift); } - $start = $shift->start->format(__('Y-m-d H:i')); + $start = $shift->start->format(__('general.datetime')); + $link = button(url('/user-shifts'), icon('chevron-left'), 'btn-sm', '', __('general.back')); return page_with_title( - $shift->shiftType->name . ' %c', + $link . ' ' + . htmlspecialchars($shift->shiftType->name) + . ' %c', $content ); } @@ -273,14 +294,15 @@ function Shift_view_render_shift_entry(ShiftEntry $shift_entry, $user_shift_admi $entry .= '
    '; if ($user_shift_admin || $isUser) { $entry .= button_icon( - page_link_to('user_myshifts', ['edit' => $shift_entry->id, 'id' => $shift_entry->user_id]), + url('/user-myshifts', ['edit' => $shift_entry->id, 'id' => $shift_entry->user_id]), 'pencil', - 'btn-sm' + 'btn-sm', + __('form.edit') ); } $angeltype = $shift_entry->angelType; $disabled = Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id) ? '' : ' btn-disabled'; - $entry .= button_icon(shift_entry_delete_link($shift_entry), 'trash', 'btn-sm' . $disabled); + $entry .= button_icon(shift_entry_delete_link($shift_entry), 'trash', 'btn-sm btn-danger' . $disabled, __('form.delete')); $entry .= '
    '; } return $entry; diff --git a/includes/view/UserAngelTypes_view.php b/includes/view/UserAngelTypes_view.php index e50e1f045..aa9162567 100644 --- a/includes/view/UserAngelTypes_view.php +++ b/includes/view/UserAngelTypes_view.php @@ -25,12 +25,12 @@ function UserAngelType_update_view(UserAngelType $user_angeltype, User $user, An form([ buttons([ button( - page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), - icon('x-lg') . __('cancel') + url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), + icon('x-lg') . __('form.cancel') ), - form_submit('submit', icon('check-lg') . __('yes'), 'btn-primary', false), + form_submit('submit', icon('check-lg') . __('Yes'), 'btn-primary', false), ]), - ], page_link_to('user_angeltypes', [ + ], url('/user-angeltypes', [ 'action' => 'update', 'user_angeltype_id' => $user_angeltype->id, 'supporter' => ($supporter ? '1' : '0'), @@ -50,15 +50,15 @@ function UserAngelTypes_delete_all_view(AngelType $angeltype) form([ buttons([ button( - page_link_to( - 'angeltypes', + url( + '/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id] ), - icon('x-lg') . __('cancel') + icon('x-lg') . __('form.cancel') ), - form_submit('deny_all', icon('check-lg') . __('yes'), 'btn-primary', false), + form_submit('deny_all', icon('check-lg') . __('Yes'), 'btn-primary', false), ]), - ], page_link_to('user_angeltypes', ['action' => 'delete_all', 'angeltype_id' => $angeltype->id])), + ], url('/user-angeltypes', ['action' => 'delete_all', 'angeltype_id' => $angeltype->id])), ]); } @@ -73,10 +73,10 @@ function UserAngelTypes_confirm_all_view(AngelType $angeltype) info(sprintf(__('Do you really want to confirm all users for %s?'), $angeltype->name), true), form([ buttons([ - button(angeltype_link($angeltype->id), icon('x-lg') . __('cancel')), - form_submit('confirm_all', icon('check-lg') . __('yes'), 'btn-primary', false), + button(angeltype_link($angeltype->id), icon('x-lg') . __('form.cancel')), + form_submit('confirm_all', icon('check-lg') . __('Yes'), 'btn-primary', false), ]), - ], page_link_to('user_angeltypes', ['action' => 'confirm_all', 'angeltype_id' => $angeltype->id])), + ], url('/user-angeltypes', ['action' => 'confirm_all', 'angeltype_id' => $angeltype->id])), ]); } @@ -97,10 +97,10 @@ function UserAngelType_confirm_view(UserAngelType $user_angeltype, User $user, A ), true), form([ buttons([ - button(angeltype_link($angeltype->id), icon('x-lg') . __('cancel')), - form_submit('confirm_user', icon('check-lg') . __('yes'), 'btn-primary', false), + button(angeltype_link($angeltype->id), icon('x-lg') . __('form.cancel')), + form_submit('confirm_user', icon('check-lg') . __('Yes'), 'btn-primary', false), ]), - ], page_link_to('user_angeltypes', ['action' => 'confirm', 'user_angeltype_id' => $user_angeltype->id])), + ], url('/user-angeltypes', ['action' => 'confirm', 'user_angeltype_id' => $user_angeltype->id])), ]); } @@ -121,10 +121,10 @@ function UserAngelType_delete_view(UserAngelType $user_angeltype, User $user, An ), true), form([ buttons([ - button(angeltype_link($angeltype->id), icon('x-lg') . __('cancel')), - form_submit('delete', icon('check-lg') . __('yes'), 'btn-primary', false), + button(angeltype_link($angeltype->id), icon('x-lg') . __('form.cancel')), + form_submit('delete', icon('check-lg') . __('Yes'), 'btn-primary', false), ]), - ], page_link_to('user_angeltypes', ['action' => 'delete', 'user_angeltype_id' => $user_angeltype->id])), + ], url('/user-angeltypes', ['action' => 'delete', 'user_angeltype_id' => $user_angeltype->id])), ], true); } @@ -138,23 +138,22 @@ function UserAngelType_add_view(AngelType $angeltype, $users_source, $user_id) { $users = []; foreach ($users_source as $user_source) { - $users[$user_source->id] = User_Nick_render($user_source); + $users[$user_source->id] = $user_source->displayName; } - - return page_with_title(__('Add user to angeltype'), [ + $link = button( + url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), + icon('chevron-left'), + 'btn-sm', + '', + __('general.back') + ); + return page_with_title($link . ' ' . __('Add user to angeltype'), [ msg(), - buttons([ - button( - page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]), - __('back'), - 'back' - ), - ]), form([ - form_info(__('Angeltype'), $angeltype->name), + form_info(__('Angeltype'), htmlspecialchars($angeltype->name)), form_checkbox('auto_confirm_user', __('Confirm user'), true), - form_select('user_id', __('User'), $users, $user_id), - form_submit('submit', __('Add')), + form_select('user_id', __('general.user'), $users, $user_id), + form_submit('submit', icon('plus-lg') . __('Add')), ]), ]); } @@ -166,21 +165,22 @@ function UserAngelType_add_view(AngelType $angeltype, $users_source, $user_id) */ function UserAngelType_join_view($user, AngelType $angeltype) { - return page_with_title(sprintf(__('Become a %s'), $angeltype->name), [ + $isOther = $user->id != auth()->user()->id; + return page_with_title(sprintf(__('Become a %s'), htmlspecialchars($angeltype->name)), [ msg(), info(sprintf( - __('Do you really want to add %s to %s?'), + $isOther ? __('Do you really want to add %s to %s?') : __('Do you want to become a %2$s?'), $user->displayName, $angeltype->name ), true), form([ auth()->can('admin_user_angeltypes') ? form_checkbox('auto_confirm_user', __('Confirm user'), true) : '', buttons([ - button(angeltype_link($angeltype->id), icon('x-lg') . __('cancel')), - form_submit('submit', icon('check-lg') . __('save'), 'btn-primary', false), + button(angeltype_link($angeltype->id), icon('x-lg') . __('form.cancel')), + form_submit('submit', icon('save') . __('form.save'), 'btn-primary', false), ]), - ], page_link_to( - 'user_angeltypes', + ], url( + '/user-angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id, 'user_id' => $user->id] )), ]); diff --git a/includes/view/UserDriverLicenses_view.php b/includes/view/UserDriverLicenses_view.php deleted file mode 100644 index b3bb24f85..000000000 --- a/includes/view/UserDriverLicenses_view.php +++ /dev/null @@ -1,73 +0,0 @@ -id), __('Back to profile'), 'back'), - ]), - msg(), - form([ - form_info(__('Privacy'), __('Your driving license information is only visible for supporters and admins.')), - form_checkbox('wants_to_drive', __('I am willing to drive a car for the event'), $user_driver_license->wantsToDrive()), - div('m-3', [ - form_checkbox( - 'has_car', - __('I have my own car with me and am willing to use it for the event (You\'ll get reimbursed for fuel)'), - $user_driver_license->has_car - ), - heading(__('Driver license'), 3), - form_checkbox('has_license_car', __('Car'), $user_driver_license->drive_car), - form_checkbox( - 'has_license_3_5t_transporter', - __('Transporter 3,5t'), - $user_driver_license->drive_3_5t - ), - form_checkbox( - 'has_license_7_5t_truck', - __('Truck 7,5t'), - $user_driver_license->drive_7_5t - ), - form_checkbox( - 'has_license_12t_truck', - __('Truck 12t'), - $user_driver_license->drive_12t - ), - form_checkbox( - 'has_license_forklift', - __('Forklift'), - $user_driver_license->drive_forklift - ), - ], 'driving_license'), - form_submit('submit', __('Save')), - ]), - ' - - ', - ], true); -} diff --git a/includes/view/UserHintsRenderer.php b/includes/view/UserHintsRenderer.php index 55c655959..4a06686f6 100644 --- a/includes/view/UserHintsRenderer.php +++ b/includes/view/UserHintsRenderer.php @@ -20,9 +20,9 @@ public function addHint($hint, $important = false) if (!empty($hint)) { if ($important) { $this->important = true; - $this->hints[] = error($hint, true); + $this->hints[] = error($hint, true, true); } else { - $this->hints[] = info($hint, true); + $this->hints[] = info($hint, true, true); } } } diff --git a/includes/view/User_view.php b/includes/view/User_view.php index aa6e5b793..c64221879 100644 --- a/includes/view/User_view.php +++ b/includes/view/User_view.php @@ -19,18 +19,16 @@ */ function User_delete_view($user) { - return page_with_title(sprintf(__('Delete %s'), User_Nick_render($user)), [ + $link = button(user_edit_link($user->id), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title($link . ' ' . sprintf(__('Delete %s'), User_Nick_render($user)), [ msg(), - buttons([ - button(user_edit_link($user->id), icon('chevron-left') . __('back')), - ]), error( __('Do you really want to delete the user including all his shifts and every other piece of his data?'), true ), form([ form_password('password', __('Your password'), 'current-password'), - form_submit('submit', __('Delete')), + form_submit('submit', __('form.delete')), ]), ]); } @@ -43,25 +41,26 @@ function User_delete_view($user) */ function User_edit_vouchers_view($user) { - return page_with_title(sprintf(__('%s\'s vouchers'), User_Nick_render($user)), [ - msg(), - buttons([ - button(user_link($user->id), icon('chevron-left') . __('back')), - ]), - info(sprintf( - $user->state->force_active - ? __('Angel can receive another %d vouchers and is FA.') - : __('Angel can receive another %d vouchers.'), - User_get_eligable_voucher_count($user) - ), true), - form( - [ - form_spinner('vouchers', __('Number of vouchers given out'), $user->state->got_voucher), - form_submit('submit', __('Save')), - ], - page_link_to('users', ['action' => 'edit_vouchers', 'user_id' => $user->id]) - ), - ]); + $link = button(user_link($user->id), icon('chevron-left'), 'btn-sm', '', __('general.back')); + return page_with_title( + $link . ' ' . sprintf(__('%s\'s vouchers'), User_Nick_render($user)), + [ + msg(), + info(sprintf( + $user->state->force_active + ? __('Angel can receive another %d vouchers and is FA.') + : __('Angel can receive another %d vouchers.'), + User_get_eligable_voucher_count($user) + ), true), + form( + [ + form_spinner('vouchers', __('Number of vouchers given out'), $user->state->got_voucher), + form_submit('submit', icon('save') . __('form.save')), + ], + url('/users', ['action' => 'edit_vouchers', 'user_id' => $user->id]) + ), + ] + ); } /** @@ -91,10 +90,12 @@ function Users_view( $usersList = []; foreach ($users as $user) { $u = []; - $u['name'] = User_Nick_render($user) . User_Pronoun_render($user); - $u['first_name'] = $user->personalData->first_name; - $u['last_name'] = $user->personalData->last_name; - $u['dect'] = sprintf('%1$s', $user->contact->dect); + $u['name'] = User_Nick_render($user) + . User_Pronoun_render($user) + . user_info_icon($user); + $u['first_name'] = htmlspecialchars((string) $user->personalData->first_name); + $u['last_name'] = htmlspecialchars((string) $user->personalData->last_name); + $u['dect'] = sprintf('%1$s', htmlspecialchars((string) $user->contact->dect)); $u['arrived'] = icon_bool($user->state->arrived); if (config('enable_voucher')) { $u['got_voucher'] = $user->state->got_voucher; @@ -109,12 +110,21 @@ function Users_view( } } $u['arrival_date'] = $user->personalData->planned_arrival_date - ? $user->personalData->planned_arrival_date->format(__('Y-m-d')) : ''; + ? $user->personalData->planned_arrival_date->format(__('general.date')) : ''; $u['departure_date'] = $user->personalData->planned_departure_date - ? $user->personalData->planned_departure_date->format(__('Y-m-d')) : ''; - $u['last_login_at'] = $user->last_login_at ? $user->last_login_at->format(__('m/d/Y h:i a')) : ''; + ? $user->personalData->planned_departure_date->format(__('general.date')) : ''; + $u['last_login_at'] = $user->last_login_at ? $user->last_login_at->format(__('general.datetime')) : ''; $u['actions'] = table_buttons([ - button_icon(page_link_to('admin_user', ['id' => $user->id]), 'pencil', 'btn-sm'), + button( + url( + '/admin-user', + ['id' => $user->id] + ), + icon('pencil'), + 'btn-sm', + '', + __('form.edit') + ), ]); $usersList[] = $u; } @@ -132,21 +142,21 @@ function Users_view( $user_table_headers = []; if (!config('display_full_name')) { - $user_table_headers['name'] = Users_table_header_link('name', __('Nick'), $order_by); + $user_table_headers['name'] = Users_table_header_link('name', __('general.nick'), $order_by); } if (config('enable_user_name')) { - $user_table_headers['first_name'] = Users_table_header_link('first_name', __('Prename'), $order_by); - $user_table_headers['last_name'] = Users_table_header_link('last_name', __('Name'), $order_by); + $user_table_headers['first_name'] = Users_table_header_link('first_name', __('settings.profile.firstname'), $order_by); + $user_table_headers['last_name'] = Users_table_header_link('last_name', __('settings.profile.lastname'), $order_by); } if (config('enable_dect')) { - $user_table_headers['dect'] = Users_table_header_link('dect', __('DECT'), $order_by); + $user_table_headers['dect'] = Users_table_header_link('dect', __('general.dect'), $order_by); } $user_table_headers['arrived'] = Users_table_header_link('arrived', __('Arrived'), $order_by); if (config('enable_voucher')) { $user_table_headers['got_voucher'] = Users_table_header_link('got_voucher', __('Voucher'), $order_by); } $user_table_headers['freeloads'] = Users_table_header_link('freeloads', __('Freeloads'), $order_by); - $user_table_headers['active'] = Users_table_header_link('active', __('Active'), $order_by); + $user_table_headers['active'] = Users_table_header_link('active', __('user.active'), $order_by); $user_table_headers['force_active'] = Users_table_header_link('force_active', __('Forced'), $order_by); if ($goodie_enabled) { if ($goodie_tshirt) { @@ -173,11 +183,9 @@ function Users_view( unset($user_table_headers[$key]); } - return page_with_title(__('All users'), [ + $link = button(url('/register'), icon('plus-lg'), 'add'); + return page_with_title(__('All users') . ' ' . $link, [ msg(), - buttons([ - button(page_link_to('register'), icon('plus-lg') . __('New user')), - ]), table($user_table_headers, $usersList), ]); } @@ -191,7 +199,7 @@ function Users_view( function Users_table_header_link($column, $label, $order_by) { return '' . $label . ($order_by == $column ? ' ' : '') . ''; @@ -209,7 +217,7 @@ function User_shift_state_render($user) $upcoming_shifts = ShiftEntries_upcoming_for_user($user); if ($upcoming_shifts->isEmpty()) { - return '' . __('Free') . ''; + return '' . __('free') . ''; } /** @var ShiftEntry $nextShiftEntry */ @@ -217,8 +225,8 @@ function User_shift_state_render($user) $start = $nextShiftEntry->shift->start; $end = $nextShiftEntry->shift->end; - $startFormat = $start->format(__('Y-m-d H:i')); - $endFormat = $end->format(__('Y-m-d H:i')); + $startFormat = $start->format(__('general.datetime')); + $endFormat = $end->format(__('general.datetime')); $startTimestamp = $start->timestamp; $endTimestamp = $end->timestamp; @@ -260,7 +268,7 @@ function User_last_shift_render($user) $lastShiftEntry = $last_shifts->first(); $end = $lastShiftEntry->shift->end; - return '' + return '' . __('Shift ended %c') . ''; } @@ -272,8 +280,8 @@ function User_last_shift_render($user) function User_view_shiftentries($needed_angel_type) { $shift_info = '
    ' . $needed_angel_type['name'] . ': '; + . url('/angeltypes', ['action' => 'view', 'angeltype_id' => $needed_angel_type['id']]) + . '">' . htmlspecialchars($needed_angel_type['name']) . ': '; $shift_entries = []; foreach ($needed_angel_type['users'] as $user_shift) { @@ -299,9 +307,9 @@ function User_view_shiftentries($needed_angel_type) */ function User_view_myshift(Shift $shift, $user_source, $its_me) { - $shift_info = '' . $shift->shiftType->name . ''; + $shift_info = '' . htmlspecialchars($shift->shiftType->name) . ''; if ($shift->title) { - $shift_info .= '
    ' . $shift->title . ''; + $shift_info .= '
    ' . htmlspecialchars($shift->title) . ''; } foreach ($shift->needed_angeltypes as $needed_angel_type) { $shift_info .= User_view_shiftentries($needed_angel_type); @@ -309,18 +317,21 @@ function User_view_myshift(Shift $shift, $user_source, $its_me) $myshift = [ 'date' => icon('calendar-event') - . $shift->start->format(__('Y-m-d')) . '
    ' + . $shift->start->format(__('general.date')) . '
    ' . icon('clock-history') . $shift->start->format('H:i') . ' - ' . $shift->end->format(__('H:i')), 'duration' => sprintf('%.2f', ($shift->end->timestamp - $shift->start->timestamp) / 3600) . ' h', - 'room' => Room_name_render($shift->room), + 'location' => location_name_render($shift->location), 'shift_info' => $shift_info, 'comment' => '', + 'start' => $shift->start, + 'end' => $shift->end, + 'freeloaded' => $shift->freeloaded, ]; if ($its_me) { - $myshift['comment'] = $shift->user_comment; + $myshift['comment'] = htmlspecialchars($shift->user_comment); } if ($shift->freeloaded) { @@ -329,31 +340,37 @@ function User_view_myshift(Shift $shift, $user_source, $its_me) . '

    '; if (auth()->can('user_shifts_admin')) { $myshift['comment'] .= '
    ' - . '

    ' . __('Freeloaded') . ': ' . $shift->freeloaded_comment . '

    '; + . '

    ' + . __('Freeloaded') . ': ' . htmlspecialchars($shift->freeloaded_comment) + . '

    '; } else { $myshift['comment'] .= '

    ' . __('Freeloaded') . '

    '; } } $myshift['actions'] = [ - button(shift_link($shift), icon('eye') . __('view'), 'btn-sm'), + button(shift_link($shift), icon('eye'), 'btn-sm btn-info', '', __('View')), ]; if ($its_me || auth()->can('user_shifts_admin')) { $myshift['actions'][] = button( - page_link_to('user_myshifts', ['edit' => $shift->shift_entry_id, 'id' => $user_source->id]), - icon('pencil') . __('edit'), - 'btn-sm' + url('/user-myshifts', ['edit' => $shift->shift_entry_id, 'id' => $user_source->id]), + icon('pencil'), + 'btn-sm', + '', + __('form.edit') ); } if (Shift_signout_allowed($shift, (new AngelType())->forceFill(['id' => $shift->angel_type_id]), $user_source->id)) { $myshift['actions'][] = button( shift_entry_delete_link($shift), - icon('trash') . __('sign off'), - 'btn-sm' + icon('trash'), + 'btn-sm btn-danger', + '', + __('Sign off') ); } - $myshift['actions'] = table_buttons($myshift['actions']); + $myshift['actions'] = '
    ' . table_buttons($myshift['actions']) . '
    '; return $myshift; } @@ -388,35 +405,48 @@ function User_view_myshifts( foreach ($shifts as $shift) { $key = $shift->start->timestamp . '-shift-' . $shift->shift_entry_id . $shift->id; $myshifts_table[$key] = User_view_myshift($shift, $user_source, $its_me); - if (!$shift->freeloaded) { $timeSum += ($shift->end->timestamp - $shift->start->timestamp); } } - if ($its_me || $admin_user_worklog_privilege) { - foreach ($user_worklogs as $worklog) { - $key = $worklog->worked_at->timestamp . '-worklog-' . $worklog->id; - $myshifts_table[$key] = User_view_worklog($worklog, $admin_user_worklog_privilege); - $timeSum += $worklog->hours * 3600; - } + foreach ($user_worklogs as $worklog) { + $key = $worklog->worked_at->timestamp . '-worklog-' . $worklog->id; + $myshifts_table[$key] = User_view_worklog($worklog, $admin_user_worklog_privilege); + $timeSum += $worklog->hours * 3600; } if (count($myshifts_table) > 0) { ksort($myshifts_table); + $myshifts_table = array_values($myshifts_table); + foreach ($myshifts_table as $i => &$shift) { + $before = $myshifts_table[$i - 1] ?? null; + $after = $myshifts_table[$i + 1] ?? null; + if ($shift['freeloaded']) { + $shift['row-class'] = 'border border-danger border-2'; + } elseif (Carbon::now() > $shift['start'] && Carbon::now() < $shift['end']) { + $shift['row-class'] = 'border border-info border-2'; + } elseif ($after && Carbon::now() > $shift['end'] && Carbon::now() < $after['start']) { + $shift['row-class'] = 'border-bottom border-info'; + } elseif (!$before && Carbon::now() < $shift['start']) { + $shift['row-class'] = 'border-top-info'; + } elseif (!$after && Carbon::now() > $shift['end']) { + $shift['row-class'] = 'border-bottom border-info'; + } + } $myshifts_table[] = [ 'date' => '' . __('Sum:') . '', 'duration' => '' . sprintf('%.2f', round($timeSum / 3600, 2)) . ' h', - 'room' => '', + 'location' => '', 'shift_info' => '', 'comment' => '', 'actions' => '', ]; - if ($goodie_enabled && ($its_me || $tshirt_admin)) { + if ($goodie_enabled && ($its_me || $tshirt_admin || auth()->can('admin_user'))) { $myshifts_table[] = [ - 'date' => '' . ($goodie_tshirt ? __('Your t-shirt score') : __('Your goodie score')) . '™:', + 'date' => '' . ($goodie_tshirt ? __('T-shirt score') : __('Goodie score')) . '™:', 'duration' => '' . $tshirt_score . '', - 'room' => '', + 'location' => '', 'shift_info' => '', 'comment' => '', 'actions' => '', @@ -437,32 +467,39 @@ function User_view_worklog(Worklog $worklog, $admin_user_worklog_privilege) { $actions = ''; if ($admin_user_worklog_privilege) { - $actions = table_buttons([ + $actions = '
    ' . table_buttons([ button( url('/admin/user/' . $worklog->user->id . '/worklog/' . $worklog->id), - icon('pencil') . __('edit'), - 'btn-sm' + icon('pencil'), + 'btn-sm', + '', + __('form.edit') ), button( url('/admin/user/' . $worklog->user->id . '/worklog/' . $worklog->id . '/delete'), - icon('trash') . __('delete'), - 'btn-sm' + icon('trash'), + 'btn-sm btn-danger', + '', + __('form.delete') ), - ]); + ]) . '
    '; } return [ - 'date' => icon('calendar-event') . date(__('Y-m-d'), $worklog->worked_at->timestamp), + 'date' => icon('calendar-event') . date(__('general.date'), $worklog->worked_at->timestamp), 'duration' => sprintf('%.2f', $worklog->hours) . ' h', - 'room' => '', + 'location' => '', 'shift_info' => __('Work log entry'), - 'comment' => $worklog->comment . '
    ' + 'comment' => htmlspecialchars($worklog->comment) . '
    ' . sprintf( __('Added by %s at %s'), User_Nick_render($worklog->creator), - $worklog->created_at->format(__('Y-m-d H:i')) + $worklog->created_at->format(__('general.datetime')) ), 'actions' => $actions, + 'start' => $worklog->worked_at, + 'end' => $worklog->worked_at, + 'freeloaded' => false, ]; } @@ -501,11 +538,10 @@ function User_view( $goodie_tshirt = $goodie === GoodieType::Tshirt; $auth = auth(); $nightShiftsConfig = config('night_shifts'); - $user_name = htmlspecialchars( - $user_source->personalData->first_name - ) . ' ' . htmlspecialchars($user_source->personalData->last_name); + $user_name = htmlspecialchars((string) $user_source->personalData->first_name) . ' ' + . htmlspecialchars((string) $user_source->personalData->last_name); $myshifts_table = ''; - if ($its_me || $admin_user_privilege) { + if ($its_me || $admin_user_privilege || $tshirt_admin) { $my_shifts = User_view_myshifts( $shifts, $user_source, @@ -516,16 +552,19 @@ function User_view( $admin_user_worklog_privilege ); if (count($my_shifts) > 0) { - $myshifts_table = table([ - 'date' => __('Day & time'), + $myshifts_table = div('table-responsive', table([ + 'date' => __('Day & Time'), 'duration' => __('Duration'), - 'room' => __('Location'), - 'shift_info' => __('Name & workmates'), - 'comment' => __('Comment'), - 'actions' => __('Action'), - ], $my_shifts); + 'location' => __('Location'), + 'shift_info' => __('Name & Workmates'), + 'comment' => __('worklog.comment'), + 'actions' => __('general.actions'), + ], $my_shifts)); } elseif ($user_source->state->force_active) { - $myshifts_table = success(__('You have done enough.'), true); + $myshifts_table = success( + ($its_me ? __('You have done enough.') : (__('%s has done enough.', [$user_source->name]))), + true + ); } } @@ -534,6 +573,11 @@ function User_view( $needs_drivers_license = $needs_drivers_license || $angeltype->requires_driver_license; } + $needs_ifsg_certificate = false; + foreach ($user_angeltypes as $angeltype) { + $needs_ifsg_certificate = $needs_ifsg_certificate || $angeltype->requires_ifsg_certificate; + } + return page_with_title( ' ' . ( @@ -542,34 +586,31 @@ function User_view( : '' ) . htmlspecialchars($user_source->name) - . (config('enable_user_name') ? ' ' . $user_name . '' : ''), + . (config('enable_user_name') ? ' ' . $user_name . '' : '') + . user_info_icon($user_source), [ msg(), div('row', [ div('col-md-12', [ - buttons([ + table_buttons([ $auth->can('user.edit.shirt') && $goodie_enabled ? button( url('/admin/user/' . $user_source->id . '/goodie'), icon('person') . ($goodie_tshirt ? __('Shirt') : __('Goodie')) ) : '', $admin_user_privilege ? button( - page_link_to('admin_user', ['id' => $user_source->id]), - icon('pencil') . __('edit') - ) : '', - $admin_user_privilege || ($its_me && $needs_drivers_license) ? button( - user_driver_license_edit_link($user_source), - icon('person-vcard') . __('driving license') + url('/admin-user', ['id' => $user_source->id]), + icon('pencil') . __('form.edit'), ) : '', (($admin_user_privilege || $auth->can('admin_arrive')) && !$user_source->state->arrived) ? form([ form_hidden('action', 'arrived'), form_hidden('user', $user_source->id), - form_submit('submit', __('arrived'), '', false), - ], page_link_to('admin_arrive'), true) : '', + form_submit('submit', icon('house') . __('user.arrive'), '', false), + ], url('/admin-arrive'), 'float:left') : '', ($admin_user_privilege || $auth->can('voucher.edit')) && config('enable_voucher') ? button( - page_link_to( - 'users', + url( + '/users', ['action' => 'edit_vouchers', 'user_id' => $user_source->id] ), icon('valentine') . __('Vouchers') @@ -579,27 +620,29 @@ function User_view( url('/admin/user/' . $user_source->id . '/worklog'), icon('clock-history') . __('worklog.add') ) : '', - $its_me ? button( - page_link_to('settings/profile'), - icon('person-fill-gear') . __('Settings') - ) : '', - ($its_me && $auth->can('ical')) ? button( - page_link_to('ical', ['key' => $user_source->api_key]), + ], 'mb-2'), + $its_me ? table_buttons([ + button( + url('/settings/profile'), + icon('person-fill-gear') . __('settings.settings') + ), + $auth->can('ical') ? button( + url('/ical', ['key' => $user_source->api_key]), icon('calendar-week') . __('iCal Export') ) : '', - ($its_me && $auth->can('shifts_json_export')) ? button( - page_link_to('shifts_json_export', ['key' => $user_source->api_key]), + $auth->can('shifts_json_export') ? button( + url('/shifts-json-export', ['key' => $user_source->api_key]), icon('braces') . __('JSON Export') ) : '', - ($its_me && ( - $auth->can('shifts_json_export') - || $auth->can('ical') - || $auth->can('atom') - )) ? button( - page_link_to('user_myshifts', ['reset' => 1]), - icon('arrow-repeat') . __('Reset API key') - ) : '', - ]), + ( + $auth->can('shifts_json_export') + || $auth->can('ical') + || $auth->can('atom') + ) ? button( + url('/user-myshifts', ['reset' => 1]), + icon('arrow-repeat') . __('Reset API key') + ) : '', + ], 'mb-2') : '', ]), ]), div('row user-info', [ @@ -607,8 +650,8 @@ function User_view( config('enable_dect') && $user_source->contact->dect ? heading( icon('phone') - . ' ' - . $user_source->contact->dect + . ' ' + . htmlspecialchars($user_source->contact->dect) . '' ) : '', @@ -616,15 +659,15 @@ function User_view( $user_source->settings->mobile_show ? heading( icon('phone') - . ' ' - . $user_source->contact->mobile + . ' ' + . htmlspecialchars($user_source->contact->mobile) . '' ) : '' : '', $auth->can('user_messages') ? heading( - '' + '' . icon('envelope') . '' ) @@ -638,18 +681,20 @@ function User_view( ($its_me || $admin_user_privilege) ? '

    ' . __('Shifts') . '

    ' : '', $myshifts_table, ($its_me && $nightShiftsConfig['enabled'] && $goodie_enabled) ? info( - icon('info-circle') . sprintf( - __('Your night shifts between %d and %d am count twice.'), + sprintf( + icon('info-circle') . __('Your night shifts between %d and %d am count twice for the %s score.'), $nightShiftsConfig['start'], - $nightShiftsConfig['end'] + $nightShiftsConfig['end'], + ($goodie_tshirt ? __('T-shirt') : __('goodie')) ), + true, true ) : '', $its_me && count($shifts) == 0 ? error(sprintf( __('Go to the shifts table to sign yourself up for some shifts.'), - page_link_to('user_shifts') - ), true) + url('/user-shifts') + ), true, true) : '', $its_me ? ical_hint() : '', ] @@ -673,7 +718,7 @@ function User_view_state($admin_user_privilege, $freeloader, $user_source) } return div('col-md-2', [ - heading(__('User state'), 4), + heading(__('State'), 4), join('
    ', $state), ]); } @@ -691,7 +736,7 @@ function User_view_state_user($user_source) ]; if ($user_source->state->arrived) { - $state[] = '' . icon('house') . __('Arrived') . ''; + $state[] = '' . icon('house') . __('user.arrived') . ''; } else { $state[] = '' . __('Not arrived') . ''; } @@ -724,24 +769,24 @@ function User_view_state_admin($freeloader, $user_source) $state[] = '' . icon('house') . sprintf( __('Arrived at %s'), - $user_source->state->arrival_date ? $user_source->state->arrival_date->format(__('Y-m-d')) : '' + $user_source->state->arrival_date ? $user_source->state->arrival_date->format(__('general.date')) : '' ) . ''; if ($user_source->state->force_active) { - $state[] = '' . __('Active (forced)') . ''; + $state[] = '' . __('user.force_active') . ''; } elseif ($user_source->state->active) { - $state[] = '' . __('Active') . ''; + $state[] = '' . __('user.active') . ''; } if ($user_source->state->got_shirt && $goodie_enabled) { - $state[] = '' . ($goodie_tshirt ? __('T-Shirt') : __('Goodie')) . ''; + $state[] = '' . ($goodie_tshirt ? __('T-shirt') : __('Goodie')) . ''; } } else { $arrivalDate = $user_source->personalData->planned_arrival_date; $state[] = '' . ($arrivalDate ? sprintf( __('Not arrived (Planned: %s)'), - $arrivalDate->format(__('Y-m-d')) + $arrivalDate->format(__('general.date')) ) : __('Not arrived')) . ''; } @@ -779,11 +824,11 @@ function User_angeltypes_render($user_angeltypes) $class = 'text-warning'; } $output[] = '' - . ($angeltype->pivot->supporter ? icon('patch-check') : '') . $angeltype->name + . ($angeltype->pivot->supporter ? icon('patch-check') : '') . htmlspecialchars($angeltype->name) . ''; } return div('col-md-2', [ - heading(__('Angeltypes'), 4), + heading(__('angeltypes.angeltypes'), 4), join('
    ', $output), ]); } @@ -796,7 +841,7 @@ function User_groups_render($user_groups) { $output = []; foreach ($user_groups as $group) { - $output[] = __($group->name); + $output[] = __(htmlspecialchars($group->name)); } return div('col-md-2', [ @@ -816,9 +861,11 @@ function User_oauth_render(User $user) $output = []; foreach ($user->oauth as $oauth) { $output[] = __( - isset($config[$oauth->provider]['name']) - ? $config[$oauth->provider]['name'] - : Str::ucfirst($oauth->provider) + htmlspecialchars( + isset($config[$oauth->provider]['name']) + ? $config[$oauth->provider]['name'] + : Str::ucfirst($oauth->provider) + ) ); } @@ -879,9 +926,9 @@ function User_Pronoun_render(User $user): string */ function render_profile_link($text, $user_id = null, $class = '') { - $profile_link = page_link_to('settings/profile'); + $profile_link = url('/settings/profile'); if (!is_null($user_id)) { - $profile_link = page_link_to('users', ['action' => 'view', 'user_id' => $user_id]); + $profile_link = url('/users', ['action' => 'view', 'user_id' => $user_id]); } return sprintf( @@ -921,17 +968,22 @@ function render_user_freeloader_hint() } /** - * Hinweis fΓΌr Engel, die noch nicht angekommen sind + * hint for angels, which are not arrived yet * * @return string|null */ -function render_user_arrived_hint() +function render_user_arrived_hint(bool $is_sys_menu = false) { - if (!auth()->user()->state->arrived) { + $user = auth()->user(); + $user_arrival_date = $user->personalData->planned_arrival_date; + $is_before_arrival_date = $is_sys_menu && $user_arrival_date && Carbon::now() < $user_arrival_date; + if (config('signup_requires_arrival') && !$user->state->arrived && !$is_before_arrival_date) { /** @var Carbon $buildup */ $buildup = config('buildup_start'); if (!empty($buildup) && $buildup->lessThan(new Carbon())) { - return __('You are not marked as arrived. Please go to heaven\'s desk, get your angel badge and/or tell them that you arrived already.'); + return $user->state->user_info + ? ($is_sys_menu ? null : __('user_info.not_arrived_hint')) + : __('You are not marked as arrived. Please go to heaven, get your angel badge and/or tell them that you arrived already.'); } } @@ -945,9 +997,13 @@ function render_user_tshirt_hint() { $goodie = GoodieType::from(config('goodie_type')); $goodie_tshirt = $goodie === GoodieType::Tshirt; - if ($goodie_tshirt && !auth()->user()->personalData->shirt_size) { - $text = __('You need to specify a tshirt size in your settings!'); - return render_profile_link($text, null, 'text-danger'); + if ( + $goodie_tshirt + && config('required_user_fields')['tshirt_size'] + && !auth()->user()->personalData->shirt_size + ) { + $text = __('tshirt.required.hint'); + return render_profile_link($text); } return null; @@ -959,9 +1015,68 @@ function render_user_tshirt_hint() function render_user_dect_hint() { $user = auth()->user(); - if ($user->state->arrived && config('enable_dect') && !$user->contact->dect) { - $text = __('You need to specify a DECT phone number in your settings! If you don\'t have a DECT phone, just enter \'-\'.'); - return render_profile_link($text, null, 'text-danger'); + if ( + (config('required_user_fields')['dect'] || $user->state->arrived) + && config('enable_dect') && !$user->contact->dect + ) { + $text = __('dect.required.hint'); + return render_profile_link($text); + } + + return null; +} + +/** + * @return string|null + */ +function render_user_pronoun_hint() +{ + $user = auth()->user(); + if (config('required_user_fields')['pronoun'] && config('enable_pronoun') && !$user->personalData->pronoun) { + $text = __('pronoun.required.hint'); + return render_profile_link($text); + } + + return null; +} + +/** + * @return string|null + */ +function render_user_firstname_hint() +{ + $user = auth()->user(); + if (config('required_user_fields')['firstname'] && config('enable_user_name') && !$user->personalData->first_name) { + $text = __('firstname.required.hint'); + return render_profile_link($text); + } + + return null; +} + +/** + * @return string|null + */ +function render_user_lastname_hint() +{ + $user = auth()->user(); + if (config('required_user_fields')['lastname'] && config('enable_user_name') && !$user->personalData->last_name) { + $text = __('lastname.required.hint'); + return render_profile_link($text); + } + + return null; +} + +/** + * @return string|null + */ +function render_user_mobile_hint() +{ + $user = auth()->user(); + if (config('required_user_fields')['mobile'] && !$user->contact->mobile) { + $text = __('mobile.required.hint'); + return render_profile_link($text); } return null; diff --git a/package.json b/package.json index 93e4e4066..f77f7e324 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,11 @@ "babel-loader": "^9.1.2", "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^4.2.2", - "editorconfig-checker": "^5.0.1", - "eslint": "^8.33.0", - "eslint-plugin-editorconfig": "^4.0.2", + "editorconfig-checker": "^5.1.1", + "eslint": "^8.44.0", + "eslint-plugin-editorconfig": "^4.0.3", "mini-css-extract-plugin": "^2.7.2", - "postcss": "^8.4.21", + "postcss": "^8.4.31", "postcss-loader": "^7.0.2", "prettier": "^2.8.3", "resolve-url-loader": "^5.0.0", @@ -47,5 +47,8 @@ "webpack": "^5.76.0", "webpack-cli": "^5.0.1", "webpack-manifest-plugin": "^5.0.0" + }, + "resolutions": { + "semver": "7.5.3" } } diff --git a/resources/api/openapi.yml b/resources/api/openapi.yml new file mode 100644 index 000000000..5669114e1 --- /dev/null +++ b/resources/api/openapi.yml @@ -0,0 +1,700 @@ +openapi: 3.0.3 + +info: + version: 0.0.1-beta + title: Engelsystem + description: > + This API is as stable as a **beta** version might be. + It could burst into flames and morph into a monster at any second! + (But we try to keep it somewhat consistent, at least during events). + contact: + name: GitHub Issues + url: https://github.com/engelsystem/engelsystem/issues + license: + name: GPL 2.0 + # identifier: GPL-2.0 + +servers: + - url: /api/v0-beta + description: This server + - url: http://localhost:5080/api/v0-beta + description: Your local dev instance + +tags: + - name: api + description: API related + - name: angeltype + description: Angeltypes + - name: event + description: Event information + - name: location + description: Event locations + - name: news + description: News and meeting announcements + - name: shift + description: Event shifts + - name: user + description: User information + +security: + - bearer-auth: [ ] + - api-key-header: [ ] + +components: + securitySchemes: + bearer-auth: + type: http + scheme: bearer + bearerFormat: API key from settings + api-key-header: + type: apiKey + name: x-api-key + in: header + + responses: + UnauthorizedError: # 401 + description: Access token is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + ForbiddenError: # 403 + description: The client is not allowed to access + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFoundError: # 404 + description: This resource can not be found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotImplementedError: # 405 + description: This endpoint or method is not implemented + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + schemas: + AngelType: + type: object + properties: + id: + type: integer + example: 42 + name: + type: string + example: Angel + description: + type: string + example: Meta-Group of all registered Angels + url: + type: string + example: https://example.com/angeltype/42 + description: Link to the page of the given angeltype. + required: + - id + - name + - description + - url + UserAngelType: + allOf: + - $ref: '#/components/schemas/AngelType' + - type: object + properties: + confirmed: + type: boolean + example: true + description: > + If the user is confirmed + (either by the angeltype not requiring confirmation, being a supporter or being confirmed by one). + supporter: + type: boolean + example: false + description: If the user is a supporter of the angeltype. + required: + - confirmed + - supporter + News: + type: object + properties: + id: + type: integer + example: 42 + title: + type: string + example: First helper introduction + text: + type: string + example: | + The first introduction meeting takes place at the **Foo Hall** at 13:37. + Please bring your own seating as it might take some time. + description: | + The complete news text with markdown formatting. + Might include a `[more]` tag to be used as a separator which must be removed from the text when shown. + is_meeting: + type: boolean + example: true + description: Whether or not the news announces a meeting + is_pinned: + type: boolean + example: false + description: True if the news is pinned to the top + is_highlighted: + type: boolean + example: false + description: True if the news should be highlightet and shown on the dashboard + created_at: + $ref: '#/components/schemas/DateTime' + updated_at: + $ref: '#/components/schemas/DateTimeOptional' + url: + type: string + example: https://example.com/news/42 + description: Direct link to the news page + required: + - id + - title + - text + - is_meeting + - is_pinned + - is_highlighted + - created_at + - updated_at + - url + Location: + type: object + properties: + id: + type: integer + example: 42 + name: + type: string + example: Heaven + url: + type: string + example: https://example.com/location/42 + description: Link of the location page + required: + - id + - name + - url + Shift: + type: object + properties: + id: + type: integer + example: 42 + title: + type: string + example: Cleanup the venue + description: + type: string + example: You clean up the venue after the event. Its fun, we promise! + description: > + Shift description, should be added to the shift type description but might be empty. + Normally contains additional information for a specific shift / task. + starts_at: + $ref: '#/components/schemas/DateTime' + ends_at: + $ref: '#/components/schemas/DateTime' + location: + $ref: '#/components/schemas/Location' + shift_type: + $ref: '#/components/schemas/ShiftType' + created_at: + $ref: '#/components/schemas/DateTimeOptional' + updated_at: + $ref: '#/components/schemas/DateTimeOptional' + entries: + type: array + description: Can be empty (for example on Schedule import of unused room) + items: + $ref: '#/components/schemas/ShiftEntry' + url: + type: string + example: https://example.com/shifts/42 + description: Direct link to the shift + required: + - id + - title + - description + - starts_at + - ends_at + - location + - shift_type + - created_at + - updated_at + - entries + - url + ShiftEntry: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + type: + $ref: '#/components/schemas/AngelType' + needs: + type: integer + description: > + Number of users needed for the shift of the given type. + Will be more than users count when not full, less when overbooked + or 0 when only additional users with given type have been added. + example: 3 + required: + - users + - type + - needs + ShiftType: + type: object + properties: + id: + type: integer + example: 42 + name: + type: string + example: Build-Up + description: + type: string + example: This is a generic build-up shift, mostly involving heavy lifting. + required: + - id + - name + - description + User: + type: object + properties: + id: + type: integer + example: 42 + name: + type: string + example: HelpfulUser + first_name: + type: string + nullable: true + example: Helpful + last_name: + type: string + nullable: true + example: User + pronoun: + type: string + nullable: true + example: They/Them + description: Should be displayed where possible to allow users to be addressed properly + contact: + type: object + properties: + dect: + type: string + nullable: true + example: 4242 + description: The DECT number is a short internal number where users can be easily reached + mobile: + type: string + nullable: true + example: 1234567890 + url: + type: string + example: https://example.com/users/42 + description: Links to the users profile page + required: + - id + - name + - first_name + - last_name + - pronoun + - contact + - url + UserDetail: + allOf: + - $ref: '#/components/schemas/User' + - type: object + properties: + email: + type: string + example: user@example.com + tshirt: + type: string + nullable: true + example: XL + dates: + type: object + properties: + planned_arrival: + $ref: '#/components/schemas/DateTimeOptional' + planned_departure: + $ref: '#/components/schemas/DateTimeOptional' + arrival: + type: string + nullable: true + format: date-time + description: Actual arrival date, DateTime in ISO-8601 format + example: 2023-05-23T13:37:42.000000Z + required: + - planned_arrival + - planned_departure + - arrival + language: + type: string + example: en_US + arrived: + type: boolean + example: true + required: + - email + - tshirt + - dates + - language + - arrived + + DateTime: + type: string + format: date-time + description: DateTime in ISO-8601 format + example: 2023-05-23T00:00:00.000000Z + DateTimeOptional: + type: string + format: date-time + description: DateTime in ISO-8601 format + example: 2023-05-23T00:00:00.000000Z + nullable: true + Error: + type: object + properties: + message: + type: string + EventInfo: + type: object + properties: + api: + type: string + description: API version for easier version detection + example: 1.2.3 + spec: + type: string + description: Link to OpenAPI specification + example: https://galactic-help.example/api/v1.2.3/openapi + name: + type: string + example: 42. Galactic Conglomeration Congress + app_name: + type: string + example: Engelsystem + url: + type: string + description: URL to be used when linking to the application + example: https://galactic-help.example + timezone: + type: string + example: Europe/Berlin + description: Timezone of the event + buildup: + type: object + properties: + start: + $ref: '#/components/schemas/DateTimeOptional' + required: + - start + event: + type: object + properties: + start: + $ref: '#/components/schemas/DateTimeOptional' + end: + $ref: '#/components/schemas/DateTimeOptional' + required: + - start + - end + teardown: + type: object + properties: + end: + $ref: '#/components/schemas/DateTimeOptional' + required: + - end + required: + - api + - spec + - name + - app_name + - url + - timezone + - buildup + - event + - teardown + +paths: + /angeltypes: + get: + tags: + - angeltype + summary: Get a list of angeltypes + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AngelType' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + + /angeltypes/{id}/shifts: + parameters: + - name: id + in: path + required: true + description: The angeltype identifier + example: 42 + schema: + type: integer + get: + tags: + - angeltype + - shift + summary: Get all shifts of the requested angeltype + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Shift' + + /news: + get: + tags: + - news + summary: Get a list of all news + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/News' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + + /locations: + get: + tags: + - location + summary: Get a list of locations + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Location' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + + /locations/{id}/shifts: + parameters: + - name: id + in: path + required: true + description: The locations identifier + example: 42 + schema: + type: integer + get: + tags: + - location + - shift + summary: Get all shifts in the requested location + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Shift' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + /users/{id}: + parameters: + - name: id + in: path + required: true + description: The user identifier or `self` + example: 42 + schema: + oneOf: + - type: string + - type: integer + get: + tags: + - user + summary: Get the requesting users information + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + anyOf: + - $ref: '#/components/schemas/UserDetail' + - $ref: '#/components/schemas/User' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + /users/{id}/angeltypes: + parameters: + - name: id + in: path + required: true + description: The user identifier or `self` + example: 42 + schema: + oneOf: + - type: string + - type: integer + get: + tags: + - angeltype + - user + summary: Get the users angel types + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserAngelType' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + /users/{id}/shifts: + parameters: + - name: id + in: path + required: true + description: The user identifier or `self` + example: 42 + schema: + oneOf: + - type: string + - type: integer + get: + tags: + - shift + - user + summary: Get all shifts of the requested user + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Shift' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + /info: + get: + tags: + - event + summary: Get event information + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/EventInfo' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + /openapi: + get: + tags: + - api + summary: Get the OpenAPI definition + responses: + '200': + description: Ok + content: + application/json: {} + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' diff --git a/resources/api/readme.md b/resources/api/readme.md new file mode 100644 index 000000000..9c41daf1a --- /dev/null +++ b/resources/api/readme.md @@ -0,0 +1,7 @@ +# Engelsystem API documentation +Here you can find the OpenAPI files that describe the Engelsystem API. +Please be aware that the API is still in Beta and might change every second (but we try to keep it consistent during events ;)) + +## Links +* [Engelsystem OpenApi](openapi.yml) +* [OpenAPI format specification](https://swagger.io/specification/) diff --git a/resources/assets/js/countdown.js b/resources/assets/js/countdown.js index 48bfbbbaf..31fb14443 100644 --- a/resources/assets/js/countdown.js +++ b/resources/assets/js/countdown.js @@ -6,7 +6,7 @@ import { ready } from './ready'; ready(() => { const lang = document.documentElement.getAttribute('lang'); - const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' }); + const rtf = new Intl.RelativeTimeFormat(lang ?? [], { numeric: 'auto' }); const timeFrames = [ [60 * 60 * 24 * 365, 'year'], diff --git a/resources/assets/js/design.js b/resources/assets/js/design.js new file mode 100644 index 000000000..f6dc09a3c --- /dev/null +++ b/resources/assets/js/design.js @@ -0,0 +1,13 @@ +import { ready } from './ready'; + +ready(() => { + [...document.getElementsByClassName('prevent-default')].forEach((element) => { + let preventDefault = (e) => { + e.preventDefault(); + return false; + }; + + element.addEventListener('submit', preventDefault); + element.addEventListener('click', preventDefault); + }); +}); diff --git a/resources/assets/js/forms.js b/resources/assets/js/forms.js index e6e191963..ee12c5ee1 100644 --- a/resources/assets/js/forms.js +++ b/resources/assets/js/forms.js @@ -4,28 +4,24 @@ import { ready } from './ready'; /** * Sets all checkboxes to the wanted state - * - * @param {string} id Id of the element containing all the checkboxes - * @param {boolean} checked True if the checkboxes should be checked */ -global.checkAll = (id, checked) => { - document.querySelectorAll(`#${id} input[type="checkbox"]`).forEach((element) => { - element.checked = checked; - }); -}; - -/** - * Sets the checkboxes according to the given type - * - * @param {string} id The Id of the element containing all the checkboxes - * @param {int[]} shiftsList A list of numbers - */ -global.checkOwnTypes = (id, shiftsList) => { - document.querySelectorAll(`#${id} input[type="checkbox"]`).forEach((element) => { - const value = Number(element.value); - element.checked = shiftsList.includes(value); +ready(() => { + document.querySelectorAll('button.checkbox-selection').forEach((buttonElement) => { + buttonElement.addEventListener('click', () => { + document.querySelectorAll(`#${buttonElement.dataset.id} input[type="checkbox"]`).forEach((checkboxElement) => { + /** + * @type {boolean|int[]} + */ + const value = JSON.parse(buttonElement.dataset.value); + if (typeof value === 'boolean') { + checkboxElement.checked = value; + } else { + checkboxElement.checked = value.includes(Number(checkboxElement.value)); + } + }); + }); }); -}; +}); ready(() => { /** @@ -156,7 +152,7 @@ const DISABLE_ELEMENTS = [ ready(() => { // get all input-radio's and add for each an onChange event listener document.querySelectorAll('input[type="radio"]').forEach((radioElement) => { - // build selector and get all corrsponding elements for this input-radio + // build selector and get all corresponding elements for this input-radio const selector = DISABLE_ELEMENTS.map( (tagName) => `${tagName}[data-radio-name="${radioElement.name}"][data-radio-value]` ).join(','); @@ -180,21 +176,29 @@ ready(() => { }); ready(() => { - document.querySelectorAll('.spinner-down').forEach((element) => { - const inputElement = document.getElementById(element.dataset.inputId); - if (inputElement) { - element.addEventListener('click', () => { - inputElement.stepDown(); - }); - } + const addClickHandler = (selector, onClick) => { + document.querySelectorAll(selector).forEach((element) => { + const inputElement = document.getElementById(element.dataset.inputId); + + if (!inputElement || !inputElement.stepUp || !inputElement.stepDown) return; + + if (inputElement.disabled || inputElement.readOnly) { + // The input element is disabled or read-only β†’ disable the +/- button as well. + // Note that changing the "disabled" or "readonly" attributes during runtime is not yet supported. + element.setAttribute('disabled', 'disabled'); + return; + } + + element.addEventListener('click', () => onClick(inputElement)); + }); + }; + + addClickHandler('.spinner-up', (inputElement) => { + inputElement.stepUp(); }); - document.querySelectorAll('.spinner-up').forEach((element) => { - const inputElement = document.getElementById(element.dataset.inputId); - if (inputElement) { - element.addEventListener('click', () => { - inputElement.stepUp(); - }); - } + + addClickHandler('.spinner-down', (inputElement) => { + inputElement.stepDown(); }); }); @@ -227,10 +231,15 @@ ready(() => { }); }); +/** + * Init select dropdown choices + */ ready(() => { document.querySelectorAll('select').forEach((element) => { element.choices = new Choices(element, { - allowHTML: false, + allowHTML: true, + shouldSort: false, + shouldSortItems: false, classNames: { containerInner: 'choices__inner form-control', }, @@ -261,6 +270,80 @@ ready(() => { document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((element) => new bootstrap.Tooltip(element)); }); +/** + * Init Bootstrap Modals + */ +ready(() => { + document.querySelectorAll('.modal').forEach((element) => new bootstrap.Modal(element)); +}); + +/** + * Show confirmation modal before submitting form + * + * Uses the buttons data attributes to show in the modal: + * - data-confirm_title: Optional title of the modal + * - data-confirm_submit: Body of the modal + * + * The class, title and content of the requesting button gets copied for confirmation + * + */ +ready(() => { + document.querySelectorAll('[data-confirm_submit_title], [data-confirm_submit_text]').forEach((element) => { + let modalOpen = false; + let oldType = element.type; + if (element.type !== 'submit') { + return; + } + + element.type = 'button'; + element.addEventListener('click', (event) => { + if (modalOpen) { + return; + } + event.preventDefault(); + + document.getElementById('confirmation-modal')?.remove(); + document.body.insertAdjacentHTML( + 'beforeend', + ` + + ` + ); + + let modal = document.getElementById('confirmation-modal'); + modal.addEventListener('hide.bs.modal', () => { + modalOpen = false; + }); + modal.querySelector('[data-submit]').addEventListener('click', (event) => { + element.type = oldType; + element.click(); + }); + + modalOpen = true; + let bootstrapModal = new bootstrap.Modal(modal); + bootstrapModal.show(); + }); + }); +}); + /** * Show oauth buttons on welcome title click */ @@ -284,27 +367,48 @@ ready(() => { * Uses DOMContentLoaded to prevent flickering */ ready(() => { - const filter = document.getElementById('collapseShiftsFilterSelect'); - if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden.bs.collapse') { - return; - } + const collapseElement = document.getElementById('collapseShiftsFilterSelect'); + if (collapseElement) { + if (localStorage.getItem('collapseShiftsFilterSelect') === 'hidden.bs.collapse') { + collapseElement.classList.remove('show'); + } - filter.classList.remove('show'); + /** + * @param {Event} event + */ + const onChange = (event) => { + localStorage.setItem('collapseShiftsFilterSelect', event.type); + }; + + collapseElement.addEventListener('hidden.bs.collapse', onChange); + collapseElement.addEventListener('shown.bs.collapse', onChange); + } }); +/** + * Show/hide checkboxes for User Driver-Licenses + */ ready(() => { - if (typeof localStorage === 'undefined') { - return; - } + const checkboxElement = document.getElementById('wants_to_drive'); + const drivingLicenseElement = document.getElementById('driving_license'); - /** - * @param {Event} event - */ - const onChange = (event) => { - localStorage.setItem('collapseShiftsFilterSelect', event.type); - }; + if (checkboxElement && drivingLicenseElement) { + drivingLicenseElement.hidden = !checkboxElement.checked; - document.getElementById('collapseShiftsFilterSelect')?.addEventListener('hidden.bs.collapse', onChange); + checkboxElement.addEventListener('click', () => { + drivingLicenseElement.hidden = !checkboxElement.checked; + }); + } +}); - document.getElementById('collapseShiftsFilterSelect')?.addEventListener('shown.bs.collapse', onChange); +/** + * Prevent scrolling on # links in menu + */ +ready(() => { + const elements = document.querySelectorAll('.navbar a[href="#"]'); + elements.forEach((a) => { + a.addEventListener('click', (e) => { + e.preventDefault(); + }); + }); }); diff --git a/resources/assets/js/ready.js b/resources/assets/js/ready.js index d664b52cc..eabd8ffb2 100644 --- a/resources/assets/js/ready.js +++ b/resources/assets/js/ready.js @@ -2,9 +2,9 @@ * @param {Function} callback */ export const ready = (callback) => { - if (document.readyState !== 'loading') { - callback(); - } else { + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); + } else { + callback(); } }; diff --git a/resources/assets/js/vendor.js b/resources/assets/js/vendor.js index 38ae6b4e0..ef30ac178 100644 --- a/resources/assets/js/vendor.js +++ b/resources/assets/js/vendor.js @@ -3,3 +3,4 @@ window.bootstrap = require('bootstrap'); import './forms'; import './countdown'; import './dashboard'; +import './design'; diff --git a/resources/assets/themes/base.scss b/resources/assets/themes/base.scss index 6893e4257..5abf23ede 100644 --- a/resources/assets/themes/base.scss +++ b/resources/assets/themes/base.scss @@ -47,6 +47,7 @@ $form-label-font-weight: $font-weight-bold; @import '~bootstrap/scss/close'; @import '~bootstrap/scss/popover'; @import '~bootstrap/scss/tooltip'; +@import '~bootstrap/scss/modal'; @import '~bootstrap/scss/helpers'; @@ -120,6 +121,27 @@ table a > .icon-icon_angel { margin-bottom: 0; } +/* making top border visible in tables */ +table .border-top { + border-top-width: calc(var(--bs-border-width) * 2) !important; +} + +/* to only highlight the top row in info color */ +table .border-top-info { + border-top-width: calc(var(--bs-border-width) * 2) !important; + border-top-color: rgba(var(--bs-info-rgb)) !important; +} + +// equal top and bottom border if highlighted +table .border-bottom { + border-bottom-width: calc(var(--bs-border-width) * 2) !important; +} + +/* to be able to highlight table rows */ +.table-responsive { + padding: 0 2px; +} + .stats { font-size: 20px; height: 150px; @@ -189,7 +211,7 @@ table a > .icon-icon_angel { flex-direction: row; flex-wrap: nowrap; align-items: stretch; - width: 100%; + width: max-content; .lane { background: $table-striped-bg; @@ -321,6 +343,10 @@ h6, } // Cards +.card > .card-body:first-child { + border-top-left-radius: var(--bs-card-border-radius); + border-top-right-radius: var(--bs-card-border-radius); +} .card-body > *:last-child { margin-bottom: 0; } @@ -382,6 +408,10 @@ code { } } +.btn-group .btn { + border-radius: inherit; +} + /* Hide the arrow up/down buttons rendered by the browser in the input field */ /* Chrome, Safari, Edge, Opera */ input[type='number']::-webkit-outer-spin-button, diff --git a/resources/assets/themes/choices.scss b/resources/assets/themes/choices.scss index cd1bf2110..72f441f14 100644 --- a/resources/assets/themes/choices.scss +++ b/resources/assets/themes/choices.scss @@ -20,6 +20,10 @@ $es-choices-highlight-color: $choices-text-color !default; padding: $input-padding-y 1.75rem $input-padding-y $input-padding-x !important; } +.#{$choices-selector} .#{$choices-selector}__list { + z-index: $zindex-sticky + 2; +} + .#{$choices-selector}__list--single { padding: 0; } diff --git a/resources/assets/themes/cyborg_styles.scss b/resources/assets/themes/cyborg_styles.scss index a81b1d27f..ad56fdb9f 100644 --- a/resources/assets/themes/cyborg_styles.scss +++ b/resources/assets/themes/cyborg_styles.scss @@ -107,6 +107,10 @@ table, } } +.list-group .form-check-input { + border-color: $list-group-form-check-input-border-color; +} + // Navs ======================================================================= .nav-tabs, diff --git a/resources/assets/themes/cyborg_variables.scss b/resources/assets/themes/cyborg_variables.scss index f4d6c294e..0a18e1760 100644 --- a/resources/assets/themes/cyborg_variables.scss +++ b/resources/assets/themes/cyborg_variables.scss @@ -150,6 +150,9 @@ $input-border-focus: #66afe9 !default; //** Placeholder text color $input-color-placeholder: $gray-light !default; +$form-check-input-border: 1px solid $input-border-color !default; +$list-group-form-check-input-border-color: $input-border-color !default; + $legend-color: $text-color !default; $legend-border-color: $gray-dark !default; @@ -513,10 +516,11 @@ $progress-bar-info-bg: $info !default; // //## +$list-group-color: $input-color !default; //** Background color on `.list-group-item` $list-group-bg: $gray-darker !default; //** `.list-group-item` border color -$list-group-border: $gray-dark !default; +$list-group-border-color: $input-border-color !default; //** List group border radius $list-group-border-radius: $border-radius-base !default; @@ -561,9 +565,7 @@ $kbd-color: #fff !default; $kbd-bg: #333 !default; $pre-bg: #f5f5f5 !default; -$pre-color: $gray-dark !default; -$pre-border-color: #ccc !default; -$pre-scrollable-max-height: 340px !default; +$pre-color: $body-color !default; //== Type // @@ -589,4 +591,13 @@ $dl-horizontal-offset: $component-offset-horizontal !default; //** Horizontal line color. $hr-border: $gray-dark !default; +//== Modal +// +//## +$modal-header-border-color: $hr-border !default; +$modal-footer-border-color: $hr-border !default; + +//== Base +// +//## @import 'base.scss'; diff --git a/resources/assets/themes/dark.scss b/resources/assets/themes/dark.scss index cf2d624ac..de9ddfdd6 100644 --- a/resources/assets/themes/dark.scss +++ b/resources/assets/themes/dark.scss @@ -4,6 +4,7 @@ $input-disabled-bg: #111; $alert-bg-scale: 70%; $secondary: #222; $table-striped-bg: rgba(#fff, 0.05); +$list-group-form-check-input-border-color: #999; $es-choices-highlight-color: #000; @@ -13,3 +14,7 @@ $invert-color-value: 1 !default; input[type='time']::-webkit-calendar-picker-indicator { filter: invert($invert-color-value); } + +.modal .bg-dark .modal-header .btn-close { + background-color: $list-group-form-check-input-border-color; +} diff --git a/resources/assets/themes/theme11.scss b/resources/assets/themes/theme11.scss index c22d23a38..05bda48b4 100644 --- a/resources/assets/themes/theme11.scss +++ b/resources/assets/themes/theme11.scss @@ -81,6 +81,12 @@ $headings-small-color: $gray-lighter; color: lighten($brand-danger, 10%); } +// Alerts ================================================================= + +.alert a { + color: #fff; +} + // Forms ====================================================================== .legend { diff --git a/resources/assets/themes/theme16.scss b/resources/assets/themes/theme16.scss new file mode 100644 index 000000000..22cae7952 --- /dev/null +++ b/resources/assets/themes/theme16.scss @@ -0,0 +1,204 @@ +// cccamp23 + +// Variables +// -------------------------------------------------- + +@import 'dark'; + +//== changed Colors +$gray-dark: #231f20; +$gray-darker: darken($gray-dark, 30%); +$gray: lighten($gray-dark, 30%); +$gray-light: lighten($gray, 30%); +$gray-lighter: lighten($gray-light, 30%); +$dark: $gray-dark; + +$primary: #fb48c4; +$secondary: #231f20; +$success: #3fff21; +$info: #3dbddf; +$warning: #f6b345; +$danger: #991a24; + +$text-muted: $gray-light; + +$btn-link-disabled-color: $gray-light; + +//== Typography + +@font-face { + font-family: 'Questrial'; + src: url('theme16/Questrial-Regular.woff2') format('woff2'), url('theme16/Questrial-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Beon'; + src: url('theme16/Beon-Regular.woff2') format('woff2'), url('theme16/Beon-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +$font-family-sans-serif: 'Questrial', Helvetica Neue, Helvetica, Arial, sans-serif; +$font-family-serif: 'Beon', Georgia, 'Times New Roman', Times, serif; +$headings-font-family: $font-family-serif; + +//== changed Forms + +$input-bg: $gray-darker; +$input-bg-disabled: lighten($gray-lighter, 15%); +$input-border-color: lighten($gray-dark, 15%); +$input-group-addon-bg: $input-bg; + +$form-check-input-border: 1px solid $gray; + +//== changed Pagination + +$pagination-hover-color: $gray-lighter; +$pagination-active-color: $gray-lighter; + +//== changed Form states and alerts + +$state-success-text: #fff; +$state-success-bg: $success; +$state-success-border: darken($state-success-bg, 5%); + +$state-info-text: #fff; +$state-info-bg: $info; +$state-info-border: darken($state-info-bg, 7%); + +$state-warning-text: #fff; +$state-warning-bg: $warning; +$state-warning-border: darken($state-warning-bg, 3%); + +$state-danger-text: #fff; +$state-danger-bg: $danger; +$state-danger-border: darken($state-danger-bg, 3%); + +$headings-small-color: $gray-light; + +code { + background-color: $state-info-bg; + color: $state-info-text; +} + +$alert-bg-scale: 0%; +$alert-border-scale: 0%; +$alert-color-scale: 0%; + +// Navs ======================================================================= + +$nav-tabs-link-active-border-color: $gray-dark; + +$nav-tabs-link-active-color: $gray-darker; +$nav-pills-link-active-color: $gray-darker; + +//== Pagination +// +//## + +$pagination-color: $gray-lighter; +$pagination-border-color: $gray-dark; + +$pagination-hover-color: #000; +$pagination-hover-border-color: $gray-dark; + +$pagination-active-color: #000; +$pagination-active-border-color: $gray-dark; + +$pagination-disabled-color: $gray-light; +$pagination-disabled-border-color: $gray-dark; + +// dark +@import 'cyborg_variables'; +@import 'cyborg_styles'; + +// Specials =================================================================== + +.bg-success a, +.bg-primary a, +.bg-warning a, +.bg-info a, +.bg-light a { + color: $gray-darker !important; +} + +.bg-body a, +.bg-danger a, +.bg-secondary a, +.bg-dark a { + color: $state-danger-text !important; +} + +.bg-primary, +.bg-success, +.bg-warning, +.bg-info, +.bg-light { + color: $gray-darker; +} + +.bg-body, +.bg-danger, +.bg-secondary, +.bg-dark { + color: $state-danger-text; +} + +.navbar { + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(6px); +} + +.navbar-brand { + color: $primary; + font-family: $headings-font-family; +} + +.nav-tabs, +.nav-pills, +.pager { + a { + color: $gray-lighter; + } +} + +.alert a { + color: $gray-darker; +} + +.alert.alert-danger, +.alert.alert-danger a { + color: $gray-lighter; +} + +h1, +h3, +h5, +.h1, +.h3, +.h5 { + color: $primary; + + &.bg-warning, + &.bg-info { + color: $primary; + } +} + +h2, +h4, +h6, +.h2, +.h4, +.h6 { + color: $success; + + &.bg-warning, + &.bg-info { + color: $success; + } +} diff --git a/resources/assets/themes/theme16/Beon-Regular.woff b/resources/assets/themes/theme16/Beon-Regular.woff new file mode 100644 index 000000000..cf81ae876 Binary files /dev/null and b/resources/assets/themes/theme16/Beon-Regular.woff differ diff --git a/resources/assets/themes/theme16/Beon-Regular.woff2 b/resources/assets/themes/theme16/Beon-Regular.woff2 new file mode 100644 index 000000000..f3439a7e2 Binary files /dev/null and b/resources/assets/themes/theme16/Beon-Regular.woff2 differ diff --git a/resources/assets/themes/theme16/License-OFL.txt b/resources/assets/themes/theme16/License-OFL.txt new file mode 100644 index 000000000..79d0f2fe1 --- /dev/null +++ b/resources/assets/themes/theme16/License-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Questrial Project Authors (https://github.com/googlefonts/questrial) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/assets/themes/theme16/Questrial-Regular.woff b/resources/assets/themes/theme16/Questrial-Regular.woff new file mode 100644 index 000000000..687d232d6 Binary files /dev/null and b/resources/assets/themes/theme16/Questrial-Regular.woff differ diff --git a/resources/assets/themes/theme16/Questrial-Regular.woff2 b/resources/assets/themes/theme16/Questrial-Regular.woff2 new file mode 100644 index 000000000..8e580dec6 Binary files /dev/null and b/resources/assets/themes/theme16/Questrial-Regular.woff2 differ diff --git a/resources/assets/themes/theme17.scss b/resources/assets/themes/theme17.scss new file mode 100644 index 000000000..8e9b1b236 --- /dev/null +++ b/resources/assets/themes/theme17.scss @@ -0,0 +1,200 @@ +// cccamp23 + +// Variables +// -------------------------------------------------- + +@import 'dark'; + +//== changed Colors +$gray-dark: #231f20; +$gray-darker: darken($gray-dark, 30%); +$gray: lighten($gray-dark, 30%); +$gray-light: lighten($gray, 30%); +$gray-lighter: lighten($gray-light, 30%); +$dark: $gray-dark; + +$primary: #de37ff; +$secondary: #28ffff; +$success: #79ff5e; +$info: #3dbddf; +$warning: #f6b345; +$danger: #de4040; + +$text-muted: $gray-light; + +$btn-link-disabled-color: $gray-light; + +$dropdown-bg: #212529; +$dropdown-link-hover-color: #000000; + +//== changed Forms + +$input-bg: $gray-darker; +$input-bg-disabled: lighten($gray-lighter, 15%); +$input-border-color: $secondary; +$input-group-addon-bg: $input-bg; + +$form-check-input-border: 1px solid $gray; + +//== changed Pagination + +$pagination-hover-color: $gray-lighter; +$pagination-active-color: $gray-lighter; + +//== changed Form states and alerts + +$state-success-text: #fff; +$state-success-bg: $success; +$state-success-border: darken($state-success-bg, 5%); + +$state-info-text: #fff; +$state-info-bg: $info; +$state-info-border: darken($state-info-bg, 7%); + +$state-warning-text: #fff; +$state-warning-bg: $warning; +$state-warning-border: darken($state-warning-bg, 3%); + +$state-danger-text: #fff; +$state-danger-bg: $danger; +$state-danger-border: darken($state-danger-bg, 3%); + +$headings-small-color: $gray-light; + +code { + background-color: $state-info-bg; + color: $state-info-text; +} + +$alert-bg-scale: 0%; +$alert-border-scale: 0%; +$alert-color-scale: 0%; + +// Navs ======================================================================= + +$nav-tabs-link-active-border-color: $gray-dark; + +$nav-tabs-link-active-color: $gray-darker; +$nav-pills-link-active-color: $gray-darker; + +//== Pagination +// +//## + +$pagination-color: $gray-lighter; +$pagination-border-color: $gray-dark; + +$pagination-hover-color: #000; +$pagination-hover-border-color: $gray-dark; + +$pagination-active-color: #000; +$pagination-active-border-color: $gray-dark; + +$pagination-disabled-color: $gray-light; +$pagination-disabled-border-color: $gray-dark; + +// dark +@import 'cyborg_variables'; +@import 'cyborg_styles'; + +//== Typography + +@font-face { + font-family: 'VCR OCD Faux'; + src: url('theme17/VCROCDFaux.ttf') format('truetype'), url('theme17/VCROCDFaux.woff') format('woff'), + url('theme17/VCROCDFaux.woff2') format('woff2'); + font-weight: 400; +} + +@font-face { + font-family: Gabriella; + src: url('theme17/GabriellaHeavy.otf') format('opentype'); + font-weight: 400; +} + +$headings-font-family: Gabriella, $font-family-sans-serif; +$font-family-monospace: 'VCR OCD Faux', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + +h1, +.h1, +h2, +.h2, +h3, +.h3, +h4, +.h4, +h5, +.h5 { + color: $white; +} + +h1, +.h1 { + font-family: $headings-font-family; +} + +.btn-secondary { + background: transparent !important; + border-color: $secondary !important; + color: $secondary !important; +} + +// Specials =================================================================== + +.bg-success a, +.bg-primary a, +.bg-warning a, +.bg-info a, +.bg-light a { + color: $gray-darker !important; +} + +.bg-body a, +.bg-danger a, +.bg-secondary a, +.bg-dark a { + color: $state-danger-text !important; +} + +.bg-primary, +.bg-success, +.bg-warning, +.bg-info, +.bg-light { + color: $gray-darker; +} + +.bg-body, +.bg-danger, +.bg-secondary, +.bg-dark { + color: $state-danger-text; +} + +.navbar { + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(6px); +} + +.navbar-brand { + color: $primary; + font-family: $font-family-monospace; +} + +.nav-tabs, +.nav-pills, +.pager { + a { + color: $gray-lighter; + } +} + +.alert a { + color: $gray-darker; +} + +.alert.alert-danger, +.alert.alert-danger a { + color: $gray-lighter; +} diff --git a/resources/assets/themes/theme17/GabriellaHeavy.otf b/resources/assets/themes/theme17/GabriellaHeavy.otf new file mode 100644 index 000000000..f28c1b317 Binary files /dev/null and b/resources/assets/themes/theme17/GabriellaHeavy.otf differ diff --git a/resources/assets/themes/theme17/VCROCDFaux.ttf b/resources/assets/themes/theme17/VCROCDFaux.ttf new file mode 100644 index 000000000..086259295 Binary files /dev/null and b/resources/assets/themes/theme17/VCROCDFaux.ttf differ diff --git a/resources/assets/themes/theme17/VCROCDFaux.woff b/resources/assets/themes/theme17/VCROCDFaux.woff new file mode 100644 index 000000000..e9f148bbd Binary files /dev/null and b/resources/assets/themes/theme17/VCROCDFaux.woff differ diff --git a/resources/assets/themes/theme17/VCROCDFaux.woff2 b/resources/assets/themes/theme17/VCROCDFaux.woff2 new file mode 100644 index 000000000..5c823bdf2 Binary files /dev/null and b/resources/assets/themes/theme17/VCROCDFaux.woff2 differ diff --git a/resources/assets/themes/theme3.scss b/resources/assets/themes/theme3.scss index 32ba04ce8..59ff00a66 100644 --- a/resources/assets/themes/theme3.scss +++ b/resources/assets/themes/theme3.scss @@ -45,6 +45,13 @@ $navbar-default-brand-color: #fff; $navbar-default-brand-hover-color: $brand-primary; $navbar-default-brand-hover-bg: #000; +// Alerts ================================================================= + +.alert a { + color: #000; + text-decoration: underline; +} + .navbar { .bg-info { background-color: darken($brand-info, 30%); diff --git a/resources/assets/themes/theme4.scss b/resources/assets/themes/theme4.scss index 44802aec4..3ea7f587c 100644 --- a/resources/assets/themes/theme4.scss +++ b/resources/assets/themes/theme4.scss @@ -126,3 +126,14 @@ h6, color: #000; } } + +// Alerts ================================================================= + +.alert a { + color: #fff; +} + +.alert.alert-success a, +.alert.alert-warning a { + color: #000; +} diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index 0e8cd7cef..eda8a898e 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -17,19 +17,42 @@ msgstr "" msgid "validation.password.required" msgstr "Bitte gib ein Passwort an." +msgid "validation.password.length" +msgstr "Das angegebene Passwort ist zu kurz." + msgid "validation.login.required" msgstr "Bitte gib einen Loginnamen an." +msgid "validation.pronoun.required" +msgstr "Bitte gebe dein Pronomen an." + +msgid "validation.firstname.required" +msgstr "Bitte gebe deinen Vornamen an." + +msgid "validation.lastname.required" +msgstr "Bitte gebe deinen Nachnamen an." + +msgid "validation.mobile.required" +msgstr "Bitte gebe deine Handynummer an." + +msgid "validation.dect.required" +msgstr "Bitte gebe deine DECT-Nummer an." + +msgid "validation.username.required" +msgstr "Bitte gebe deinen Nick an." + +msgid "validation.username.username" +msgstr "" +"Bitte gebe einen gΓΌltigen Nick ein: " +"Verwende bis zu 24 Buchstaben, Zahlen oder verbindende Schriftzeichen (.-_) fΓΌr deinen Nick." + msgid "validation.email.required" msgstr "Bitte gib eine E-Mail-Adresse an." msgid "validation.email.email" msgstr "Die E-Mail-Adresse ist nicht gΓΌltig." -msgid "validation.password.min" -msgstr "Dein neues Passwort ist zu kurz." - -msgid "validation.new_password.min" +msgid "validation.new_password.length" msgstr "Dein neues Passwort ist zu kurz." msgid "validation.password.confirmed" @@ -38,6 +61,21 @@ msgstr "Deine PasswΓΆrter stimmen nicht ΓΌberein." msgid "validation.password_confirmation.required" msgstr "Du musst dein Passwort bestΓ€tigen." +msgid "validation.tshirt_size.required" +msgstr "Bitte wΓ€hle deine T-Shirt-Grâße aus." + +msgid "validation.tshirt_size.shirtSize" +msgstr "Bitte wΓ€hle eine gΓΌltige T-Shirt-Grâße aus." + +msgid "validation.planned_arrival_date.required" +msgstr "Bitte gebe dein geplantes Ankunftsdatum an." + +msgid "validation.planned_arrival_date.min" +msgstr "Das geplante Ankunftsdatum darf nicht vor Aufbaubeginn liegen." + +msgid "validation.planned_arrival_date.between" +msgstr "Das geplante Ankunftsdatum muss zwischen Aufbaubeginn und Abbauende liegen." + msgid "schedule.edit.success" msgstr "Das Programm wurde erfolgreich konfiguriert." @@ -53,6 +91,9 @@ msgstr "Der Schichttyp konnte nicht gefunden werden." msgid "schedule.import.success" msgstr "Das Programm wurde erfolgreich importiert." +msgid "schedule.delete.success" +msgstr "Das Programm wurde erfolgreich gelΓΆscht." + msgid "shifts.filter.toggle" msgstr "Filter verstecken/anzeigen" @@ -80,6 +121,9 @@ msgstr "Kommentar gespeichert." msgid "news.comment-delete.success" msgstr "Kommentar erfolgreich gelΓΆscht." +msgid "news.edit.duplicate" +msgstr "Diese News wurde bereits erstellt." + msgid "news.edit.success" msgstr "News erfolgreich aktualisiert." @@ -128,6 +172,9 @@ msgstr "Der OAuth-Provider ist aufgrund eines unerwarteten Fehlers nicht in der msgid "oauth.temporarily_unavailable" msgstr "Der OAuth-Provider ist aufgrund Überlastung oder Wartung temporΓ€r nicht in der Lage, die Anfrage zu erfΓΌllen" +msgid "profile.my-shifts" +msgstr "Meine Schichten" + msgid "settings.profile" msgstr "Profil" @@ -139,9 +186,15 @@ msgid "settings.profile.planned_departure_date.invalid" msgstr "Bitte gib Dein geplantes Abreisedatum an, damit wir ein GefΓΌhl fΓΌr die Abbauplanung bekommen. " "Es sollte nach dem Aufbaubeginn und vor dem Abbauende liegen." -msgid "settings.profile.success" +msgid "settings.success" msgstr "Einstellungen gespeichert." +msgid "settings.sessions.delete_success" +msgstr "Sitzung erfolgreich gelΓΆscht." + +msgid "settings.api.key_reset_success" +msgstr "API Key erfolgreich zurΓΌckgesetzt." + msgid "faq.delete.success" msgstr "FAQ Eintrag erfolgreich gelΓΆscht." @@ -166,6 +219,9 @@ msgstr "Es gibt eine neue News: %1$s" msgid "notification.news.new.text" msgstr "Du kannst sie dir unter %3$s anschauen." +msgid "notification.news.updated" +msgstr "Aktualisierte News: %s" + msgid "notification.messages.new" msgstr "Neue private Nachricht von %s" @@ -201,6 +257,12 @@ msgstr "" "Da die gelΓΆschte Schicht bereits vergangen ist, " "haben wir einen entsprechenden Arbeitseinsatz hinzugefΓΌgt." +msgid "notification.shift.updated" +msgstr "Deine Schicht wurde aktualisiert" + +msgid "notification.shift.no_next_found" +msgstr "Es wurde keine verfΓΌgbare Schicht gefunden." + msgid "user.edit.success" msgstr "Benutzer erfolgreich bearbeitet." @@ -216,11 +278,26 @@ msgstr "Arbeitseinsatz erfolgreich bearbeitet." msgid "worklog.delete.success" msgstr "Arbeitseinsatz erfolgreich gelΓΆscht." -msgid "room.edit.success" -msgstr "Raum erfolgreich bearbeitet." +msgid "location.edit.success" +msgstr "Ort erfolgreich bearbeitet." + +msgid "location.delete.success" +msgstr "Ort erfolgreich gelΓΆscht." + +msgid "shifttype.edit.success" +msgstr "Schichttyp erfolgreich bearbeitet." -msgid "room.delete.success" -msgstr "Raum erfolgreich gelΓΆscht." +msgid "shifttype.delete.success" +msgstr "Schichttyp erfolgreich gelΓΆscht." msgid "validation.name.exists" msgstr "Der Name wird bereits verwendet." + +msgid "registration.disabled" +msgstr "Die Registrierung ist deaktiviert." + +msgid "registration.successful" +msgstr "Registrierung erfolgreich. Du kannst dich jetzt anmelden!" + +msgid "shifts.history.delete.success" +msgstr "Schichten erfolgreich gelΓΆscht." diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index d5e96a228..26f0c534e 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -18,127 +18,86 @@ msgstr "" "X-Poedit-SearchPath-1: src\n" "X-Poedit-SearchPath-2: includes\n" -#: resources/views/emails/mail.twig:1 -#, python-format -msgid "Hi %s," +msgid "email.greeting" msgstr "Hallo %s," -#: resources/views/emails/mail.twig:3 -#, python-format -msgid "here is a message for you from the %s:" +msgid "email.introduction" msgstr "hier ist eine Nachricht aus dem %s fΓΌr Dich:" -#: resources/views/emails/mail.twig:6 -#, python-format -msgid "" -"This email is autogenerated and has not been signed. You got this email " -"because you are registered in the %s." +msgid "email.footer" msgstr "" "Diese E-Mail wurde automatisch generiert und muss daher nicht unterschrieben " "werden. Du hast diese E-Mail bekommen, weil Du im %s registriert bist." -#: resources/views/emails/password-reset.twig:3 -#, python-format -msgid "Please visit %s to recover your password." -msgstr "Bitte besuche %s, um Dein Passwort zurΓΌckzusetzen" +msgid "password.email.message" +msgstr "Um dein Passwort zurΓΌckzusetzen, besuche %s" + +msgid "page.error.title" +msgstr "Fehler %s" -#: resources/views/errors/403.twig:3 -msgid "Forbidden" +msgid "page.403.title" msgstr "Nicht erlaubt" -#: resources/views/errors/403.twig:5 -msgid "You are not allowed to access this page" +msgid "page.403.headline" msgstr "Du darfst diese Seite nicht aufrufen" -#: resources/views/errors/404.twig:3 src/Middleware/LegacyMiddleware.php:82 -msgid "Page not found" +msgid "page.404.title" msgstr "Seite nicht gefunden" -#: resources/views/errors/404.twig:10 -msgid "No sleep found" +msgid "page.404.not_found" msgstr "No sleep found" -#: resources/views/errors/405.twig:3 resources/views/errors/405.twig:5 -msgid "405: Method not allowed" -msgstr "" +msgid "page.405.title" +msgstr "405: Anfragetyp nicht erlaubt" -#: resources/views/errors/419.twig:3 -msgid "Authentication expired" +msgid "page.419.title" msgstr "Autorisierung ist abgelaufen" -#: resources/views/errors/419.twig:6 -msgid "The provided CSRF token is invalid or has expired" +msgid "page.419.text" msgstr "Das angegebene CSRF Token ist ungΓΌltig oder abgelaufen" -#: resources/views/layouts/parts/footer.twig:9 -#: resources/views/layouts/parts/footer.twig:10 -#: resources/views/layouts/parts/footer.twig:15 -#: resources/views/layouts/parts/footer.twig:22 -#: resources/views/layouts/parts/footer.twig:23 -#: resources/views/pages/login.twig:25 includes/pages/admin_arrive.php:98 -#: includes/pages/admin_arrive.php:99 includes/pages/admin_arrive.php:100 -#: includes/view/EventConfig_view.php:36 includes/view/EventConfig_view.php:44 -#: includes/view/EventConfig_view.php:52 includes/view/EventConfig_view.php:60 -#: includes/view/Shifts_view.php:28 includes/view/Shifts_view.php:36 -#: includes/view/Shifts_view.php:148 includes/view/User_view.php:197 -#: includes/view/User_view.php:199 -msgid "Y-m-d" +msgid "general.date" msgstr "d.m.Y" -#: resources/views/layouts/parts/footer.twig:7 -msgid "%1$s, from %2$s to %3$s" +msgid "footer.eventinfo.name_start_end" msgstr "%1$s, vom %2$s bis %3$s" -#: resources/views/layouts/parts/footer.twig:13 -msgid "%1$s, starting %2$s" +msgid "footer.eventinfo.name_start" msgstr "%1$s, ab dem %2$s" -#: resources/views/layouts/parts/footer.twig:21 -msgid "Event from %1$s to %2$s" -msgstr "Event von %1$s bis %2$s" +msgid "footer.eventinfo.start_end" +msgstr "Event vom %1$s bis %2$s" -#: resources/views/layouts/parts/footer.twig:34 -msgid "Bugs / Features" +msgid "footer.issues" msgstr "Bugs / Features" -#: resources/views/layouts/parts/footer.twig:35 -msgid "Development Platform" +msgid "footer.github" msgstr "Entwicklerplattform" -#: resources/views/layouts/parts/footer.twig:36 -#: resources/views/pages/credits.twig:3 resources/views/pages/credits.twig:7 -msgid "Credits" +msgid "credits.title" msgstr "Credits" -#: resources/views/layouts/parts/navbar.twig:39 -#: resources/views/pages/login.twig:83 resources/views/pages/login.twig:87 -#: includes/pages/guest_login.php:16 includes/pages/guest_login.php:353 -msgid "Register" +msgid "general.register" msgstr "Registrieren" -#: resources/views/layouts/parts/navbar.twig:43 -#: resources/views/pages/login.twig:4 resources/views/pages/login.twig:66 -#: includes/view/AngelTypes_view.php:581 -msgid "login.login" +msgid "general.login" msgstr "Login" msgid "page.403.login" msgstr "Bitte melde dich an." -#: resources/views/macros/form.twig:17 msgid "form.submit" msgstr "Absenden" -#: resources/views/pages/credits.twig:17 -msgid "Source code" +msgid "form.send_notification" +msgstr "Benachrichtigungen versenden" + +msgid "credits.source" msgstr "Quellcode" -#: resources/views/pages/credits.twig:18 -#, python-format -msgid "Version: _%s_" -msgstr "" +msgid "credits.version" +msgstr "Version: _%s_" -#: resources/views/pages/credits.twig:20 msgid "credits.credit" msgstr "" "Das ursprΓΌngliche Engelsystem wurde entwickelt von [cookie](https://github." @@ -146,292 +105,162 @@ msgstr "" "notrademark.de) (Maintainer) und [MyIgel](https://myigel.name) vΓΆllig " "ΓΌberarbeitet und verbessert." -#: resources/views/pages/credits.twig:23 msgid "credits.contributors" msgstr "" "Eine vollstΓ€ndige Liste der Mitwirkenden findest Du [auf GitHub](https://" "github.com/engelsystem/engelsystem/graphs/contributors)." -#: resources/views/pages/login.twig:10 includes/view/EventConfig_view.php:27 -#, python-format, php-format -msgid "Welcome to the %s!" +msgid "login.welcome" msgstr "Willkommen beim %s!" -#: resources/views/pages/login.twig:16 includes/view/EventConfig_view.php:34 -msgid "Buildup starts" +msgid "event.buildup.start" msgstr "Aufbau startet" -#: resources/views/pages/login.twig:17 includes/view/EventConfig_view.php:42 -msgid "Event starts" +msgid "event.starts" msgstr "Event startet" -#: resources/views/pages/login.twig:18 includes/view/EventConfig_view.php:50 -msgid "Event ends" +msgid "event.ends" msgstr "Event endet" -#: resources/views/pages/login.twig:19 includes/view/EventConfig_view.php:58 -msgid "Teardown ends" +msgid "event.teardown.ends" msgstr "Abbau endet" -#: resources/views/pages/login.twig:72 -msgid "I forgot my password" +msgid "login.password.reset" msgstr "Passwort vergessen" -#: resources/views/pages/login.twig:85 -msgid "Please sign up, if you want to help us!" +msgid "login.registration" msgstr "Bitte registriere Dich, wenn Du helfen mΓΆchtest!" -msgid "Registration is only available via external login." +msgid "login.registration.external" msgstr "Die Registrierung ist nur ΓΌber einen externen Login mΓΆglich." -#: resources/views/pages/login.twig:90 includes/pages/guest_login.php:61 -msgid "Registration is disabled." +msgid "login.registration.disabled" msgstr "Registrierung ist abgeschaltet." -#: resources/views/pages/login.twig:95 -msgid "What can I do?" +msgid "login.do" msgstr "Was kann ich machen?" -#: resources/views/pages/login.twig:96 -msgid "Please read about the jobs you can do to help us." -msgstr "" -"Bitte informiere Dich ΓΌber die TΓ€tigkeiten bei denen Du uns helfen kannst." +msgid "login.jobs" +msgstr "Informiere Dich ΓΌber die TΓ€tigkeiten bei denen Du uns helfen kannst." -#: resources/views/pages/login.twig:105 -msgid "Please note: You have to activate cookies!" +msgid "login.cookies" msgstr "Hinweis: Cookies mΓΌssen aktiviert sein!" -#: resources/views/pages/password/reset-form.twig:10 -#: includes/pages/guest_login.php:312 -msgid "Password" -msgstr "Passwort" - -#: resources/views/pages/password/reset-form.twig:11 -#: includes/pages/guest_login.php:315 -msgid "Confirm password" +msgid "password.reset.confirm" msgstr "Passwort wiederholen" -#: resources/views/pages/password/reset-form.twig:14 -#: includes/controller/shifts_controller.php:194 -#: includes/pages/admin_questions.php:55 includes/pages/admin_rooms.php:177 -#: includes/pages/admin_shifts.php:339 includes/pages/user_messages.php:78 -#: includes/view/AngelTypes_view.php:120 includes/view/EventConfig_view.php:110 -#: includes/view/Questions_view.php:43 includes/view/ShiftEntry_view.php:93 -#: includes/view/ShiftEntry_view.php:118 includes/view/ShiftEntry_view.php:141 -#: includes/view/ShiftEntry_view.php:207 includes/view/ShiftTypes_view.php:64 -#: includes/view/UserDriverLicenses_view.php:54 -#: includes/view/User_view.php:84 -#: includes/view/User_view.php:93 includes/view/User_view.php:98 -#: includes/view/User_view.php:103 includes/view/User_view.php:154 -msgid "Save" -msgstr "Speichern" - -#: resources/views/pages/password/reset-success.twig:7 -msgid "We sent you an email containing your password recovery link." +msgid "password.recovery.success" msgstr "" -"Wir haben dir eine eMail mit einem Link zum Passwort-zurΓΌcksetzen geschickt." +"Wir haben dir eine E-Mail mit einem Link zum Passwort-zurΓΌcksetzen geschickt." -#: resources/views/pages/password/reset-success.twig:9 -msgid "Password saved." -msgstr "Passwort gespeichert." - -#: resources/views/pages/password/reset.twig:5 -#: resources/views/pages/password/reset.twig:9 -msgid "Password recovery" +msgid "password.reset.title" msgstr "Passwort wiederherstellen" -#: resources/views/pages/password/reset.twig:21 -msgid "" -"We will send you an e-mail with a password recovery link. Please use the " -"email address you used for registration." +msgid "password.recovery.text" msgstr "" -"Wir werden eine eMail mit einem Link schicken, mit dem du das Passwort " -"zurΓΌcksetzen kannst. Bitte benutze die Mailadresse, die du bei der Anmeldung " +"Wir werden eine E-Mail mit einem Link schicken, mit dem du das Passwort " +"zurΓΌcksetzen kannst. Bitte benutze die E-Mail-Adresse, die du bei der Anmeldung " "verwendet hast." -#: resources/views/pages/password/reset.twig:22 -#: includes/pages/admin_free.php:116 includes/pages/guest_login.php:280 -#: includes/view/AngelTypes_view.php:119 includes/view/AngelTypes_view.php:482 -#: includes/view/User_view.php:62 -msgid "E-Mail" -msgstr "E-Mail" +msgid "password.minimal_length" +msgstr "MindestlΓ€nge %d Zeichen" -#: resources/views/pages/password/reset.twig:25 -msgid "Recover" +msgid "form.recover" msgstr "Wiederherstellen" -#: includes/controller/angeltypes_controller.php:13 -#: includes/pages/user_shifts.php:249 includes/sys_menu.php:97 -#: includes/view/AngelTypes_view.php:81 includes/view/AngelTypes_view.php:139 -#: includes/view/User_view.php:779 -msgid "Angeltypes" -msgstr "Engeltypen" - -#: includes/controller/angeltypes_controller.php:89 -#, php-format msgid "Angeltype %s deleted." msgstr "Engeltyp %s gelΓΆscht." -#: includes/controller/angeltypes_controller.php:94 -#: includes/view/AngelTypes_view.php:59 -#, php-format msgid "Delete angeltype %s" msgstr "LΓΆsche Engeltyp %s" -#: includes/controller/angeltypes_controller.php:135 msgid "Please check the name. Maybe it already exists." msgstr "Bitte ΓΌberprΓΌfe den Namen. Vielleicht ist er bereits vergeben." -#: includes/controller/angeltypes_controller.php:165 -#: includes/view/AngelTypes_view.php:79 -#, php-format +msgid "Create angeltype" +msgstr "Engeltyp erstellen" + msgid "Edit %s" msgstr "%s bearbeiten" -#: includes/controller/angeltypes_controller.php:203 -#: includes/view/AngelTypes_view.php:346 -#, php-format msgid "Team %s" msgstr "Team %s" -#: includes/controller/angeltypes_controller.php:287 -#: includes/view/User_view.php:397 -msgid "view" -msgstr "ansehen" - -#: includes/controller/angeltypes_controller.php:295 -#: includes/pages/admin_free.php:97 includes/pages/admin_groups.php:44 -#: includes/pages/admin_rooms.php:27 includes/view/AngelTypes_view.php:175 -#: includes/view/ShiftTypes_view.php:91 includes/view/ShiftTypes_view.php:123 -#: includes/view/Shifts_view.php:155 includes/view/User_view.php:402 -#: includes/view/User_view.php:496 includes/view/User_view.php:591 -msgid "edit" -msgstr "bearbeiten" - -#: includes/controller/angeltypes_controller.php:300 -#: includes/controller/shifts_controller.php:245 -#: includes/pages/admin_questions.php:58 includes/pages/admin_questions.php:74 -#: includes/pages/admin_rooms.php:32 includes/pages/admin_user.php:181 -#: includes/view/AngelTypes_view.php:64 includes/view/AngelTypes_view.php:182 -#: includes/view/Questions_view.php:13 includes/view/Questions_view.php:22 -#: includes/view/ShiftEntry_view.php:28 includes/view/ShiftEntry_view.php:57 -#: includes/view/ShiftTypes_view.php:28 includes/view/ShiftTypes_view.php:96 -#: includes/view/ShiftTypes_view.php:128 includes/view/Shifts_view.php:156 -#: includes/view/User_view.php:501 -msgid "delete" -msgstr "lΓΆschen" - -#: includes/controller/angeltypes_controller.php:311 -#: includes/view/AngelTypes_view.php:168 includes/view/AngelTypes_view.php:537 -msgid "leave" -msgstr "verlassen" - -# As already mentioned in Issue #312 : I'd suggest "join angel" and the German translation -# "Engel hinzufΓΌgen" for this button. -#: includes/controller/angeltypes_controller.php:317 -#: includes/view/AngelTypes_view.php:152 includes/view/AngelTypes_view.php:542 -msgid "join" -msgstr "mitmachen" - -#: includes/controller/angeltypes_controller.php:354 -msgid "Angeltype doesn't exist . " -msgstr "Engeltyp existiert nicht." - -#: includes/controller/event_config_controller.php:11 includes/sys_menu.php:122 +msgid "%s (not \"%s\")" +msgstr "%s (kein \"%s\")" + +msgid "View" +msgstr "Ansehen" + +msgid "Leave" +msgstr "Verlassen" + +msgid "Join" +msgstr "Mitmachen" + msgid "Event config" msgstr "Event Einstellungen" -#: includes/controller/event_config_controller.php:53 msgid "Please enter buildup start date." msgstr "Bitte gib das Aufbau Start Datum an." -#: includes/controller/event_config_controller.php:57 msgid "Please enter event start date." msgstr "Bitte gib das Event Start Datum an." -#: includes/controller/event_config_controller.php:61 msgid "Please enter event end date." msgstr "Bitte gib das Event Ende Datum an." -#: includes/controller/event_config_controller.php:65 msgid "Please enter teardown end date." msgstr "Bitte gib das Abbau Ende Datum an." -#: includes/controller/event_config_controller.php:71 msgid "The buildup start date has to be before the event start date." msgstr "Das Aufbau Start Datum muss vor dem Event Start Datum liegen." -#: includes/controller/event_config_controller.php:76 msgid "The event start date has to be before the event end date." msgstr "Das Event Start Datum muss vor dem Event End Datum liegen." -#: includes/controller/event_config_controller.php:81 msgid "The event end date has to be before the teardown end date." msgstr "Das Event Ende Datum muss vor dem Abbau Ende Datum liegen." -#: includes/controller/event_config_controller.php:86 msgid "The buildup start date has to be before the teardown end date." msgstr "Das Aufbau Start Datum muss vor dem Abbau Ende Datum liegen." -#: includes/controller/event_config_controller.php:120 -msgid "Settings saved." -msgstr "Einstellungen gespeichert." - -#: includes/controller/public_dashboard_controller.php:27 -#: includes/pages/user_shifts.php:277 includes/view/PublicDashboard_view.php:55 msgid "Public Dashboard" msgstr "Γ–ffentliches Dashboard" -#: includes/controller/shift_entries_controller.php:109 -#: includes/controller/shift_entries_controller.php:181 -#, php-format +msgid "shifts.random" +msgstr "ZufΓ€llige Schicht" + msgid "%s has been subscribed to the shift." msgstr "%s wurde in die Schicht eingetragen." -#: includes/controller/shift_entries_controller.php:149 msgid "User is not in angeltype." msgstr "User ist nicht im Engeltyp." -#: includes/controller/shift_entries_controller.php:166 -#: includes/controller/shift_entries_controller.php:210 msgid "This shift is already occupied." msgstr "Die Schicht ist schon voll." -#: includes/controller/shift_entries_controller.php:206 msgid "You need be accepted member of the angeltype." msgstr "Du musst bestΓ€tigtes Mitglied des Engeltyps sein." -#: includes/controller/shift_entries_controller.php:208 -#: includes/view/Shifts_view.php:138 msgid "This shift collides with one of your shifts." msgstr "Diese Schicht kollidiert mit deinen Schichten." -#: includes/controller/shift_entries_controller.php:212 msgid "This shift ended already." msgstr "Die Schicht ist schon vorbei." -#: includes/controller/shift_entries_controller.php:214 msgid "You are not marked as arrived." msgstr "Du bist nicht als angekommen markiert." -#: includes/controller/shift_entries_controller.php:216 msgid "You are not allowed to sign up yet." msgstr "Du darfst dich noch nicht anmelden." -#: includes/controller/shift_entries_controller.php:218 -#: includes/view/Shifts_view.php:142 msgid "You are signed up for this shift." msgstr "Du bist fΓΌr diese Schicht eingetragen." -#: includes/controller/shift_entries_controller.php:267 msgid "You are subscribed. Thank you!" msgstr "Du bist eingetragen. Danke!" -#: includes/controller/shift_entries_controller.php:326 -msgid "Shift entry not found." -msgstr "Schichteintrag nicht gefunden." - -#: includes/controller/shift_entries_controller.php:348 msgid "" "You are not allowed to remove this shift entry. If necessary, ask your " "supporter or heaven to do so." @@ -439,442 +268,191 @@ msgstr "" "Du darfst diesen Schichteintrag nicht entfernen. Falls notwendig, frage " "deinen Supporter oder im Himmel danach." -#: includes/controller/shift_entries_controller.php:354 msgid "Shift entry removed." msgstr "Schichteintrag gelΓΆscht." -#: includes/controller/shifts_controller.php:65 msgid "This shift was imported from a schedule so some changes will be overwritten with the next import." msgstr "" "Diese Schicht wurde aus einem Fahrplan importiert. " "Dadurch werden einige Γ„nderungen beim nΓ€chsten Import ΓΌberschrieben." -#: includes/controller/shifts_controller.php:96 -msgid "Please select a room." -msgstr "Bitte einen Raum auswΓ€hlen." +msgid "Please select a location." +msgstr "Bitte einen Ort auswΓ€hlen." -#: includes/controller/shifts_controller.php:103 msgid "Please select a shifttype." msgstr "Bitte einen Schichttyp wΓ€hlen." -#: includes/controller/shifts_controller.php:110 msgid "Please enter a valid starting time for the shifts." msgstr "Bitte gib eine korrekte Startzeit fΓΌr die Schichten ein." -#: includes/controller/shifts_controller.php:117 msgid "Please enter a valid ending time for the shifts." msgstr "Bitte gib eine korrekte Endzeit fΓΌr die Schichten ein." -#: includes/controller/shifts_controller.php:122 msgid "The ending time has to be after the starting time." msgstr "Die Endzeit muss nach der Startzeit liegen." -#: includes/controller/shifts_controller.php:135 -#, php-format msgid "Please check your input for needed angels of type %s." msgstr "Bitte prΓΌfe deine Eingabe fΓΌr benΓΆtigte Engel des Typs %s." -#: includes/controller/shifts_controller.php:165 msgid "Shift updated." msgstr "Schicht aktualisiert." -#: includes/controller/shifts_controller.php:184 msgid "This page is much more comfortable with javascript." msgstr "Diese Seite ist mit JavaScript viel komfortabler." -#: includes/controller/shifts_controller.php:187 -#: includes/pages/admin_import.php:123 includes/pages/admin_shifts.php:415 msgid "Shifttype" msgstr "Schichttyp" -#: includes/controller/shifts_controller.php:188 -#: includes/pages/admin_import.php:197 includes/pages/admin_import.php:206 -#: includes/pages/admin_import.php:215 includes/pages/admin_shifts.php:416 -#: includes/view/Shifts_view.php:18 -msgid "Title" +msgid "title.title" msgstr "Titel" -#: includes/controller/shifts_controller.php:189 -msgid "Room:" -msgstr "Raum:" +msgid "Location:" +msgstr "Ort:" -#: includes/controller/shifts_controller.php:190 msgid "Start:" msgstr "Start:" -#: includes/controller/shifts_controller.php:191 msgid "End:" msgstr "Ende:" -#: includes/controller/shifts_controller.php:192 -#: includes/pages/admin_shifts.php:337 includes/pages/admin_shifts.php:452 -#: includes/view/Shifts_view.php:167 msgid "Needed angels" msgstr "BenΓΆtigte Engel" -#: includes/controller/shifts_controller.php:232 msgid "Shift deleted." msgstr "Schicht gelΓΆscht." -msgid "Shifts history" -msgstr "Schichten Historie" - -msgid "%s shifts deleted." -msgstr "%s Schichten gelΓΆscht." - -msgid "Created" -msgstr "Erstellt" - -msgid "delete all" -msgstr "alle lΓΆschen" - -#: includes/controller/shifts_controller.php:238 -#, php-format msgid "Do you want to delete the shift %s from %s to %s?" msgstr "MΓΆchtest Du die Schicht %s von %s bis %s lΓΆschen?" -#: includes/controller/shifts_controller.php:268 msgid "Shift could not be found." msgstr "Schicht konnte nicht gefunden werden." -#: includes/controller/shifttypes_controller.php:33 -#, php-format -msgid "Shifttype %s deleted." -msgstr "Schichttyp %s gelΓΆscht." - -#: includes/controller/shifttypes_controller.php:38 -#: includes/view/ShiftTypes_view.php:21 -#, php-format -msgid "Delete shifttype %s" -msgstr "LΓΆsche Schichttyp %s" - -#: includes/controller/shifttypes_controller.php:61 -msgid "Shifttype not found." -msgstr "Schichttyp nicht gefunden." - -#: includes/controller/shifttypes_controller.php:77 -#: includes/pages/admin_rooms.php:88 -msgid "Please enter a name." -msgstr "Gib bitte einen Namen an." - -#: includes/controller/shifttypes_controller.php:95 -msgid "Updated shifttype." -msgstr "Schichttyp geΓ€ndert." - -#: includes/controller/shifttypes_controller.php:100 -msgid "Created shifttype." -msgstr "Schichttyp erstellt." - -#: includes/controller/shifttypes_controller.php:159 includes/sys_menu.php:116 -msgid "Shifttypes" -msgstr "Schichttypen" - -#: includes/controller/user_angeltypes_controller.php:28 -#, php-format msgid "There is %d unconfirmed angeltype." msgid_plural "There are %d unconfirmed angeltypes." msgstr[0] "Es gibt %d nicht freigeschalteten Engeltypen!" msgstr[1] "Es gibt %d nicht freigeschaltete Engeltypen!" -#: includes/controller/user_angeltypes_controller.php:33 msgid "Angel types which need approvals:" msgstr "Engeltypen die bestΓ€tigt werden mΓΌssen:" -#: includes/controller/user_angeltypes_controller.php:47 -#: includes/controller/user_angeltypes_controller.php:53 -#: includes/controller/user_angeltypes_controller.php:87 -#: includes/controller/user_angeltypes_controller.php:93 -#: includes/controller/user_angeltypes_controller.php:139 -#: includes/controller/user_angeltypes_controller.php:199 -#: includes/controller/user_angeltypes_controller.php:264 -msgid "Angeltype doesn't exist." -msgstr "Engeltyp existiert nicht." - -#: includes/controller/user_angeltypes_controller.php:58 msgid "You are not allowed to delete all users for this angeltype." msgstr "Du darfst nicht alle Benutzer von diesem Engeltyp entfernen." -#: includes/controller/user_angeltypes_controller.php:66 -#, php-format msgid "Denied all users for angeltype %s." msgstr "Alle Benutzer mit Engeltyp %s abgelehnt." -#: includes/controller/user_angeltypes_controller.php:71 -#: includes/view/UserAngelTypes_view.php:45 msgid "Deny all users" msgstr "Alle Benutzer ablehnen" -#: includes/controller/user_angeltypes_controller.php:98 msgid "You are not allowed to confirm all users for this angeltype." msgstr "Du darfst nicht alle Benutzer fΓΌr diesen Engeltyp freischalten." -#: includes/controller/user_angeltypes_controller.php:106 -#, php-format msgid "Confirmed all users for angeltype %s." msgstr "Alle Benutzer fΓΌr Engeltyp %s freigeschaltet." -#: includes/controller/user_angeltypes_controller.php:111 -#: includes/view/UserAngelTypes_view.php:69 msgid "Confirm all users" msgstr "Alle Benutzer bestΓ€tigen" -#: includes/controller/user_angeltypes_controller.php:127 -#: includes/controller/user_angeltypes_controller.php:133 -#: includes/controller/user_angeltypes_controller.php:187 -#: includes/controller/user_angeltypes_controller.php:193 -#: includes/controller/user_angeltypes_controller.php:245 -#: includes/controller/user_angeltypes_controller.php:258 -msgid "User angeltype doesn't exist." -msgstr "Benutzer-Engeltype existiert nicht." - -#: includes/controller/user_angeltypes_controller.php:144 msgid "You are not allowed to confirm this users angeltype." msgstr "Du darfst diesen Benutzer nicht fΓΌr diesen Engeltyp freischalten." -#: includes/controller/user_angeltypes_controller.php:150 -#: includes/controller/user_angeltypes_controller.php:205 -#: includes/controller/user_angeltypes_controller.php:270 -#: includes/controller/users_controller.php:327 -msgid "User doesn't exist." -msgstr "Benutzer existiert nicht." - -#: includes/controller/user_angeltypes_controller.php:163 -#, php-format msgid "%s confirmed for angeltype %s." msgstr "%s fΓΌr Engeltyp %s freigeschaltet." -#: includes/controller/user_angeltypes_controller.php:171 -#: includes/view/UserAngelTypes_view.php:89 msgid "Confirm angeltype for user" msgstr "Engeltyp fΓΌr Benutzer bestΓ€tigen" -#: includes/controller/user_angeltypes_controller.php:210 msgid "You are not allowed to delete this users angeltype." msgstr "Du darfst diesen Benutzer nicht von diesem Engeltyp entfernen." -#: includes/controller/user_angeltypes_controller.php:218 -#, php-format msgid "User %s removed from %s." msgstr "Benutzer %s von %s entfernt." -#: includes/controller/user_angeltypes_controller.php:224 -#: includes/view/UserAngelTypes_view.php:113 msgid "Remove angeltype" msgstr "Engeltyp lΓΆschen" -#: includes/controller/user_angeltypes_controller.php:240 msgid "You are not allowed to set supporter rights." msgstr "Du darfst keine Supporterrechte bearbeiten." -#: includes/controller/user_angeltypes_controller.php:252 msgid "No supporter update given." msgstr "Kein Update fΓΌr Supporterrechte gegeben." -#: includes/controller/user_angeltypes_controller.php:278 -#, php-format msgid "Added supporter rights for %s to %s." msgstr "%s hat %s als Supporter bekommen." -#: includes/controller/user_angeltypes_controller.php:279 -#, php-format msgid "Removed supporter rights for %s from %s." msgstr "%s hat jetzt nicht mehr %s als Supporter." -#: includes/controller/user_angeltypes_controller.php:295 -#: includes/view/AngelTypes_view.php:263 -#: includes/view/UserAngelTypes_view.php:14 msgid "Add supporter rights" msgstr "Supporterrechte geben" -#: includes/controller/user_angeltypes_controller.php:295 -#: includes/view/AngelTypes_view.php:246 -#: includes/view/UserAngelTypes_view.php:14 msgid "Remove supporter rights" msgstr "Supporterrechte entfernen" -#: includes/controller/user_angeltypes_controller.php:334 -#, php-format msgid "User %s added to %s." msgstr "Benutzer %s zu %s hinzugefΓΌgt." -#: includes/controller/user_angeltypes_controller.php:351 -#: includes/view/UserAngelTypes_view.php:142 msgid "Add user to angeltype" msgstr "Benutzer zu Engeltyp hinzufΓΌgen" -#: includes/controller/user_angeltypes_controller.php:368 -#, php-format msgid "You are already a %s." msgstr "Du bist bereits %s." -#: includes/controller/user_angeltypes_controller.php:375 -#, php-format msgid "You joined %s." msgstr "Du bist %s beigetreten." -#: includes/controller/user_angeltypes_controller.php:396 -#: includes/view/UserAngelTypes_view.php:166 -#, php-format msgid "Become a %s" msgstr "Werde ein %s" -#: includes/controller/user_driver_licenses_controller.php:26 -#, php-format -msgid "" -"You joined an angeltype which requires a driving license. Please edit your " -"driving license information here: %s." -msgstr "" -"Du bist einem Engeltypen beigetreten, der FΓΌhrerschein-Infos benΓΆtigt. Bitte " -"trage Deine FΓΌhrerschein-Infos hier ein: %s." - -#: includes/controller/user_driver_licenses_controller.php:27 -msgid "driving license information" -msgstr "FΓΌhrerschein-Infos" - -#: includes/controller/user_driver_licenses_controller.php:133 -msgid "Your driver license information has been saved." -msgstr "Deine FΓΌhrerschein-Infos wurden gespeichert." - -#: includes/controller/user_driver_licenses_controller.php:136 -msgid "Please select at least one driving license." -msgstr "Bitte wΓ€hle mindestens einen FΓΌhrerschein-Typen aus." - -#: includes/controller/user_driver_licenses_controller.php:141 -msgid "Your driver license information has been removed." -msgstr "Deine FΓΌhrerschein-Infos wurden gelΓΆscht." - -#: includes/controller/user_driver_licenses_controller.php:147 -#: includes/view/UserDriverLicenses_view.php:15 -#, php-format -msgid "Edit %s driving license information" -msgstr "Bearbeite die FΓΌhrerschein-Infos von %s" - -#: includes/controller/users_controller.php:64 msgid "You cannot delete yourself." msgstr "Du kannst Dich nicht selber lΓΆschen." -#: includes/controller/users_controller.php:76 msgid "auth.password.error" msgstr "Dein Passwort stimmt nicht. Bitte probiere es nochmal." -#: includes/controller/users_controller.php:85 msgid "User deleted." msgstr "Engel gelΓΆscht." -#: includes/controller/users_controller.php:93 includes/view/User_view.php:118 -#, php-format msgid "Delete %s" msgstr "%s lΓΆschen" -#: includes/controller/users_controller.php:163 msgid "Please enter a valid number of vouchers." msgstr "Bitte gib eine korrekte Anzahl von Gutscheinen ein." -#: includes/controller/users_controller.php:170 msgid "Saved the number of vouchers." msgstr "Anzahl der Gutscheine gespeichert." -#: includes/controller/users_controller.php:179 includes/view/User_view.php:142 -#, php-format -msgid "%s's vouchers" -msgstr "Gutschein von %s" - -#: includes/controller/users_controller.php:196 msgid "User not found." msgstr "Benutzer nicht gefunden." -#: includes/controller/users_controller.php:231 msgid "Enough" msgstr "Genug" -#: includes/controller/users_controller.php:299 includes/view/User_view.php:247 msgid "All users" msgstr "Alle Benutzer" -#: includes/helper/email_helper.php:50 -#, php-format -msgid "User %s could not be notified by email due to an error." +msgid "User %s could not be notified by e-mail due to an error." msgstr "" "Aufgrund eines Fehlers konnte dem User %s keine E-Mail gesendet werden." msgid "%s (%s as %s) in %s, %s - %s" msgstr "%s (%s als %s) in %s, %s - %s" -#: includes/mailer/shifts_mailer.php:17 -msgid "A Shift you are registered on has changed:" -msgstr "Eine deiner Schichten hat sich geΓ€ndert:" - -#: includes/mailer/shifts_mailer.php:21 -#, php-format -msgid "* Shift type changed from %s to %s" -msgstr "* Schichttyp von %s in %s geΓ€ndert" - -#: includes/mailer/shifts_mailer.php:26 -#, php-format -msgid "* Shift title changed from %s to %s" -msgstr "* Schicht Titel von %s nach %s geΓ€ndert" - -#: includes/mailer/shifts_mailer.php:32 -#, php-format -msgid "* Shift Start changed from %s to %s" -msgstr "* Schicht Beginn von %s nach %s geΓ€ndert" - -#: includes/mailer/shifts_mailer.php:41 -#, php-format -msgid "* Shift End changed from %s to %s" -msgstr "* Schicht Ende von %s nach %s geΓ€ndert" - -#: includes/mailer/shifts_mailer.php:49 -#, php-format -msgid "* Shift Location changed from %s to %s" -msgstr "* Schicht Ort von %s to %s geΓ€ndert" - -#: includes/mailer/shifts_mailer.php:59 -msgid "The updated Shift:" -msgstr "Die aktualisierte Schicht:" - -#: includes/mailer/shifts_mailer.php:71 -msgid "Your Shift has changed" -msgstr "Deine Schicht hat sich geΓ€ndert" - -#: includes/mailer/shifts_mailer.php:87 -msgid "A Shift you are registered on was deleted:" -msgstr "Eine deiner Schichten wurde gelΓΆscht:" - -#: includes/mailer/shifts_mailer.php:99 -msgid "" -"Since the deleted shift was already done, we added a worklog entry instead, " -"to keep your work hours correct." -msgstr "" -"Da die gelΓΆschte Schicht bereits vergangen ist, haben wir einen " -"entsprechenden Arbeitseinsatz hinzugefΓΌgt." - -#: includes/mailer/shifts_mailer.php:102 -msgid "Your Shift was deleted" -msgstr "Deine Schicht wurde gelΓΆscht" - -#: includes/mailer/shifts_mailer.php:119 msgid "You have been assigned to a Shift:" msgstr "Du wurdest in eine Schicht eingetragen:" -#: includes/mailer/shifts_mailer.php:125 msgid "Assigned to Shift" msgstr "In Schicht eingetragen" -#: includes/mailer/shifts_mailer.php:140 msgid "You have been removed from a Shift:" msgstr "Du wurdest aus einer Schicht ausgetragen:" -#: includes/mailer/shifts_mailer.php:146 msgid "Removed from Shift" msgstr "Von Schicht ausgetragen" -#: includes/mailer/users_mailer.php:13 -msgid "Your account has been deleted" +msgid "Your account has been deleted." msgstr "Dein Konto wurde gelΓΆscht." -#: includes/mailer/users_mailer.php:15 -#, php-format msgid "" "Your %s account has been deleted. If you have any questions regarding your " "account deletion, please contact heaven." @@ -882,846 +460,289 @@ msgstr "" "Dein %s-Konto wurde gelΓΆscht. Wenn Du dazu Fragen hast, kontaktiere bitte " "den Himmel." -#: includes/model/UserWorkLog_model.php:167 -#: includes/model/UserWorkLog_model.php:168 includes/view/User_view.php:200 -msgid "m/d/Y h:i a" -msgstr "d.m.Y H:i" - -#: includes/pages/admin_active.php:13 includes/sys_menu.php:112 msgid "Active angels" msgstr "Aktive Engel" -#: includes/pages/admin_active.php:42 -#, php-format msgid "" "At least %s angels are forced to be active. The number has to be greater." msgstr "" "Mindestens %s Engel werden als aktiv gekennzeichnet. Die Nummer muss grâßer " "sein." -#: includes/pages/admin_active.php:48 msgid "Please enter a number of angels to be marked as active." msgstr "" "Bitte gib eine Anzahl an Engeln ein, die als Aktiv markiert werden sollen." -#: includes/pages/admin_active.php:95 msgid "Marked angels." msgstr "Engel wurden markiert." -#: includes/pages/admin_active.php:98 includes/pages/admin_rooms.php:155 -#: includes/pages/admin_rooms.php:198 includes/pages/admin_shifts.php:333 -#: includes/view/UserAngelTypes_view.php:147 -#: includes/view/User_view.php:121 includes/view/User_view.php:145 -msgid "back" -msgstr "zurΓΌck" +msgid "general.back" +msgstr "ZurΓΌck" -#: includes/pages/admin_active.php:99 -msgid "apply" -msgstr "anwenden" +msgid "Apply" +msgstr "Anwenden" -#: includes/pages/admin_active.php:112 msgid "Angel has been marked as active." msgstr "Engel wurde als aktiv markiert." -#: includes/pages/admin_active.php:114 includes/pages/admin_active.php:125 -#: includes/pages/admin_active.php:147 includes/pages/admin_arrive.php:44 -#: includes/pages/admin_arrive.php:62 msgid "Angel not found." msgstr "Engel nicht gefunden." -#: includes/pages/admin_active.php:123 msgid "Angel has been marked as not active." msgstr "Engel wurde als nicht aktiv markiert." -#: includes/pages/admin_active.php:134 -msgid "Angel has got a t-shirt." +msgid "Angel has got a T-shirt." msgstr "Engel hat ein T-Shirt bekommen." -#: includes/pages/admin_active.php:145 -msgid "Angel has got no t-shirt." +msgid "Angel has got no T-shirt." msgstr "Engel hat kein T-Shirt bekommen." -#: includes/pages/admin_active.php:235 msgid "set active" msgstr "setze aktiv" -#: includes/pages/admin_active.php:248 -msgid "remove active" +msgid "Remove active" msgstr "entferne aktiv" -#: includes/pages/admin_active.php:261 -msgid "got t-shirt" +msgid "Got T-shirt" msgstr "T-Shirt bekommen" -#: includes/pages/admin_active.php:274 -msgid "remove t-shirt" +msgid "Remove T-shirt" msgstr "entferne T-Shirt" -#: includes/pages/admin_active.php:299 includes/pages/admin_arrive.php:202 -#: includes/pages/admin_arrive.php:217 includes/pages/admin_arrive.php:232 -#: includes/view/AngelTypes_view.php:424 includes/view/AngelTypes_view.php:432 -#: includes/view/User_view.php:207 msgid "Sum" msgstr "Summe" -#: includes/pages/admin_active.php:305 msgid "Search angel:" msgstr "Suche Engel:" -#: includes/pages/admin_active.php:306 msgid "Show all shifts" msgstr "Alle Schichten anzeigen" -#: includes/pages/admin_active.php:307 includes/pages/admin_arrive.php:178 -#: includes/pages/admin_arrive.php:179 includes/pages/admin_free.php:105 -msgid "Search" -msgstr "Suche" - -#: includes/pages/admin_active.php:310 msgid "How much angels should be active?" msgstr "Wie viele Engel sollten aktiv sein?" -#: includes/pages/admin_active.php:311 includes/pages/admin_shifts.php:320 -#: includes/pages/admin_shifts.php:470 -msgid "Preview" -msgstr "Vorschau" - -#: includes/pages/admin_active.php:315 includes/pages/admin_arrive.php:182 -msgid "Nickname" -msgstr "Nick" - -#: includes/pages/admin_active.php:316 includes/pages/admin_active.php:326 -#: includes/view/User_view.php:233 msgid "Size" msgstr "Grâße" msgid "No." msgstr "Nr." -#: includes/pages/admin_active.php:317 includes/pages/user_shifts.php:11 -#: includes/sys_menu.php:96 includes/view/AngelTypes_view.php:357 -#: includes/view/Rooms_view.php:39 includes/view/User_view.php:641 msgid "Shifts" msgstr "Schichten" -#: includes/pages/admin_active.php:318 includes/pages/admin_shifts.php:427 msgid "Length" msgstr "LΓ€nge" -#: includes/pages/admin_active.php:319 msgid "Active?" msgstr "Aktiv?" -#: includes/pages/admin_active.php:320 includes/view/User_view.php:231 msgid "Forced" msgstr "Erzwungen" -#: includes/pages/admin_active.php:321 msgid "T-shirt?" msgstr "T-Shirt?" -#: includes/pages/admin_active.php:324 -msgid "Shirt statistic" +msgid "T-shirt statistic" msgstr "T-Shirt Statistik" -#: includes/pages/admin_active.php:327 -msgid "Given shirts" +msgid "Given T-shirts" msgstr "Ausgegebene T-Shirts" -#: includes/pages/admin_arrive.php:10 includes/sys_menu.php:111 msgid "Arrive angels" msgstr "Ankommende Engel" -#: includes/pages/admin_arrive.php:41 msgid "Reset done. Angel has not arrived." msgstr "ZurΓΌckgesetzt. Engel ist nicht angekommen." -#: includes/pages/admin_arrive.php:59 msgid "Angel has been marked as arrived." msgstr "Engel wurde als angekommen markiert." -#: includes/pages/admin_arrive.php:106 -msgid "reset" -msgstr "zurΓΌcksetzen" - -#: includes/pages/admin_arrive.php:106 includes/pages/admin_arrive.php:193 -#: includes/pages/admin_arrive.php:208 includes/pages/admin_arrive.php:223 -#: includes/view/User_view.php:601 -msgid "arrived" -msgstr "angekommen" +msgid "Reset" +msgstr "ZurΓΌcksetzen" -#: includes/pages/admin_arrive.php:183 includes/view/User_view.php:236 msgid "Planned arrival" msgstr "Geplanter Ankunftstag" -#: includes/pages/admin_arrive.php:184 msgid "Arrived?" msgstr "Angekommen?" -#: includes/pages/admin_arrive.php:185 msgid "Arrival date" msgstr "Ankunftsdatum" -#: includes/pages/admin_arrive.php:186 includes/view/User_view.php:241 msgid "Planned departure" msgstr "Geplante Abreise" -#: includes/pages/admin_arrive.php:191 msgid "Planned arrival statistics" msgstr "Geplante Ankunfts-Statistik" -#: includes/pages/admin_arrive.php:194 includes/pages/admin_arrive.php:209 -#: includes/pages/admin_arrive.php:224 msgid "arrived sum" msgstr "Summe angekommen" -#: includes/pages/admin_arrive.php:200 includes/pages/admin_arrive.php:215 -#: includes/pages/admin_arrive.php:230 -#: includes/pages/user_messages.php:118 -msgid "Date" +msgid "title.date" msgstr "Datum" -#: includes/pages/admin_arrive.php:201 includes/pages/admin_arrive.php:216 -#: includes/pages/admin_arrive.php:231 -msgid "Count" -msgstr "Anzahl" - -#: includes/pages/admin_arrive.php:206 msgid "Arrival statistics" msgstr "Ankunfts-Statistik" -#: includes/pages/admin_arrive.php:221 msgid "Planned departure statistics" msgstr "Geplante Abreise-Statistik" -#: includes/pages/admin_free.php:12 includes/sys_menu.php:114 msgid "Free angels" msgstr "Freie Engel" -#: includes/pages/admin_free.php:29 -msgid "Alle" -msgstr "All" - -#: includes/pages/admin_free.php:106 includes/view/ShiftEntry_view.php:91 -#: includes/view/ShiftTypes_view.php:61 -#: includes/view/UserAngelTypes_view.php:152 msgid "Angeltype" msgstr "Engeltyp" -#: includes/pages/admin_free.php:112 includes/pages/guest_login.php:275 -#: includes/view/AngelTypes_view.php:298 includes/view/AngelTypes_view.php:311 -#: includes/view/User_view.php:39 includes/view/User_view.php:218 -msgid "Nick" -msgstr "Nick" - -#: includes/pages/admin_free.php:113 -msgid "Next shift" +msgid "shift.next" msgstr "NΓ€chste Schicht" -#: includes/pages/admin_free.php:114 msgid "Last shift" msgstr "Letzte Schicht" -#: includes/pages/admin_free.php:115 includes/pages/guest_login.php:336 -#: includes/view/AngelTypes_view.php:118 includes/view/AngelTypes_view.php:299 -#: includes/view/AngelTypes_view.php:312 includes/view/AngelTypes_view.php:481 -#: includes/view/User_view.php:60 includes/view/User_view.php:225 -msgid "DECT" +msgid "general.dect" msgstr "DECT" -#: includes/pages/admin_groups.php:10 includes/sys_menu.php:119 msgid "Grouprights" msgstr "Gruppenrechte" -#: includes/pages/admin_groups.php:52 includes/pages/admin_import.php:184 -#: includes/pages/admin_import.php:188 includes/pages/admin_rooms.php:161 -#: includes/pages/admin_rooms.php:214 includes/view/AngelTypes_view.php:86 -#: includes/view/AngelTypes_view.php:87 includes/view/AngelTypes_view.php:117 -#: includes/view/AngelTypes_view.php:480 includes/view/AngelTypes_view.php:504 -#: includes/view/ShiftTypes_view.php:60 includes/view/ShiftTypes_view.php:140 -#: includes/view/User_view.php:222 -msgid "Name" +msgid "general.name" msgstr "Name" -#: includes/pages/admin_groups.php:53 msgid "Privileges" msgstr "Privilegien" -#: includes/pages/admin_groups.php:102 msgid "Edit group" msgstr "Gruppe bearbeiten" -#: includes/pages/admin_import.php:8 includes/pages/admin_rooms.php:162 -#: includes/pages/admin_rooms.php:215 includes/sys_menu.php:120 -msgid "Frab import" -msgstr "Frab Import" - -#: includes/pages/admin_import.php:39 -msgid "Webserver has no write-permission on import directory." -msgstr "Der Webserver hat keine Schreibrechte fΓΌr das Verzeichnis import." - -#: includes/pages/admin_import.php:64 includes/pages/admin_import.php:143 -#: includes/pages/admin_import.php:242 includes/pages/admin_shifts.php:60 -#: includes/pages/admin_shifts.php:66 msgid "Please select a shift type." msgstr "Bitte einen Schichttyp wΓ€hlen." -#: includes/pages/admin_import.php:72 includes/pages/admin_import.php:150 -#: includes/pages/admin_import.php:249 -msgid "Please enter an amount of minutes to add to a talk's begin." -msgstr "" -"Bitte gib eine Anzahl Minuten ein, die vor dem Talk-Beginn hinzugefΓΌgt " -"werden sollen." - -#: includes/pages/admin_import.php:79 includes/pages/admin_import.php:157 -#: includes/pages/admin_import.php:256 -msgid "Please enter an amount of minutes to add to a talk's end." -msgstr "" -"Bitte gib eine Anzahl Minuten ein, die nach dem Talk-Ende hinzugefΓΌgt werden " -"sollen." - -#: includes/pages/admin_import.php:87 -msgid "No valid xml/xcal file provided." -msgstr "Keine valide xml/xcal Datei hochgeladen." - -#: includes/pages/admin_import.php:92 -msgid "File upload went wrong." -msgstr "Das Hochladen der Datei ist schiefgegangen." - -#: includes/pages/admin_import.php:96 -msgid "Please provide some data." -msgstr "Bitte lade eine Datei hoch." - -#: includes/pages/admin_import.php:111 includes/pages/admin_import.php:172 -#: includes/pages/admin_import.php:292 -msgid "File Upload" -msgstr "Datei hochladen" - -#: includes/pages/admin_import.php:113 includes/pages/admin_import.php:174 -#: includes/pages/admin_import.php:294 -msgid "Validation" -msgstr "ÜberprΓΌfen" - -#: includes/pages/admin_import.php:115 includes/pages/admin_import.php:127 -#: includes/pages/admin_import.php:176 includes/pages/admin_import.php:218 -#: includes/pages/admin_import.php:296 -msgid "Import" -msgstr "Importieren" - -#: includes/pages/admin_import.php:121 -msgid "" -"This import will create/update/delete rooms and shifts by given FRAB-export " -"file. The needed file format is xcal." -msgstr "" -"Dieser Import erzeugt, Γ€ndert und lΓΆscht RΓ€ume und Schichten anhand einer " -"FRAB-Export Datei. Das benΓΆtigte Format ist xcal." - -#: includes/pages/admin_import.php:124 -msgid "Add minutes to start" -msgstr "Minuten vor Talk-Beginn hinzufΓΌgen" - -#: includes/pages/admin_import.php:125 -msgid "Add minutes to end" -msgstr "Minuten nach Talk-Ende hinzufΓΌgen" - -#: includes/pages/admin_import.php:126 -msgid "xcal-File (.xcal)" -msgstr "xcal-Datei (.xcal)" +msgid "Reset to previous state" +msgstr "Auf vorherigen Stand zurΓΌcksetzen" -#: includes/pages/admin_import.php:136 includes/pages/admin_import.php:231 -msgid "Missing import file." -msgstr "Import-Datei nicht vorhanden." - -#: includes/pages/admin_import.php:183 -msgid "Rooms to create" -msgstr "Anzulegende RΓ€ume" - -#: includes/pages/admin_import.php:187 -msgid "Rooms to delete" -msgstr "Zu lΓΆschende RΓ€ume" - -#: includes/pages/admin_import.php:191 -msgid "Shifts to create" -msgstr "Anzulegende Schichten" - -#: includes/pages/admin_import.php:193 includes/pages/admin_import.php:202 -#: includes/pages/admin_import.php:211 -msgid "Day" -msgstr "Tag" - -#: includes/pages/admin_import.php:194 includes/pages/admin_import.php:203 -#: includes/pages/admin_import.php:212 includes/pages/admin_shifts.php:420 -#: includes/view/Shifts_view.php:26 -msgid "Start" -msgstr "Beginn" - -#: includes/pages/admin_import.php:195 includes/pages/admin_import.php:204 -#: includes/pages/admin_import.php:213 includes/pages/admin_shifts.php:421 -#: includes/view/Shifts_view.php:34 -msgid "End" -msgstr "Ende" - -#: includes/pages/admin_import.php:196 includes/pages/admin_import.php:205 -#: includes/pages/admin_import.php:214 -msgid "Shift type" -msgstr "Schichttyp" - -#: includes/pages/admin_import.php:198 includes/pages/admin_import.php:207 -#: includes/pages/admin_import.php:216 includes/pages/admin_shifts.php:417 -msgid "Room" -msgstr "Raum" - -#: includes/pages/admin_import.php:200 -msgid "Shifts to update" -msgstr "Zu aktualisierende Schichten" - -#: includes/pages/admin_import.php:209 -msgid "Shifts to delete" -msgstr "Zu lΓΆschende Schichten" - -#: includes/pages/admin_import.php:297 -msgid "It's done!" -msgstr "Erledigt!" - -#: includes/pages/admin_rooms.php:202 -#: includes/view/User_view.php:129 -msgid "Delete" -msgstr "lΓΆschen" +msgid "Location" +msgstr "Ort" -#: includes/pages/admin_questions.php:11 includes/sys_menu.php:115 msgid "Answer questions" msgstr "Fragen beantworten" -#: includes/pages/admin_questions.php:27 msgid "There are unanswered questions!" msgstr "Es gibt unbeantwortete Fragen!" -#: includes/pages/admin_questions.php:80 -msgid "Unanswered questions" -msgstr "Unbeantwortete Fragen" - -#: includes/pages/admin_questions.php:82 includes/pages/admin_questions.php:89 -msgid "From" -msgstr "Von" - -#: includes/pages/admin_questions.php:83 includes/pages/admin_questions.php:90 -#: includes/view/Questions_view.php:30 includes/view/Questions_view.php:35 -msgid "Question" -msgstr "Frage" - -#: includes/pages/admin_questions.php:84 includes/pages/admin_questions.php:92 -#: includes/view/Questions_view.php:37 -msgid "Answer" -msgstr "Antwort" - -#: includes/pages/admin_questions.php:87 includes/view/Questions_view.php:33 -msgid "Answered questions" -msgstr "Beantwortete Fragen" - -#: includes/pages/admin_questions.php:91 includes/view/Questions_view.php:36 -msgid "Answered by" -msgstr "Antwort von" - -#: includes/pages/admin_questions.php:102 includes/view/Questions_view.php:40 -msgid "Answered at" -msgstr "Beantwortet am" - -#: includes/pages/admin_questions.php:91 includes/view/Questions_view.php:37 -msgid "Asked at" -msgstr "Gefragt am" +msgid "Locations" +msgstr "Orte" -#: includes/pages/admin_rooms.php:7 includes/pages/user_shifts.php:230 -#: includes/sys_menu.php:118 includes/sys_menu.php:172 -msgid "Rooms" -msgstr "RΓ€ume" - -#: includes/pages/admin_rooms.php:82 -msgid "This name is already in use." -msgstr "Dieser Name ist bereits vergeben." - -#: includes/pages/admin_rooms.php:113 -#, php-format -msgid "Please enter needed angels for type %s." -msgstr "Bitte gib die Anzahl der benΓΆtigten Engel vom Typ %s an." - -#: includes/pages/admin_rooms.php:142 -msgid "Room saved." -msgstr "Raum gespeichert." - -#: includes/pages/admin_rooms.php:163 -msgid "Map URL" -msgstr "Karten URL" - -#: includes/pages/admin_rooms.php:164 -msgid "The map url is used to display an iframe on the room page." -msgstr "" -"Die Karten URL wird benutzt um auf der Raum-Seite ein iframe anzuzeigen." - -#: includes/pages/admin_rooms.php:165 includes/view/AngelTypes_view.php:110 -#: includes/view/AngelTypes_view.php:403 includes/view/Rooms_view.php:24 -#: includes/view/ShiftTypes_view.php:62 includes/view/ShiftTypes_view.php:100 -#: includes/view/Shifts_view.php:171 -msgid "Description" +msgid "general.description" msgstr "Beschreibung" -#: includes/pages/admin_rooms.php:166 includes/view/AngelTypes_view.php:111 -#: includes/view/ShiftTypes_view.php:63 msgid "Please use markdown for the description." msgstr "Bitte benutze Markdown fΓΌr die Beschreibung." -#: includes/pages/admin_rooms.php:171 includes/view/PublicDashboard_view.php:26 msgid "Needed angels:" msgstr "BenΓΆtigte Engel:" -#: includes/pages/admin_rooms.php:192 -#, php-format -msgid "Room %s deleted." -msgstr "Raum %s gelΓΆscht." - -#: includes/pages/admin_rooms.php:200 -#, php-format -msgid "Do you want to delete room %s?" -msgstr "MΓΆchest Du den Raum %s wirklich lΓΆschen?" - -#: includes/pages/admin_rooms.php:210 -msgid "add" -msgstr "Neu" - -#: includes/pages/admin_rooms.php:216 includes/view/Rooms_view.php:31 -msgid "Map" -msgstr "Karte" - -#: includes/pages/admin_shifts.php:10 includes/sys_menu.php:117 msgid "Create shifts" msgstr "Schichten erstellen" -#: includes/pages/admin_shifts.php msgid "Additional description" msgstr "ZusΓ€tzliche Beschreibung" msgid "This description is for single shifts, otherwise please use the description in shift type." msgstr "Diese Beschreibung ist fΓΌr einzelne Schichten, ansonsten nutze bitte die Beschreibung im Schichttyp." -#: includes/pages/admin_shifts.php:82 -msgid "Please select a location." -msgstr "Bitte einen Ort auswΓ€hlen." - -#: includes/pages/admin_shifts.php:89 msgid "Please select a start time." msgstr "Bitte eine Startzeit auswΓ€hlen." -#: includes/pages/admin_shifts.php:96 msgid "Please select an end time." msgstr "Bitte eine Endzeit auswΓ€hlen." -#: includes/pages/admin_shifts.php:101 msgid "The shifts end has to be after its start." msgstr "Die Endzeit muss nach der Startzeit liegen." -#: includes/pages/admin_shifts.php:113 msgid "Please enter a shift duration in minutes." msgstr "Gib bitte eine SchichtlΓ€nge in Minuten ein." -#: includes/pages/admin_shifts.php:130 +msgid "Please validate the change hour %s. It should be between 00:00 and 24:00." +msgstr "Bitte korrigiere die Schichtwechselstunde %s. Sie sollte zwischen 00:00 und 24:00 Uhr liegen." + msgid "Please split the shift-change hours by colons." msgstr "Trenne die Schichtwechselstunden mit einem Komma." -#: includes/pages/admin_shifts.php:137 msgid "Please select a mode." msgstr "Bitte einen Modus auswΓ€hlen." -#: includes/pages/admin_shifts.php:150 -#, php-format msgid "Please check the needed angels for team %s." msgstr "Bitte gib die Anzahl der benΓΆtigten Engel vom Team %s an." -#: includes/pages/admin_shifts.php:156 msgid "There are 0 angels needed. Please enter the amounts of needed angels." msgstr "Es werden 0 Engel benΓΆtigt. Bitte Γ€ndere das." -#: includes/pages/admin_shifts.php:160 msgid "Please select a mode for needed angels." msgstr "Bitte wΓ€hle einen Modus fΓΌr benΓΆtigte Engel." -#: includes/pages/admin_shifts.php:164 msgid "Please select needed angels." msgstr "Bitte wΓ€hle benΓΆtigte Engel." -#: includes/pages/admin_shifts.php:335 msgid "Time and location" msgstr "Zeit und Ort" -#: includes/pages/admin_shifts.php:336 msgid "Type and title" msgstr "Typ und Titel" -#: includes/pages/admin_shifts.php:422 msgid "Mode" msgstr "Modus" -#: includes/pages/admin_shifts.php:423 msgid "Create one shift" msgstr "Eine Schicht erstellen" -#: includes/pages/admin_shifts.php:424 msgid "Create multiple shifts" msgstr "Mehrere Schichten erstellen" -#: includes/pages/admin_shifts.php:434 msgid "Create multiple shifts with variable length" msgstr "Erstelle mehrere Schichten mit unterschiedlicher LΓ€nge" -#: includes/pages/admin_shifts.php:440 msgid "Shift change hours" msgstr "Schichtwechsel-Stunden" -#: includes/pages/admin_shifts.php:447 msgid "Create a shift over midnight." -msgstr "" +msgstr "Erstelle Schichten ΓΌber Mitternacht" -#: includes/pages/admin_shifts.php:455 -msgid "Take needed angels from room settings" -msgstr "Übernehme benΓΆtigte Engel von den Raum-Einstellungen" +msgid "Copy needed angels from location settings" +msgstr "Kopiere benΓΆtigte Engel von den Ort-Einstellungen" + +msgid "Copy needed angels from shift type settings" +msgstr "Kopiere benΓΆtigte Engel von den Schichttyp-Einstellungen" -#: includes/pages/admin_shifts.php:461 msgid "The following angels are needed" msgstr "Die folgenden Engel werden benΓΆtigt" -#: includes/pages/admin_user.php:11 includes/sys_menu.php:113 msgid "All Angels" -msgstr "Engelliste" +msgstr "Alle Engel" -#: includes/pages/admin_user.php:32 msgid "This user does not exist." msgstr "Benutzer existiert nicht." -#: includes/pages/admin_user.php:71 includes/pages/guest_login.php:307 -#: includes/view/User_view.php:81 -msgid "Please select..." -msgstr "Bitte auswΓ€hlen..." - -#: includes/pages/admin_user.php:76 includes/pages/admin_user.php:83 -#: includes/view/AngelTypes_view.php:89 includes/view/AngelTypes_view.php:96 -#: includes/view/AngelTypes_view.php:101 includes/view/AngelTypes_view.php:108 msgid "Yes" msgstr "Ja" -#: includes/pages/admin_user.php:77 includes/pages/admin_user.php:85 -#: includes/view/AngelTypes_view.php:89 includes/view/AngelTypes_view.php:96 -#: includes/view/AngelTypes_view.php:102 includes/view/AngelTypes_view.php:108 msgid "No" msgstr "Nein" -#: includes/pages/admin_user.php:95 msgid "Force active" msgstr "Aktiv erzwingen" -#: includes/pages/admin_user.php:112 -msgid "" -"Please visit the angeltypes page or the users profile to manage users " -"angeltypes." -msgstr "" -"Bitte benutze die Engeltypen-Seite um die Engeltypen des Users zu verwalten." - -#: includes/pages/admin_user.php:314 msgid "Edit user" msgstr "User bearbeiten" -#: includes/pages/guest_login.php:77 -msgid "Please enter a valid nick." -msgstr "Gib bitte einen erlaubten Nick an." - -#: includes/pages/guest_login.php:77 includes/pages/guest_login.php:277 -#: includes/view/User_view.php:42 -msgid "" -"Use up to 24 letters, numbers, connecting punctuations or spaces for your " -"nickname." -msgstr "" -"Verwende bis zu 24 Buchstaben, Zahlen, verbindende Schriftzeichen (.-_) oder " -"Leerzeichen fΓΌr deinen Nick." - -#: includes/pages/guest_login.php:82 -#, php-format -msgid "Your nick \"%s\" already exists." -msgstr "Der Nick \"%s\" existiert bereits." - -#: includes/pages/guest_login.php:86 -msgid "Please enter a nickname." -msgstr "Gib bitte einen Nick an." - -#: includes/pages/guest_login.php:93 -msgid "E-mail address is not correct." -msgstr "Die E-Mail Adresse ist nicht in Ordnung." - -#: includes/pages/guest_login.php:97 -msgid "E-mail address is already used by another user." -msgstr "Die E-Mail Adresse wurde bereits von einem anderen User benutzt." - -#: includes/pages/guest_login.php:101 -msgid "Please enter your e-mail." -msgstr "Bitte gib Deine E-Mail-Adresse ein." - -#: includes/pages/guest_login.php:117 -msgid "Please select your shirt size." -msgstr "Bitte wΓ€hle Deine T-Shirt Grâße." - -#: includes/pages/guest_login.php:129 -#, php-format -msgid "Your password is too short (please use at least %s characters)." -msgstr "Dein Passwort ist zu kurz (Bitte mindestens %s Zeichen nutzen)." - -#: includes/pages/guest_login.php:140 includes/pages/guest_login.php:144 -msgid "" -"Please enter your planned date of arrival. It should be after the buildup " -"start date and before teardown end date." -msgstr "" -"Bitte gib Dein geplantes Ankunftsdatum an. Es sollte nach dem Aufbaubeginn " -"und vor dem Abbauende liegen." - -#: includes/pages/guest_login.php:166 -msgid "For dect numbers are only 40 digits allowed." -msgstr "Die DECT Nummer darf nur 40 Zeichen lang sein." - -#: includes/pages/guest_login.php:238 -msgid "Angel registration successful!" -msgstr "Engel-Registrierung erfolgreich!" - -#: includes/pages/guest_login.php:267 -msgid "" -"By completing this form you're registering as a Chaos-Angel. This script " -"will create you an account in the angel task scheduler." -msgstr "" -"Mit diesem Formular registrierst Du Dich als Engel. Du bekommst ein Konto in " -"der Engel-Aufgabenverwaltung." - -#: includes/pages/guest_login.php:284 includes/view/User_view.php:66 -#, php-format -msgid "The %s is allowed to send me an email (e.g. when my shifts change)" -msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten Γ€ndern)" - -msgid "Notify me of new news" -msgstr "Benachrichtige mich bei neuen News" - -#: includes/pages/guest_login.php:291 includes/view/User_view.php:73 -msgid "Allow heaven angels to contact you by e-mail." -msgstr "Erlaube Himmel-Engeln dich per Mail zu kontaktieren." - -msgid "" -"To receive vouchers, give consent that nick, email address, worked hours and shirt size " -"will be stored until the next similar event." -msgstr "Um Voucher zu erhalten, stimme zu, dass Nick, E-Mail-Adresse, geleistete Arbeit und Shirtgrâße " -"bis zum nΓ€chsten gleichartigen Event gespeichert werden." - -msgid "To withdraw your approval, send an email to %1$s." -msgstr "Dies kann jederzeit durch eine E-Mail an %1$s widerrufen werden." - -#: includes/pages/guest_login.php:300 includes/view/User_view.php:48 -msgid "Planned date of arrival" -msgstr "Geplanter Ankunftstag" - -#: includes/pages/guest_login.php:306 includes/view/User_view.php:78 -msgid "Shirt size" -msgstr "T-Shirt Grâße" - -#: includes/pages/guest_login.php:320 -msgid "What do you want to do?" -msgstr "Was mΓΆchtest Du machen?" - -#: includes/pages/guest_login.php:323 -msgid "Description of job types" -msgstr "Beschreibung der Aufgaben" - -#: includes/pages/guest_login.php:330 -msgid "" -"Some angel types have to be confirmed later by a supporter at an " -"introduction meeting. You can change your selection in the options section." -msgstr "" -"Engeltypen welche eine EinfΓΌhrung benΓΆtigen, werden bei einem EinfΓΌhrungstreffen von " -"einem Supporter freigeschaltet. Du kannst Deine Auswahl spΓ€ter in den Einstellungen Γ€ndern." - -#: includes/pages/guest_login.php:339 includes/view/User_view.php:61 -msgid "Mobile" -msgstr "Handy" - -#: includes/pages/guest_login.php:344 includes/view/User_view.php:45 -msgid "First name" -msgstr "Vorname" - -#: includes/pages/guest_login.php:347 includes/view/User_view.php:44 -msgid "Last name" -msgstr "Nachname" - -#: includes/pages/guest_login.php:350 includes/view/User_view.php:38 -msgid "Entry required!" -msgstr "Pflichtfeld!" - -#: includes/pages/user_messages.php:49 -msgid "Select recipient..." -msgstr "EmpfΓ€nger auswΓ€hlen..." - -#: includes/pages/user_messages.php:49 -msgid "Send" -msgstr "Senden" - -#: includes/pages/user_messages.php:61 includes/pages/user_messages.php:75 -msgid "Y-m-d H:i" +msgid "general.datetime" msgstr "d.m.Y H:i" -#: includes/pages/user_messages.php:98 -msgid "mark as read" -msgstr "als gelesen markieren" - -#: includes/pages/user_messages.php:105 -msgid "delete message" -msgstr "Nachricht lΓΆschen" - -#: includes/pages/user_messages.php:114 -#, php-format -msgid "Hello %s, here can you leave messages for other angels" -msgstr "Hallo %s, hier kannst Du anderen Engeln Nachrichten schreiben." - -#: includes/pages/user_messages.php:117 -msgid "New" -msgstr "Neu" - -#: includes/pages/user_messages.php:119 -msgid "Transmitted" -msgstr "Gesendet" - -#: includes/pages/user_messages.php:120 -msgid "Recipient" -msgstr "EmpfΓ€nger" - -#: includes/pages/user_messages.php:132 includes/pages/user_messages.php:154 -msgid "Incomplete call, missing Message ID." -msgstr "UnvollstΓ€ndiger Aufruf, fehlende Nachrichten ID." - -#: includes/pages/user_messages.php:146 includes/pages/user_messages.php:165 -msgid "No Message found." -msgstr "Keine Nachricht gefunden." - -#: includes/pages/user_messages.php:173 -msgid "Transmitting was terminated with an Error." -msgstr "Übertragung wurde mit einem Fehler abgebrochen." - -#: includes/pages/user_messages.php:178 -msgid "Wrong action." -msgstr "Falsche Aktion." - -#: includes/pages/user_myshifts.php:11 includes/view/Shifts_view.php:161 -msgid "My shifts" -msgstr "Meine Schichten" - -#: includes/pages/user_myshifts.php:39 msgid "Key changed." msgstr "Key geΓ€ndert." -#: includes/pages/user_myshifts.php:42 includes/view/User_view.php:646 -#: includes/pages/user_myshifts.php:42 includes/pages/user_shifts.php:310 msgid "Reset API key" msgstr "API-Key zurΓΌcksetzen" -#: includes/pages/user_myshifts.php:44 msgid "" "If you reset the key, the url to your iCal- and JSON-export and your atom/rss " "feed changes! You have to update it in every application using one of these " @@ -1730,74 +751,24 @@ msgstr "" "Wenn du den API-Key zurΓΌcksetzt, Γ€ndert sich die URL zu deinem iCal-, JSON-" "Export und Atom/RSS Feed! Du musst diesen ΓΌberall Γ€ndern, wo er in Benutzung ist." -#: includes/pages/user_myshifts.php:47 msgid "Continue" msgstr "Fortfahren" -#: includes/pages/user_myshifts.php:86 msgid "Please enter a freeload comment!" msgstr "Gib bitte einen SchwΓ€nz-Kommentar ein!" -#: includes/pages/user_myshifts.php:111 msgid "Shift saved." msgstr "Schicht gespeichert." -#: includes/sys_menu.php:94 -msgid "News" -msgstr "News" - -#: includes/sys_menu.php:95 -msgid "Meetings" -msgstr "Treffen" - -#: includes/pages/user_questions.php:11 includes/sys_menu.php:98 -#: includes/view/Questions_view.php:40 msgid "Ask the Heaven" msgstr "Frag den Himmel" -#: includes/pages/user_questions.php:55 -msgid "You question was saved." -msgstr "Frage gespeichert." - -#: includes/pages/user_questions.php:59 -msgid "Please enter a question!" -msgstr "Gib eine Frage ein!" +msgid "The administration has not configured any locations yet." +msgstr "Die Administratoren habe noch keine Orte eingerichtet." -#: includes/pages/user_questions.php:71 -msgid "Incomplete call, missing Question ID." -msgstr "UnvollstΓ€ndiger Aufruf, fehlende Fragen ID." - -#: includes/pages/user_questions.php:86 -msgid "No question found." -msgstr "Keine Frage gefunden." - -#: includes/sys_menu.php:69 -#: includes/view/User_view.php:616 -msgid "Settings" -msgstr "Einstellungen" - -msgid "" -"Please enter your planned date of departure. It should be after your planned " -"arrival date and after buildup start date and before teardown end date." -msgstr "" -"Bitte gibt dein geplantes Abreisedatum an. Es sollte nach Deinem " -"Anreisedatum, nach dem Aufbaubeginn und vor dem Abbauende liegen." - -msgid "-> not OK. Please try again." -msgstr "-> Nicht OK. Bitte erneut versuchen." - -msgid "Your password is to short (please use at least 6 characters)." -msgstr "Dein Passwort ist zu kurz (Bitte mindestens 6 Zeichen nutzen)." - -#: includes/pages/user_shifts.php:101 -msgid "The administration has not configured any rooms yet." -msgstr "Die Administratoren habe noch keine RΓ€ume eingerichtet." - -#: includes/pages/user_shifts.php:120 msgid "The administration has not configured any shifts yet." msgstr "Die Administratoren haben noch keine Schichten angelegt." -#: includes/pages/user_shifts.php:137 msgid "" "The administration has not configured any angeltypes yet - or you are not " "subscribed to any angeltype." @@ -1805,71 +776,51 @@ msgstr "" "Die Administratoren haben noch keine Engeltypen konfiguriert - oder Du hast " "noch keine Engeltypen ausgewΓ€hlt." -#: includes/pages/user_shifts.php:204 msgid "occupied" msgstr "belegt" -#: includes/pages/user_shifts.php:208 msgid "free" msgstr "frei" -#: includes/pages/user_shifts.php:253 msgid "Own" msgstr "Eigene" -#: includes/pages/user_shifts.php:258 msgid "Occupancy" msgstr "Belegung" -#: includes/pages/user_shifts.php:261 msgid "" "The tasks shown here are influenced by the angeltypes you joined already!" msgstr "" "Die Schichten, die hier angezeigt werden, sind von Deinen Einstellungen " "(Engeltypen/Aufgaben) abhΓ€ngig!" -#: includes/pages/user_shifts.php:263 -msgid "Description of the jobs." -msgstr "Beschreibung der Aufgaben." - -#: includes/pages/user_shifts.php:267 msgid "Filter" msgstr "Filter" -#: includes/pages/user_shifts.php:268 msgid "Yesterday" msgstr "Gestern" -#: includes/pages/user_shifts.php:269 msgid "Today" msgstr "Heute" -#: includes/pages/user_shifts.php:270 msgid "Tomorrow" msgstr "Morgen" -#: includes/pages/user_shifts.php:271 msgid "last 8h" msgstr "letzte 8h" -#: includes/pages/user_shifts.php:272 msgid "last 4h" msgstr "letzte 4h" -#: includes/pages/user_shifts.php:273 msgid "next 4h" msgstr "nΓ€chste 4h" -#: includes/pages/user_shifts.php:274 msgid "next 8h" msgstr "nΓ€chste 8h" -#: includes/pages/user_shifts.php:292 msgid "iCal export and API" msgstr "iCal Export und API" -#: includes/pages/user_shifts.php:293 -#, php-format msgid "" "Export your own shifts. iCal format or JSON format available (please keep secret, otherwise JSON Format verfΓΌgbar (Link bitte geheimhalten, sonst API-Key zurΓΌcksetzen)." -#: includes/pages/user_shifts.php:304 msgid "Show API Key" msgstr "API Key anzeigen" -#: includes/pages/user_shifts.php:327 includes/view/ShiftTypes_view.php:48 msgid "All" msgstr "Alle" -#: includes/pages/user_shifts.php:327 msgid "None" msgstr "Keine" -#: includes/sys_menu.php:78 -msgid "Logout" -msgstr "Logout" +msgid "general.logout" +msgstr "Abmelden" -#: includes/sys_menu.php:141 msgid "Admin" msgstr "Admin" -#: includes/sys_menu.php:163 -msgid "Manage rooms" -msgstr "Verwalte RΓ€ume" +msgid "Manage locations" +msgstr "Orte verwalten" -#: includes/sys_template.php:316 msgid "No data found." msgstr "Nichts gefunden." -#: includes/view/AngelTypes_view.php:40 includes/view/AngelTypes_view.php:454 msgid "Unconfirmed" msgstr "UnbestΓ€tigt" -#: includes/view/AngelTypes_view.php:42 includes/view/AngelTypes_view.php:46 msgid "Supporter" msgstr "Supporter" -#: includes/view/AngelTypes_view.php:44 includes/view/AngelTypes_view.php:48 msgid "Member" msgstr "Mitglied" -#: includes/view/AngelTypes_view.php:60 -#, php-format msgid "Do you want to delete angeltype %s?" msgstr "MΓΆchtest Du den Engeltypen %s lΓΆschen?" -#: includes/view/AngelTypes_view.php:63 includes/view/ShiftEntry_view.php:27 -#: includes/view/ShiftEntry_view.php:56 includes/view/ShiftTypes_view.php:25 -#: includes/view/UserAngelTypes_view.php:27 -#: includes/view/UserAngelTypes_view.php:55 -#: includes/view/UserAngelTypes_view.php:74 -#: includes/view/UserAngelTypes_view.php:98 -#: includes/view/UserAngelTypes_view.php:122 -#: includes/view/UserAngelTypes_view.php:175 -msgid "cancel" -msgstr "abbrechen" - -#: includes/view/AngelTypes_view.php:89 includes/view/AngelTypes_view.php:90 -#: includes/view/AngelTypes_view.php:505 -msgid "Requires introduction" +msgid "angeltypes.restricted" msgstr "BenΓΆtigt EinfΓΌhrung" -#: includes/view/AngelTypes_view.php:93 -msgid "" -"Angel types which require introduction can only be used by an angel if " -"enabled by a supporter (double opt-in)." +msgid "angeltypes.restricted.info" msgstr "" "Engeltypen, welche eine EinfΓΌhrung benΓΆtigen, mΓΌssen von einem Supporter freigeschaltet werden " "(double-opt-in)." -#: includes/view/AngelTypes_view.php:96 includes/view/AngelTypes_view.php:97 -msgid "No Self Sign Up allowed" -msgstr "Kein Selbst-Eintragen erlaubt" +msgid "shift.self_signup" +msgstr "Schichten selbst eintragen" + +msgid "shift.self_signup.allowed" +msgstr "Schichten selbst eintragen erlaubt" + +msgid "angeltypes.shift.self_signup.info" +msgstr "Engeltypen, welche Schichten selbst eintragen erlaubt haben, " +"erlauben Engeln sich selbst in ihre Schichten einzutragen, " +"ist Schichten selbst eintragen nicht erlaubt kΓΆnnen nur Supporter " +"oder Admins Engel in die Schichten des Engeltyps eintragen." + -#: includes/view/AngelTypes_view.php:99 includes/view/AngelTypes_view.php:105 msgid "Requires driver license" msgstr "BenΓΆtigt FΓΌhrerschein" -#: includes/view/AngelTypes_view.php:108 includes/view/AngelTypes_view.php:109 msgid "Show on dashboard" msgstr "Auf dem Dashboard anzeigen" -#: includes/view/AngelTypes_view.php:112 includes/view/AngelTypes_view.php:479 msgid "Contact" msgstr "Kontakt" -#: includes/view/AngelTypes_view.php:115 msgid "Primary contact person/desk for user questions." msgstr "Ansprechpartner fΓΌr Fragen." -#: includes/view/AngelTypes_view.php:145 msgid "my driving license" msgstr "Meine FΓΌhrerschein-Infos" -#: includes/view/AngelTypes_view.php:157 msgid "" "This angeltype requires a driver license. Please enter your driver license " "information!" @@ -1980,8 +906,6 @@ msgstr "" "Dieser Engeltyp benΓΆtigt FΓΌhrerschein-Infos. Bitte trage Deine FΓΌhrerschein-" "Infos ein!" -#: includes/view/AngelTypes_view.php:162 -#, php-format msgid "" "You are unconfirmed for this angeltype. Please go to the introduction for %s " "to get confirmed." @@ -1989,462 +913,222 @@ msgstr "" "Du bist noch nicht fΓΌr diesen Engeltyp bestΓ€tigt. Bitte gehe zur EinfΓΌhrung " "fΓΌr %s um bestΓ€tigt zu werden." -#: includes/view/AngelTypes_view.php:224 -msgid "confirm" -msgstr "bestΓ€tigen" +msgid "Confirm" +msgstr "BestΓ€tigen" -#: includes/view/AngelTypes_view.php:232 -msgid "deny" -msgstr "ablehnen" +msgid "Deny" +msgstr "Ablehnen" -#: includes/view/AngelTypes_view.php:270 -msgid "remove" -msgstr "entfernen" +msgid "Remove" +msgstr "Entfernen" -#: includes/view/AngelTypes_view.php:300 msgid "Driver" msgstr "Fahrer" -#: includes/view/AngelTypes_view.php:301 msgid "Has car" msgstr "Hat Auto" -#: includes/view/AngelTypes_view.php:302 -#: includes/view/UserDriverLicenses_view.php:31 -msgid "Car" -msgstr "Auto" - -#: includes/view/AngelTypes_view.php:303 -msgid "3,5t Transporter" -msgstr "3,5t Transporter" - -#: includes/view/AngelTypes_view.php:304 -msgid "7,5t Truck" -msgstr "7,5t LKW" - -#: includes/view/AngelTypes_view.php:305 -msgid "12t Truck" -msgstr "12t LKW" - -#: includes/view/AngelTypes_view.php:306 -#: includes/view/UserDriverLicenses_view.php:49 -msgid "Forklift" -msgstr "Gabelstapler" - -#: includes/view/AngelTypes_view.php:350 msgid "Info" msgstr "Info" -#: includes/view/AngelTypes_view.php:418 msgid "Supporters" msgstr "Supporter" -#: includes/view/AngelTypes_view.php:438 msgid "Members" msgstr "Mitglieder" -#: includes/view/AngelTypes_view.php:446 -#: includes/view/UserAngelTypes_view.php:154 msgid "Add" msgstr "HinzufΓΌgen" -#: includes/view/AngelTypes_view.php:458 -msgid "confirm all" +msgid "Confirm all" msgstr "Alle bestΓ€tigen" -#: includes/view/AngelTypes_view.php:462 -msgid "deny all" +msgid "Deny all" msgstr "Alle ablehnen" -#: includes/view/AngelTypes_view.php:499 -msgid "New angeltype" -msgstr "Neuer Engeltyp" - -#: includes/view/AngelTypes_view.php:506 -msgid "Self Sign Up Allowed" -msgstr "Selbst-Eintragen erlaubt" - -#: includes/view/AngelTypes_view.php:507 msgid "Membership" msgstr "Mitgliedschaft" -#: includes/view/AngelTypes_view.php:586 -msgid "FAQ" -msgstr "FAQ" - -#: includes/view/EventConfig_view.php:91 msgid "Event Name" msgstr "Event Name" -#: includes/view/EventConfig_view.php:92 msgid "Event Name is shown on the start page." msgstr "Event Name wird auf der Startseite angezeigt." -#: includes/view/EventConfig_view.php:93 msgid "Event Welcome Message" msgstr "Event Willkommens-Nachricht" -#: includes/view/EventConfig_view.php:96 msgid "" "Welcome message is shown after successful registration. You can use markdown." msgstr "" "Die Willkommens-Nachricht wird nach einer erfolgreichen Registrierung " "angezeigt. Du kannst Markdown benutzen." -#: includes/view/EventConfig_view.php:100 msgid "Buildup date" msgstr "Aufbau Datum" -#: includes/view/EventConfig_view.php:101 msgid "Event start date" msgstr "Event Start Datum" -#: includes/view/EventConfig_view.php:104 msgid "Teardown end date" msgstr "Abbau Ende Datum" -#: includes/view/EventConfig_view.php:105 msgid "Event end date" msgstr "Event Ende Datum" -#: includes/view/PublicDashboard_view.php:37 msgid "Angels needed in the next 3 hrs" msgstr "BenΓΆtigte Engel in den nΓ€chsten 3 Stunden" -#: includes/view/PublicDashboard_view.php:38 msgid "Angels needed for nightshifts" msgstr "BenΓΆtigte Engel fΓΌr Nachtschichten" -#: includes/view/PublicDashboard_view.php:39 msgid "Angels currently working" msgstr "Aktuell arbeitende Engel" -#: includes/view/PublicDashboard_view.php:40 msgid "Hours to be worked" msgstr "Noch zu schaffende Stunden" -#: includes/view/PublicDashboard_view.php:56 msgid "Fullscreen" msgstr "Vollbild" msgid "Filtered" msgstr "Filtern" -#: includes/view/Questions_view.php:28 -msgid "Open questions" -msgstr "Offene Fragen" - -#: includes/view/Questions_view.php:42 -msgid "Your Question:" -msgstr "Deine Frage:" - -#: includes/view/ShiftCalendarRenderer.php:146 msgid "No shifts found." msgstr "Keine Schichten gefunden." -#: includes/view/ShiftCalendarRenderer.php:246 -msgid "Time" -msgstr "Zeit" - -#: includes/view/ShiftCalendarRenderer.php:312 msgid "Your shift" msgstr "Meine Schicht" -#: includes/view/ShiftCalendarRenderer.php:313 msgid "Help needed" msgstr "Hilfe benΓΆtigt" -#: includes/view/ShiftCalendarRenderer.php:314 msgid "Other angeltype needed / collides with my shifts" msgstr "Andere Engeltypen benΓΆtigt / kollidiert mit meinen Schichten" -#: includes/view/ShiftCalendarRenderer.php:315 msgid "Shift is full" msgstr "Schicht ist voll" -#: includes/view/ShiftCalendarRenderer.php:316 -msgid "Shift running/ended or user not arrived/allowed" -msgstr "" -"Schicht lΓ€uft/vorbei oder du bist noch nicht angekommen/darfst dich noch " -"nicht anmelden" +msgid "Shift is running/ended or you have not arrived" +msgstr "Schicht lΓ€uft/vorbei oder du bist noch nicht angekommen" -#: includes/view/ShiftCalendarShiftRenderer.php:135 msgid "Add more angels" -msgstr "Neue Engel hinzufΓΌgen" +msgstr "Mehr Engel hinzufΓΌgen" -#: includes/view/ShiftCalendarShiftRenderer.php:180 -#, php-format msgid "%d helper needed" msgid_plural "%d helpers needed" msgstr[0] "%d Helfer benΓΆtigt" msgstr[1] "%d Helfer benΓΆtigt" -#: includes/view/ShiftCalendarShiftRenderer.php:193 -#: includes/view/Shifts_view.php:85 msgid "Sign up" msgstr "Eintragen" -#: includes/view/ShiftCalendarShiftRenderer.php:199 msgid "ended" msgstr "vorbei" -# Wie ist dies zu verstehen bitte? -#: includes/view/ShiftCalendarShiftRenderer.php:204 msgid "please arrive for signup" msgstr "Ankommen zum Eintragen" -#: includes/view/ShiftCalendarShiftRenderer.php:208 msgid "not yet" msgstr "noch nicht" -#: includes/view/ShiftCalendarShiftRenderer.php:223 -#: includes/view/Shifts_view.php:89 -#, php-format msgid "Become %s" msgstr "Werde ein %s" -#: includes/view/ShiftCalenderRenderer.php:225 -#: includes/view/ShiftCalenderRenderer.php:232 msgid "m-d" msgstr "d.m." -#: includes/view/ShiftEntry_view.php:18 -#, php-format msgid "Do you want to sign off %s from shift %s from %s to %s as %s?" msgstr "MΓΆchtest Du %s von der Schicht %s von %s bis %s als %s austragen?" -#: includes/view/ShiftEntry_view.php:47 -#, php-format msgid "Do you want to sign off from your shift %s from %s to %s as %s?" msgstr "MΓΆchtest du dich von deiner Schicht %s von %s bis %s als %s austragen?" -#: includes/view/ShiftEntry_view.php:68 msgid "Shift sign off" msgstr "Von Schicht austragen" -#: includes/view/ShiftEntry_view.php:89 msgid "Do you want to sign up the following user for this shift?" msgstr "MΓΆchtest du den folgenden User fΓΌr die Schicht eintragen?" -#: includes/view/ShiftEntry_view.php:92 includes/view/ShiftEntry_view.php:117 -#: includes/view/UserAngelTypes_view.php:153 -msgid "User" -msgstr "Benutzer" - -#: includes/view/ShiftEntry_view.php:114 -#, php-format msgid "Do you want to sign up the following user for this shift as %s?" msgstr "MΓΆchtest du den folgenden User als %s in die Schicht eintragen?" -#: includes/view/ShiftEntry_view.php:138 -#, php-format msgid "Do you want to sign up for this shift as %s?" msgstr "MΓΆchtest du dich fΓΌr diese Schicht als %s eintragen?" -#: includes/view/ShiftEntry_view.php:140 includes/view/ShiftEntry_view.php:192 msgid "Comment (for your eyes only):" msgstr "Kommentar (nur fΓΌr Dich):" -#: includes/view/ShiftEntry_view.php:151 msgid "Shift signup" msgstr "Schicht Anmeldung" -#: includes/view/ShiftEntry_view.php:182 includes/view/User_view.php:390 -#: includes/view/User_view.php:392 msgid "Freeloaded" msgstr "GeschwΓ€nzt" -#: includes/view/ShiftEntry_view.php:185 msgid "Freeload comment (Only for shift coordination):" msgstr "SchwΓ€nzer Kommentar (Nur fΓΌr die Schicht-Koordination):" -#: includes/view/ShiftEntry_view.php:197 msgid "Edit shift entry" msgstr "Schichteintrag bearbeiten" -#: includes/view/ShiftEntry_view.php:200 msgid "Angel:" msgstr "Engel:" -#: includes/view/ShiftEntry_view.php:201 msgid "Date, Duration:" msgstr "Termin, Dauer:" -#: includes/view/ShiftEntry_view.php:202 -msgid "Location:" -msgstr "Ort:" - -#: includes/view/ShiftEntry_view.php:203 msgid "Title:" msgstr "Titel:" -#: includes/view/ShiftEntry_view.php:204 msgid "Type:" msgstr "Typ:" -#: includes/view/ShiftTypes_view.php:22 -#, php-format -msgid "Do you want to delete shifttype %s?" -msgstr "MΓΆchtest Du den Schichttypen %s lΓΆschen?" - -#: includes/view/ShiftTypes_view.php:54 -msgid "Edit shifttype" -msgstr "Schichttyp bearbeiten" - -#: includes/view/ShiftTypes_view.php:54 -msgid "Create shifttype" -msgstr "Schichttyp erstellen" - -#: includes/view/ShiftTypes_view.php:79 -#, php-format -msgid "for team %s" -msgstr "fΓΌr Team %s" - -#: includes/view/ShiftTypes_view.php:137 -msgid "New shifttype" -msgstr "Neuer Schichttyp" - -#: includes/view/Shifts_view.php:42 includes/view/User_view.php:570 -msgid "Location" -msgstr "Ort" - -#: includes/view/Shifts_view.php:57 -#, php-format msgid "created at %s by %s" msgstr "erstellt am %s von %s" -#: includes/view/Shifts_view.php:64 -#, php-format msgid "edited at %s by %s" msgstr "bearbeitet am %s von %s" -#: includes/view/Shifts_view.php:147 -#, php-format +msgid "History ID: %s" +msgstr "Historien-ID: %s" + msgid "This shift is in the far future and becomes available for signup at %s." msgstr "" "Diese Schicht liegt in der fernen Zukunft und du kannst dich ab %s eintragen." -#: includes/view/UserAngelTypes_view.php:18 -#, php-format msgid "Do you really want to add supporter rights for %s to %s?" msgstr "Sollen %s %s als neuen Supporter bekommen?" -#: includes/view/UserAngelTypes_view.php:19 -#, php-format msgid "Do you really want to remove supporter rights for %s from %s?" msgstr "MΓΆchtest Du wirklich %s von %s als Supporter befreien?" -#: includes/view/UserAngelTypes_view.php:29 -#: includes/view/UserAngelTypes_view.php:57 -#: includes/view/UserAngelTypes_view.php:75 -#: includes/view/UserAngelTypes_view.php:99 -#: includes/view/UserAngelTypes_view.php:123 -msgid "yes" -msgstr "Ja" - -#: includes/view/UserAngelTypes_view.php:47 -#, php-format msgid "Do you really want to deny all users for %s?" msgstr "MΓΆchtest Du wirklich alle Benutzer als %s ablehnen?" -#: includes/view/UserAngelTypes_view.php:71 -#, php-format msgid "Do you really want to confirm all users for %s?" msgstr "MΓΆchtest Du wirklich alle Benutzer als %s bestΓ€tigen?" -#: includes/view/UserAngelTypes_view.php:92 -#, php-format msgid "Do you really want to confirm %s for %s?" msgstr "MΓΆchtest Du wirklich %s fΓΌr %s bestΓ€tigen?" -#: includes/view/UserAngelTypes_view.php:116 -#, php-format msgid "Do you really want to delete %s from %s?" msgstr "MΓΆchtest Du wirklich %s von %s entfernen?" -#: includes/view/UserAngelTypes_view.php:169 -#, php-format msgid "Do you really want to add %s to %s?" msgstr "MΓΆchtest Du wirklich %s zu %s hinzufΓΌgen?" +msgid "Do you want to become a %2$s?" +msgstr "MΓΆchtest Du ein %2$s werden?" + msgid "Confirm user" msgstr "Benutzer bestΓ€tigen" msgid "Hide at Registration" msgstr "Ausblenden bei Registrierung" -#: includes/view/UserAngelTypes_view.php:176 -msgid "save" -msgstr "Speichern" - -#: includes/view/UserDriverLicenses_view.php:17 msgid "Back to profile" msgstr "ZurΓΌck zum Profil" -#: includes/view/UserDriverLicenses_view.php:21 -msgid "Privacy" -msgstr "PrivatsphΓ€re" - -#: includes/view/UserDriverLicenses_view.php:21 -msgid "" -"Your driving license information is only visible for supporters and admins." -msgstr "Deine FΓΌhrerschein-Infos sind nur fΓΌr Supporter und Admins sichtbar." - -#: includes/view/UserDriverLicenses_view.php:22 -msgid "I am willing to drive a car for the event" -msgstr "Ich mΓΆchte fΓΌr das Event Auto fahren" - -#: includes/view/UserDriverLicenses_view.php:27 -msgid "" -"I have my own car with me and am willing to use it for the event (You'll get " -"reimbursed for fuel)" -msgstr "" -"Ich habe mein eigenes Auto dabei und mΓΆchte es zum Fahren fΓΌr das Event " -"verwenden (Du wirst fΓΌr Spritkosten entschΓ€digt)" - -#: includes/view/UserDriverLicenses_view.php:30 -msgid "Driver license" -msgstr "FΓΌhrerschein" - -#: includes/view/UserDriverLicenses_view.php:34 -msgid "Transporter 3,5t" -msgstr "3,5t Transporter" - -#: includes/view/UserDriverLicenses_view.php:39 -msgid "Truck 7,5t" -msgstr "7,5t LKW" - -#: includes/view/UserDriverLicenses_view.php:44 -msgid "Truck 12t" -msgstr "12t LKW" - -#, php-format -msgid "Do you want to delete the worklog entry for %s?" -msgstr "MΓΆchtest du den Arbeitseinsatz von %s wirklich lΓΆschen?" - -#: includes/view/User_view.php:572 -msgid "Comment" -msgstr "Kommentar" - -msgid "Pronoun" -msgstr "Pronomen" - -msgid "Will be shown on your profile page and in angel lists." -msgstr "Wird auf deiner Profilseite und in Engellisten angezeigt." - -#: includes/view/User_view.php:37 -msgid "Here you can change your user details." -msgstr "Hier kannst Du Deine Details Γ€ndern." - -#: includes/view/User_view.php:55 -msgid "Planned date of departure" -msgstr "Geplanter Abreisetag" - -#: includes/view/User_view.php:83 -msgid "You can manage your Angeltypes on the Angeltypes page." -msgstr "Du kannst deine Engeltypen auf der Engeltypen-Seite verwalten." - -#: includes/view/User_view.php:124 msgid "" "Do you really want to delete the user including all his shifts and every " "other piece of his data?" @@ -2452,144 +1136,84 @@ msgstr "" "MΓΆchtest Du wirklich den Engel inklusive aller seiner Schichten und allen " "anderen seiner Daten lΓΆschen?" -#: includes/view/User_view.php:128 msgid "Your password" msgstr "Dein Passwort" -#: includes/view/User_view.php:148 -#, php-format msgid "Angel can receive another %d vouchers." msgstr "Engel kann noch %d Gutscheine bekommen." -#: includes/view/User_view.php:148 -#, php-format msgid "Angel can receive another %d vouchers and is FA." msgstr "Engel kann noch %d Gutscheine bekommen und ist FA." -#: includes/view/User_view.php:153 msgid "Number of vouchers given out" msgstr "Anzahl Gutscheine bekommen" -#: includes/view/User_view.php:221 -msgid "Prename" -msgstr "Vorname" - -#: includes/view/User_view.php:227 includes/view/User_view.php:697 -msgid "Arrived" -msgstr "Angekommen" - -#: includes/view/User_view.php:228 msgid "Voucher" msgstr "Gutschein" -#: includes/view/User_view.php:229 msgid "Freeloads" msgstr "SchwΓ€nzereien" -#: includes/view/User_view.php:230 includes/view/User_view.php:734 -msgid "Active" -msgstr "Aktiv" - -#: includes/view/User_view.php:232 includes/view/User_view.php:737 -msgid "T-Shirt" +msgid "T-shirt" msgstr "T-Shirt" -#: includes/view/User_view.php:244 msgid "Last login" msgstr "Letzter Login" -#: includes/view/User_view.php:250 -msgid "New user" -msgstr "Neuer User" - -#: includes/view/User_view.php:283 -msgid "Free" -msgstr "Frei" - -#: includes/view/User_view.php:291 includes/view/User_view.php:295 -#, php-format msgid "Next shift %c" msgstr "NΓ€chste Schicht %c" -#: includes/view/User_view.php:302 -#, php-format msgid "Shift started %c" msgstr "Schicht startete %c" -#: includes/view/User_view.php:307 -#, php-format msgid "Shift ends %c" msgstr "Schicht endet %c" -#: includes/view/User_view.php:324 -#, php-format msgid "Shift ended %c" msgstr "Schicht endete %c" -#: includes/view/User_view.php:409 -msgid "sign off" -msgstr "austragen" +msgid "Sign off" +msgstr "Austragen" -#: includes/view/User_view.php:461 msgid "Sum:" msgstr "Summe:" -#: includes/view/User_view.php:470 -msgid "Your t-shirt score" -msgstr "Dein T-Shirt Score" +msgid "T-shirt score" +msgstr "T-Shirt Score" -#: includes/view/User_view.php:511 msgid "Work log entry" msgstr "Arbeitseinsatz" -#: includes/view/User_view.php:514 -#, php-format msgid "Added by %s at %s" msgstr "Erstellt von %s am %s" -#: includes/view/User_view.php:568 -msgid "Day & time" -msgstr "Tag & Zeit" +msgid "Day & Time" +msgstr "Tag & Zeit" -#: includes/view/User_view.php:569 msgid "Duration" msgstr "Dauer" -#: includes/view/User_view.php:571 -msgid "Name & workmates" -msgstr "Name & Kollegen" - -#: includes/view/User_view.php:573 -msgid "Action" -msgstr "Aktion" +msgid "Name & Workmates" +msgstr "Name & Kollegen" -#: includes/view/User_view.php:576 msgid "You have done enough." msgstr "Du hast genug gemacht." -#: includes/view/User_view.php:595 -msgid "driving license" -msgstr "FΓΌhrerschein" +msgid "%s has done enough." +msgstr "%s hat genug gemacht." -#: includes/view/User_view.php:608 msgid "Vouchers" msgstr "Gutscheine" -#: includes/view/User_view.php:620 msgid "iCal Export" msgstr "iCal Export" -#: includes/view/User_view.php:624 msgid "JSON Export" msgstr "JSON Export" -#: includes/view/User_view.php:645 -#, php-format -msgid "Your night shifts between %d and %d am count twice." -msgstr "Deine Nachtschichten zwischen %d und %d Uhr zΓ€hlen doppelt." +msgid "Your night shifts between %d and %d am count twice for the %s score." +msgstr "Deine Nachtschichten zwischen %d und %d Uhr zΓ€hlen fΓΌr den %s Score doppelt." -#: includes/view/User_view.php:653 -#, php-format msgid "" "Go to the shifts table to sign yourself up for some " "shifts." @@ -2597,53 +1221,40 @@ msgstr "" "Gehe zur Schicht-Tabelle, um Dich fΓΌr Schichten " "einzutragen." -#: includes/view/User_view.php:679 -msgid "User state" -msgstr "Engelzustand" +msgid "State" +msgstr "Status" -#: includes/view/User_view.php:699 msgid "Not arrived" msgstr "Nicht angekommen" -#: includes/view/User_view.php:718 msgid "Freeloader" msgstr "SchwΓ€nzer" -#: includes/view/User_view.php:726 -#, php-format msgid "Arrived at %s" msgstr "Angekommen am %s" -#: includes/view/User_view.php:732 -msgid "Active (forced)" -msgstr "Aktiv (erzwungen)" - -#: includes/view/User_view.php:743 -#, php-format msgid "Not arrived (Planned: %s)" msgstr "Nicht angekommen (Geplant: %s)" -#: includes/view/User_view.php:753 -#, php-format msgid "Got %s of %s vouchers" msgstr "%s von %s Gutscheinen bekommen" -#: includes/view/User_view.php:756 msgid "Got no vouchers" msgstr "Keine Gutscheine bekommen" msgid "out of %s" msgstr "von %s" -#: includes/view/User_view.php:89 -msgid "Show mobile number to other users to contact me" -msgstr "Mache meine Handynummer fΓΌr andere Benutzer sichtbar" - -#: includes/view/User_view.php:797 msgid "Rights" -msgstr "Rechte" +msgstr "Berechtigungen" + +msgid "" +"You are not marked as arrived. Please go to heaven, get your angel " +"badge and/or tell them that you arrived already." +msgstr "" +"Du bist nicht als angekommen markiert. Geh bitte zum Himmel, " +"hole Dein Badge ab und/oder melde dich als angekommen." -#: includes/view/User_view.php:853 msgid "" "Please enter your planned date of departure on your settings page to give us " "a feeling for teardown capacities." @@ -2651,8 +1262,6 @@ msgstr "" "Bitte gib Dein geplantes Abreisedatum an, damit wir ein GefΓΌhl fΓΌr die Abbau-" "Planung bekommen." -#: includes/view/User_view.php:867 -#, php-format msgid "" "You freeloaded at least %s shifts. Shift signup is locked. Please go to " "heavens desk to be unlocked again." @@ -2660,25 +1269,25 @@ msgstr "" "Du hast mindestens %s Schichten geschwΓ€nzt. Schicht-Registrierung ist " "gesperrt. Bitte gehe zum Himmelsschreibtisch um wieder entsperrt zu werden." -#: includes/view/User_view.php:886 -msgid "" -"You are not marked as arrived. Please go to heaven's desk, get your angel " -"badge and/or tell them that you arrived already." +msgid "tshirt.required.hint" +msgstr "Bitte gib eine T-Shirt-Grâße in deinen Einstellungen an." + +msgid "dect.required.hint" msgstr "" -"Du bist nicht als angekommen markiert. Bitte gehe zur Himmelsverwaltung, " -"hole Dein Badge ab und/oder erklΓ€re ihnen, dass Du bereits angekommen bist." +"Bitte gib eine DECT-Telefonnummer in deinen Einstellungen an. Wenn du " +"keine Nummer hast, trage einfach '-' ein." -#: includes/view/User_view.php:899 -msgid "You need to specify a tshirt size in your settings!" -msgstr "Bitte eine T-Shirt-Grâße auswΓ€hlen" +msgid "pronoun.required.hint" +msgstr "Bitte gib ein Pronomen in deinen Einstellungen an." -#: includes/view/User_view.php:913 -msgid "" -"You need to specify a DECT phone number in your settings! If you don't have " -"a DECT phone, just enter '-'." -msgstr "" -"Bitte eine DECT-Telefonnummer in den Einstellungen eingeben. Wenn du noch " -"keine Nummer hast, bitte einfach \"-\" angeben." +msgid "firstname.required.hint" +msgstr "Bitte gib einen Vornamen in deinen Einstellungen an." + +msgid "lastname.required.hint" +msgstr "Bitte gib einen Nachnamen in deinen Einstellungen an." + +msgid "mobile.required.hint" +msgstr "Bitte gib eine Handynummer in deinen Einstellungen an." msgid "" "Here you can change the user entry. Under the item 'Arrived' the angel is marked as present, " @@ -2693,16 +1302,6 @@ msgstr "" "Ist der Engel Aktiv, hat er damit Anspruch auf ein T-Shirt. Wenn T-Shirt ein 'Ja' enthΓ€lt, bedeutet dies, " "dass der Engel bereits sein T-Shirt erhalten hat." -msgid "" -"If the angel was Active, this entitles him to a T-shirt. If T-shirt contains a 'Ja', " -"it means that the angel has already received his T-shirt." -msgstr "" -"War der Engel Aktiv, hat er damit Anspruch auf ein T-Shirt. Wenn T-Shirt ein 'Ja' enthΓ€lt, " -"bedeutet dies, dass der Engel bereits sein T-Shirt erhalten hat." - -msgid "Please visit the angeltypes page or the users profile to manage the users angeltypes." -msgstr "Bitte besuche die Seite Engeltypen oder das Benutzerprofil, um die Engeltypen des Benutzers zu verwalten." - msgid "Here you can reset the password of this angel:" msgstr "Hier kannst du das Passwort fΓΌr diesen Engel zurΓΌcksetzen:" @@ -2718,7 +1317,7 @@ msgstr "Du kannst keine Engel mit mehr Rechten bearbeiten." msgid "You cannot edit your own rights." msgstr "Du kannst deine eigenen Rechte nicht bearbeiten." -msgid "Changes where saved." +msgid "Changes were saved." msgstr "Γ„nderung wurde gespeichert." msgid "Password reset done." @@ -2730,6 +1329,9 @@ msgstr "Die EintrΓ€ge mΓΌssen ΓΌbereinstimmen und dΓΌrfen nicht leer sein!" msgid "Number of shifts: %s" msgstr "Anzahl an Schichten: %s" +msgid "Shifts created." +msgstr "Schichten erstellt." + msgid "If the angel is active, it can claim a goodie. If goodie is set to 'Yes', the angel already got their goodie." msgstr "Ist der Engel Aktiv, hat er damit Anspruch auf ein Goodie. Wenn Goodie ein 'Ja' enthΓ€lt, bedeutet dies, " "dass der Engel bereits sein Goodie erhalten hat." @@ -2737,8 +1339,11 @@ msgstr "Ist der Engel Aktiv, hat er damit Anspruch auf ein Goodie. Wenn Goodie e msgid "Goodie" msgstr "Goodie" -msgid "Your goodie score" -msgstr "Dein Goodie Score" +msgid "goodie" +msgstr "Goodie" + +msgid "Goodie score" +msgstr "Goodie Score" msgid "Given goodies" msgstr "Ausgegebene Goodies" @@ -2746,10 +1351,10 @@ msgstr "Ausgegebene Goodies" msgid "Goodie statistic" msgstr "Goodie Statistik" -msgid "remove goodie" -msgstr "entferne Goodie" +msgid "Remove goodie" +msgstr "Goodie entfernen" -msgid "got goodie" +msgid "Got goodie" msgstr "Goodie bekommen" msgid "Goodie?" @@ -2761,32 +1366,13 @@ msgstr "Engel hat ein Goodie bekommen." msgid "Angel has got no goodie." msgstr "Engel hat kein Goodie bekommen." -#: src/Middleware/LegacyMiddleware.php:83 msgid "page.404.text" msgstr "" "Diese Seite existiert nicht oder Du hast keinen Zugriff. Melde Dich an um " "Zugriff zu erhalten!" -#~ msgid "Token is not correct." -#~ msgstr "Der Token ist nicht in Ordnung." - -#~ msgid "Needed shirts" -#~ msgstr "BenΓΆtigte T-Shirts" - -#~ msgid "Only confirmed" -#~ msgstr "Nur bestΓ€tigte" - -#~ msgid "Registration successful" -#~ msgstr "Registrierung erfolgreich" - -#~ msgid "Please enter a new password." -#~ msgstr "Gib bitte ein neues Passwort ein." - -#~ msgid "auth.no-password" -#~ msgstr "Gib bitte ein Passwort ein." - -#~ msgid "auth.no-nickname" -#~ msgstr "Gib bitte einen Nick an." +msgid "form.select_placeholder" +msgstr "Bitte auswΓ€hlen..." msgid "form.load_schedule" msgstr "Programm laden" @@ -2806,6 +1392,9 @@ msgstr "Vorschau" msgid "form.delete" msgstr "LΓΆschen" +msgid "form.delete_all" +msgstr "Alle lΓΆschen" + msgid "form.updated" msgstr "Aktualisiert" @@ -2818,12 +1407,18 @@ msgstr "Du kannst hier Markdown verwenden" msgid "form.required" msgstr "Pflichtfeld" +msgid "form.user_select" +msgstr "WΓ€hle einen User" + msgid "schedule.import" msgstr "Programm importieren" msgid "schedule.edit.title" msgstr "Programm bearbeiten" +msgid "schedule.delete.title" +msgstr "Programm mit %u Schichten lΓΆschen" + msgid "schedule.import.title" msgstr "Programm importieren" @@ -2832,7 +1427,7 @@ msgstr "Aktualisiert am: %s" msgid "schedule.import.text" msgstr "" -"Importe erstellen RΓ€ume und erstellen, aktualisieren und lΓΆschen Schichten anhand eines schedule.xml exportes." +"Importe erstellen Orte und erstellen, aktualisieren und lΓΆschen Schichten anhand eines schedule.xml exportes." msgid "schedule.import.load.title" msgstr "Programm importieren: Vorschau" @@ -2849,14 +1444,20 @@ msgstr "Programm URL (schedule.xml)" msgid "schedule.shift-type" msgstr "Schichttyp" +msgid "schedule.needed-from-shift-type" +msgstr "Engeltypen vom Schichttyp laden (sonst vom Ort)" + msgid "schedule.minutes-before" msgstr "Minuten vor Talk beginn hinzufΓΌgen" msgid "schedule.minutes-after" msgstr "Minuten nach Talk ende hinzufΓΌgen" -msgid "schedule.import.rooms.add" -msgstr "Neue RΓ€ume" +msgid "schedule.for_locations" +msgstr "FΓΌr Orte" + +msgid "schedule.import.locations.add" +msgstr "Neue Orte" msgid "schedule.import.shifts.add" msgstr "Neue Schichten" @@ -2867,23 +1468,14 @@ msgstr "Zu aktualisierende Schichten" msgid "schedule.import.shifts.delete" msgstr "Zu lΓΆschende Schichten" -msgid "schedule.import.rooms.name" -msgstr "Name" - msgid "schedule.import.shift.dates" msgstr "Zeit" msgid "schedule.import.shift.type" msgstr "Typ" -msgid "schedule.import.shift.title" -msgstr "Titel" - -msgid "schedule.import.shift.room" -msgstr "Raum" - -msgid "shifts_history.schedule" -msgstr "Programm: %s" +msgid "schedule.import.shift.location" +msgstr "Ort" msgid "news.title" msgstr "News" @@ -2897,8 +1489,8 @@ msgstr "+" msgid "news.is_meeting" msgstr "[Treffen]" -msgid "news.edit.is_important" -msgstr "Wichtig" +msgid "news.edit.is_highlighted" +msgstr "Hervorgehoben" msgid "news.read_more" msgstr "Weiterlesen" @@ -2936,12 +1528,27 @@ msgstr "Nachricht" msgid "news.edit.hint" msgstr "Du kannst Markdown und den [more] Tag benutzen" +msgid "news.delete.title" +msgstr "News \"%s\" lΓΆschen" + +msgid "news.comments.delete.title" +msgstr "Kommentar \"%s\" lΓΆschen" + +msgid "notification.news.updated.introduction" +msgstr "Die News %1$s wurde aktualisiert" + +msgid "notification.news.updated.text" +msgstr "Du kannst sie dir unter %3$s anschauen." + msgid "form.search" msgstr "Suchen" msgid "log.log" msgstr "Logs" +msgid "log.only_own" +msgstr "Du siehst hier deine eigenen Logs. Die logs anderer User kΓΆnnen nur BΓΌrokraten sehen." + msgid "log.time" msgstr "Zeit" @@ -2954,15 +1561,15 @@ msgstr "Nachricht" msgid "settings.settings" msgstr "Einstellungen" -msgid "settings.profile.user_details.info" -msgstr "Hier kannst Du Deine Details Γ€ndern." - msgid "settings.profile.entry_required" -msgstr "Pflichtfeld!" +msgstr "Pflichtfeld" -msgid "settings.profile.nick" +msgid "general.nick" msgstr "Nick" +msgid "settings.profile.nick.already-taken" +msgstr "Der Nick ist bereits vergeben." + msgid "settings.profile.pronoun" msgstr "Pronomen" @@ -2981,18 +1588,21 @@ msgstr "Geplanter Ankunftstag" msgid "settings.profile.planned_departure_date" msgstr "Geplanter Abreisetag" -msgid "settings.profile.dect" -msgstr "DECT" - msgid "settings.profile.mobile" msgstr "Handy" msgid "settings.profile.mobile_show" msgstr "Mache meine Handynummer fΓΌr andere Benutzer sichtbar." -msgid "settings.profile.email" +msgid "settings.profile.email-preferences" +msgstr "E-Mail Einstellungen" + +msgid "general.email" msgstr "E-Mail" +msgid "settings.profile.email.already-taken" +msgstr "Diese E-Mail-Adresse ist bereits vergeben." + msgid "settings.profile.email_shiftinfo" msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten Γ€ndern)." @@ -3003,11 +1613,11 @@ msgid "settings.profile.email_messages" msgstr "Benachrichtige mich bei neuen privaten Nachrichten." msgid "settings.profile.email_by_human_allowed" -msgstr "Erlaube Himmel-Engeln dich per Mail zu kontaktieren." +msgstr "Erlaube Himmel-Engeln mich per E-Mail zu kontaktieren." msgid "settings.profile.email_goody" -msgstr "Um Voucher zu erhalten, stimme zu, dass Nick, E-Mail-Adresse, geleistete Arbeit und Shirtgrâße " -"bis zum nΓ€chsten gleichartigen Event gespeichert werden." +msgstr "Um gegebenenfalls Voucher fΓΌr das nΓ€chste gleichartige Event zu erhalten stimme ich zu, " +"dass mein Nick, E-Mail-Adresse, geleistete Arbeit und T-Shirt-Grâße solange gespeichert werden." msgid "settings.profile.privacy" msgstr "Dies kann jederzeit durch eine E-Mail an %1$s widerrufen werden." @@ -3024,6 +1634,9 @@ msgstr "Passwort" msgid "settings.password.info" msgstr "Hier kannst Du Dein Passwort Γ€ndern." +msgid "settings.password.confirmation-does-not-match" +msgstr "Passwort und Passwortwiederholung stimmen nicht ΓΌberein." + msgid "settings.password.password" msgstr "Altes Passwort" @@ -3036,6 +1649,21 @@ msgstr "Passwort wiederholen" msgid "settings.password.success" msgstr "Passwort wurde erfolgreich geΓ€ndert." +msgid "settings.sessions" +msgstr "Sitzungen" + +msgid "settings.sessions.info" +msgstr "Hier kannst Du deine Bowser-Sitzungen sehen und lΓΆschen." + +msgid "settings.sessions.current" +msgstr "Aktuelle Sitzung" + +msgid "settings.sessions.id" +msgstr "Sitzungs-ID" + +msgid "settings.sessions.last_activity" +msgstr "Zuletzt verwendet" + msgid "settings.theme" msgstr "Theme" @@ -3045,6 +1673,79 @@ msgstr "Hier kannst Du Dein Theme Γ€ndern." msgid "settings.theme.success" msgstr "Theme wurde erfolgreich geΓ€ndert." +msgid "settings.certificates" +msgstr "Zertifikate" + +msgid "settings.certificates.info" +msgstr "Diese Informationen sind nur fΓΌr Supporter und Admins sichtbar." + +msgid "settings.certificates.title.ifsg" +msgstr "Gesundheitsbelehrungen" + +msgid "settings.certificates.driving_license" +msgstr "FΓΌhrerschein" + +msgid "settings.certificates.has_car" +msgstr "" +"Ich habe mein eigenes Auto dabei und mΓΆchte es zum Fahren fΓΌr das Event " +"verwenden (Du wirst fΓΌr Spritkosten entschΓ€digt)" + +msgid "settings.certificates.drive_car" +msgstr "Auto" + +msgid "settings.certificates.drive_3_5t" +msgstr "3,5t Transporter" + +msgid "settings.certificates.drive_7_5t" +msgstr "7,5t LKW" + +msgid "settings.certificates.drive_12t" +msgstr "12t LKW" + +msgid "settings.certificates.drive_forklift" +msgstr "Gabelstapler" + +msgid "settings.certificates.ifsg_light" +msgstr "Ich wurde vor Ort nach IfSG Β§43 (Frikadellendiplom light) belehrt." + +msgid "settings.certificates.ifsg" +msgstr "Ich habe eine Belehrung nach Β§43 IfSG (Frikadellendiplom) bei meinem Gesundheitsamt " +"erhalten und innerhalb von 3 Monaten die Zweitbelehrung durch uns oder meinen Arbeitgeber/Koch/Verein bekommen. " +"ZusΓ€tzlich ist die Zweitbelehrung nicht Γ€lter als zwei Jahre." + +msgid "settings.certificates.success" +msgstr "Zertifikate wurden erfolgreich aktualisiert." + +msgid "angeltype.ifsg.required" +msgstr "BenΓΆtigt eine Gesundheitsbelehrung" + +msgid "ifsg.certificate" +msgstr "Gesundheitsbelehrung" + +msgid "ifsg.certificate_light" +msgstr "Gesundheitsbelehrung vor Ort" + +msgid "angeltype.ifsg.own" +msgstr "Meine Gesundheitsbelehrung" + +msgid "angeltype.ifsg.required.info" +msgstr "Dieser Engeltyp benΓΆtigt eine Gesundheitsbelehrung. Bitte trage deine Gesundheitsbelehrung ein!" + +msgid "angeltype.ifsg.required.info.here" +msgstr "Dieser Engeltyp benΓΆtigt eine Gesundheitsbelehrung. " +"Bitte trage deine Gesundheitsbelehrung hier ein: %s" + +msgid "angeltype.driving_license.required.info.here" +msgstr "" +"Du bist einem Engeltypen beigetreten, der FΓΌhrerschein-Infos benΓΆtigt. " +"Bitte trage Deine FΓΌhrerschein-Infos hier ein: %s." + +msgid "ifsg.info" +msgstr "Gesundheitsbelehrungs-Infos" + +msgid "driving_license.info" +msgstr "FΓΌhrerschein-Infos" + msgid "settings.language" msgstr "Sprache" @@ -3054,6 +1755,35 @@ msgstr "Hier kannst Du Deine Sprache Γ€ndern." msgid "settings.language.success" msgstr "Sprache wurde erfolgreich geΓ€ndert." +msgid "settings.api" +msgstr "API" + +msgid "settings.api.about" +msgstr "" +"Die API erlaubt es dir, ΓΌber externe Programme, mit dem Engelsystem zu interagieren. " +"Sie ist noch nicht vollstΓ€ndig, wir arbeiten aber daran sie zu erweitern.\n" +"Der API Einstiegspunkt befindet sich unter `%s` und ist in der [OpenAPI Spezifikation](%s) beschrieben.\n" +"Teile deinen persΓΆnlichen API Key mit niemandem, er erlaubt es deine persΓΆnlichen Daten einzusehen " +"und Γ„nderungen in deinem Namen durch zu fΓΌhren!" + +msgid "settings.api.shifts_json_show" +msgstr "JSON Schichten Export anzeigen" + +msgid "settings.api.ical_show" +msgstr "iCal export anzeigen" + +msgid "settings.api.news_show" +msgstr "News feeds anzeigen" + +msgid "settings.api.key_show" +msgstr "API Key anzeigen" + +msgid "settings.api.key_reset" +msgstr "API Key zurΓΌcksetzen" + +msgid "settings.api.key_reset_confirm" +msgstr "Wenn du den API Key zurΓΌcksetzt, musst ihn in allen deinen Anwendungen aktualisieren." + msgid "settings.oauth" msgstr "Single Sign-On" @@ -3090,6 +1820,9 @@ msgstr "Frage" msgid "faq.message" msgstr "Antwort" +msgid "faq.delete.title" +msgstr "FAQ \"%s\" lΓΆschen" + msgid "question.questions" msgstr "Fragen" @@ -3108,17 +1841,20 @@ msgstr "Frage" msgid "question.answer" msgstr "Antwort" +msgid "question.delete.title" +msgstr "Frage \"%s\" lΓΆschen" + msgid "user.edit.shirt" -msgstr "Shirt bearbeiten" +msgstr "T-Shirt bearbeiten" msgid "user.edit.goodie" msgstr "Goodie bearbeiten" msgid "form.shirt" -msgstr "Shirt" +msgstr "T-Shirt" msgid "user.shirt_size" -msgstr "Shirt grâße" +msgstr "T-Shirt grâße" msgid "user.active" msgstr "Aktiv" @@ -3129,8 +1865,11 @@ msgstr "Aktiv (erzwungen)" msgid "user.arrived" msgstr "Angekommen" +msgid "user.arrive" +msgstr "Ankommen" + msgid "user.got_shirt" -msgstr "Shirt bekommen" +msgstr "T-Shirt bekommen" msgid "user.got_goodie" msgstr "Goodie bekommen" @@ -3147,12 +1886,9 @@ msgstr "Zur Konversation" msgid "message.message" msgstr "Nachricht" -msgid "angel" +msgid "general.angel" msgstr "Engel" -msgid "date" -msgstr "Datum" - msgid "worklog.add" msgstr "Arbeitseinsatz hinzufΓΌgen" @@ -3187,41 +1923,144 @@ msgid "angeltypes.restricted.hint" msgstr "Dieser Engeltyp benΓΆtigt eine Einweisung bei einem EinfΓΌhrungstreffen. " "Weitere Informationen findest du mΓΆglicherweise in der Beschreibung." -msgid "angeltypes.name" -msgstr "Name" - -msgid "angeltypes.dect" -msgstr "DECT" +msgid "angeltypes.can-change-later" +msgstr "Du kannst Deine Auswahl spΓ€ter in den Einstellungen Γ€ndern." msgid "angeltypes.email" msgstr "E-Mail" +msgid "angeltypes.hide_on_shift_view" +msgstr "Auf Schicht-Ansicht ausblenden" + +msgid "angeltypes.hide_on_shift_view.info" +msgstr "Wenn ausgewΓ€hlt, kΓΆnnen nur Admins und Mitglieder des Engeltyps auf der " +"Schicht Seite die Filteroption fΓΌr diesen Engeltyp sehen." + +msgid "location.location" +msgstr "Ort" + +msgid "location.locations" +msgstr "Orte" + +msgid "location.map_url" +msgstr "Karte" + +msgid "location.required_angels" +msgstr "BenΓΆtigte Engel (bei Fahrplan import)" + +msgid "location.map_url.info" +msgstr "Die Karte wird auf der Ort-Seite als iframe eingebettet." + +msgid "location.create.title" +msgstr "Ort erstellen" + +msgid "location.edit.title" +msgstr "Ort bearbeiten" + +msgid "location.delete.title" +msgstr "Ort \"%s\" lΓΆschen" + +msgid "shifttype.shifttypes" +msgstr "Schichttypen" + +msgid "shifttype.edit.title" +msgstr "Schichttyp bearbeiten" + +msgid "shifttype.create.title" +msgstr "Schichttyp erstellen" + +msgid "shifttype.delete.title" +msgstr "Schichttyp \"%s\" lΓΆschen" + +msgid "shifttype.required_angels" +msgstr "BenΓΆtigte Engel (bei Fahrplan import)" + +msgid "event.day" +msgstr "Tag %1$d" + +msgid "dashboard.day" +msgstr "Tag" + +msgid "registration.title" +msgstr "Engel-Registrierung" + +msgid "registration.login_data" +msgstr "Anmeldedaten" + +msgid "registration.event_data" +msgstr "Eventdaten" + +msgid "registration.what_todo" +msgstr "Was mΓΆchtest Du machen?" + msgid "registration.register" msgstr "Registrieren" -msgid "room.rooms" -msgstr "RΓ€ume" +msgid "confirmation.delete" +msgstr "MΓΆchtest du es wirklich lΓΆschen?" -msgid "room.name" -msgstr "Name" +msgid "general.id" +msgstr "ID" -msgid "room.dect" -msgstr "DECT" +msgid "general.user" +msgstr "Benutzer" -msgid "room.map_url" -msgstr "Karte" +msgid "general.count" +msgstr "Anzahl" -msgid "room.description" -msgstr "Beschreibung" +msgid "general.created_at" +msgstr "Erstellt am" -msgid "room.required_angels" -msgstr "BenΓΆtigte Engel" +msgid "shifts.history" +msgstr "Schichten Historie" + +msgid "shifts.history.schedule" +msgstr "Programm: %s" + +msgid "shifts.history.delete_all.title" +msgstr "%u Schichten lΓΆschen" -msgid "room.map_url.info" -msgstr "Die Karte wird auf der Raum-Seite als iframe eingebettet." +msgid "shifts.start" +msgstr "Start" -msgid "room.create.title" -msgstr "Raum erstellen" +msgid "shifts.end" +msgstr "Ende" + +msgid "user.info" +msgstr "Benutzer Info" + +msgid "user.info.hint" +msgstr "" +"Wird auf der Benutzer Seite fΓΌr Schichtkoordinatoren und Admins angezeigt. " +"Wenn ein Engel nicht als angekommen markiert ist, " +"entfernt eine Benutzer Info die nicht-angekommen Nachricht im SystemmenΓΌ des Engels." + +msgid "user_info.not_arrived_hint" +msgstr "Wenn du dich fΓΌr Schichten eintragen willst, komm gerne im Himmel vorbei." + +msgid "design.title" +msgstr "Design" + +msgid "notification.shift.updated.introduction" +msgstr "Eine deiner Schichten wurde aktualisiert:" -msgid "room.edit.title" -msgstr "Raum bearbeiten" +msgid "notification.shift.updated.type" +msgstr "Schichttyp wurde von %s in %s geΓ€ndert" + +msgid "notification.shift.updated.title" +msgstr "Schicht Titel wurde von %s in %s geΓ€ndert" + +msgid "notification.shift.updated.description" +msgstr "Schicht Beschreibung wurde geΓ€ndert" + +msgid "notification.shift.updated.start" +msgstr "Schicht Start wurde von %s zu %s geΓ€ndert" + +msgid "notification.shift.updated.end" +msgstr "Schicht Ende wurde von %s zu %s geΓ€ndert" + +msgid "notification.shift.updated.location" +msgstr "Schicht Ort wurde von %s nach %s verschoben" + +msgid "notification.shift.updated.shift" +msgstr "Die aktualisierte Schicht:" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index 6d3f1864e..441b94ffb 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -15,19 +15,42 @@ msgstr "No user was found or password is wrong. Please try again. If you are sti msgid "validation.password.required" msgstr "The password is required." +msgid "validation.password.length" +msgstr "The password entered is too short." + msgid "validation.login.required" msgstr "The login name is required." +msgid "validation.pronoun.required" +msgstr "Please enter your pronoun." + +msgid "validation.firstname.required" +msgstr "Please enter your first name." + +msgid "validation.lastname.required" +msgstr "Please enter your last name." + +msgid "validation.mobile.required" +msgstr "Please enter your mobile number." + +msgid "validation.dect.required" +msgstr "Please enter your DECT number." + +msgid "validation.username.required" +msgstr "Please enter your nick." + +msgid "validation.username.username" +msgstr "" +"Please enter a valid nick:" +"Use up to 24 letters, numbers or connecting punctuations (.-_) for your nickname." + msgid "validation.email.required" -msgstr "The email address is required." +msgstr "The e-mail address is required." msgid "validation.email.email" -msgstr "This email address is not valid." - -msgid "validation.password.min" -msgstr "Your new password is too short." +msgstr "This e-mail address is not valid." -msgid "validation.new_password.min" +msgid "validation.new_password.length" msgstr "Your new password is too short." msgid "validation.password.confirmed" @@ -36,6 +59,21 @@ msgstr "Your passwords are not equal." msgid "validation.password_confirmation.required" msgstr "You have to confirm your password." +msgid "validation.tshirt_size.required" +msgstr "Please choose your T-shirt size." + +msgid "validation.tshirt_size.shirtSize" +msgstr "Please choose a valid T-shirt size." + +msgid "validation.planned_arrival_date.required" +msgstr "Please enter your planned date of arrival." + +msgid "validation.planned_arrival_date.min" +msgstr "The planned date of arrival must not be before the buil-up start date." + +msgid "validation.planned_arrival_date.between" +msgstr "The planned date of arrival must be between the build-up and tear-down date." + msgid "schedule.edit.success" msgstr "The schedule was configured successfully." @@ -51,6 +89,9 @@ msgstr "The shift type can't not be found." msgid "schedule.import.success" msgstr "Schedule import successful." +msgid "schedule.delete.success" +msgstr "Schedule deletion successful." + msgid "shifts.filter.toggle" msgstr "collapse/show filters" @@ -78,6 +119,9 @@ msgstr "Comment saved." msgid "news.comment-delete.success" msgstr "Comment successfully deleted." +msgid "news.edit.duplicate" +msgstr "This news has already been created." + msgid "news.edit.success" msgstr "News successfully updated." @@ -127,6 +171,9 @@ msgid "oauth.temporarily_unavailable" msgstr "The OAuth-Provider is currently unable to handle the request " "due to a temporary overloading or maintenance of the server." +msgid "profile.my-shifts" +msgstr "My shifts" + msgid "settings.profile" msgstr "Profile" @@ -138,9 +185,15 @@ msgid "settings.profile.planned_departure_date.invalid" msgstr "Please enter your planned date of departure. " "It should be after your planned arrival date and after buildup start date and before teardown end date." -msgid "settings.profile.success" +msgid "settings.success" msgstr "Settings saved." +msgid "settings.sessions.delete_success" +msgstr "Session deleted successfully." + +msgid "settings.api.key_reset_success" +msgstr "API key successfully reset." + msgid "faq.delete.success" msgstr "FAQ entry successfully deleted." @@ -163,7 +216,10 @@ msgid "notification.news.new.introduction" msgstr "A new news is available: %1$s" msgid "notification.news.new.text" -msgstr "You can watch it at %3$s" +msgstr "You can view it at %3$s" + +msgid "notification.news.updated" +msgstr "Updated News: %s" msgid "notification.messages.new" msgstr "New private message from %s" @@ -200,6 +256,12 @@ msgstr "" "Since the deleted shift was already done, " "we added a worklog entry instead, to keep your work hours correct." +msgid "notification.shift.updated" +msgstr "Your shift was updated" + +msgid "notification.shift.no_next_found" +msgstr "There is no available shift." + msgid "user.edit.success" msgstr "User edited successfully." @@ -215,11 +277,26 @@ msgstr "Work log successfully updated." msgid "worklog.delete.success" msgstr "Work log successfully deleted." -msgid "room.edit.success" -msgstr "Room edited successfully." +msgid "location.edit.success" +msgstr "Location edited successfully." + +msgid "location.delete.success" +msgstr "Location successfully deleted." + +msgid "shifttype.edit.success" +msgstr "Shift type edited successfully." -msgid "room.delete.success" -msgstr "Room successfully deleted." +msgid "shifttype.delete.success" +msgstr "Shift type successfully deleted." msgid "validation.name.exists" msgstr "The name is already used." + +msgid "registration.disabled" +msgstr "The registration is disabled." + +msgid "registration.successful" +msgstr "Registration successful. You can now log in!" + +msgid "shifts.history.delete.success" +msgstr "Shifts deleted successfully." diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 1683b2d24..378317afe 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -16,29 +16,59 @@ msgstr "" "Language: en_US\n" "X-Poedit-SearchPath-0: .\n" -#~ msgid "auth.no-nickname" -#~ msgstr "Please enter a nickname." - -#~ msgid "auth.no-password" -#~ msgstr "Please enter a password." - msgid "auth.password.error" msgstr "Your password is incorrect. Please try it again." msgid "form.submit" msgstr "Submit" -msgid "login.login" +msgid "form.send_notification" +msgstr "Send notifications" + +msgid "general.login" msgstr "Login" +msgid "general.dect" +msgstr "DECT" + +msgid "general.name" +msgstr "Name" + +msgid "general.description" +msgstr "Description" + +msgid "page.403.title" +msgstr "Forbidden" + +msgid "page.403.headline" +msgstr "You are not allowed to access this page" + msgid "page.403.login" msgstr "Please log in." +msgid "page.error.title" +msgstr "Error %s" + +msgid "page.404.title" +msgstr "Page not found" + +msgid "page.404.not_found" +msgstr "No sleep found" + msgid "page.404.text" msgstr "" "This page could not be found or you don't have permission to view it. " "You probably have to sign in or register in order to gain access!" +msgid "page.405.title" +msgstr "405: Method not allowed" + +msgid "page.419.title" +msgstr "Authentication expired" + +msgid "page.419.text" +msgstr "The provided CSRF token is invalid or has expired" + msgid "credits.credit" msgstr "" "The original engelsystem was written by " @@ -55,6 +85,9 @@ msgstr "" "[contributors list on GitHub](https://github.com/engelsystem/engelsystem/graphs/contributors)" " for a complete list." +msgid "form.select_placeholder" +msgstr "Please select..." + msgid "form.load_schedule" msgstr "Load schedule" @@ -73,6 +106,9 @@ msgstr "Preview" msgid "form.delete" msgstr "Delete" +msgid "form.delete_all" +msgstr "Delete all" + msgid "form.updated" msgstr "Updated" @@ -85,12 +121,18 @@ msgstr "Required" msgid "form.markdown" msgstr "You can use Markdown here" +msgid "form.user_select" +msgstr "Select a user" + msgid "schedule.import" msgstr "Import schedule" msgid "schedule.edit.title" msgstr "Edit schedule" +msgid "schedule.delete.title" +msgstr "Delete schedule with %u shifts" + msgid "schedule.import.title" msgstr "Import schedule" @@ -98,7 +140,7 @@ msgid "schedule.last_update" msgstr "Last updated: %s" msgid "schedule.import.text" -msgstr "Imports create rooms and create, update and delete shifts according to a schedule.xml export." +msgstr "Imports create locations and create, update and delete shifts according to a schedule.xml export." msgid "schedule.import.load.title" msgstr "Import schedule: Preview" @@ -115,17 +157,23 @@ msgstr "Schedule URL (schedule.xml)" msgid "schedule.shift-type" msgstr "Shift type" +msgid "schedule.needed-from-shift-type" +msgstr "Load angel types from shift type (else from location)" + msgid "schedule.minutes-before" msgstr "Add minutes before talk begins" msgid "schedule.minutes-after" msgstr "Add minutes after talk ends" +msgid "schedule.for_locations" +msgstr "For locations" + msgid "schedule.import.request_error" msgstr "Unable to load schedule." -msgid "schedule.import.rooms.add" -msgstr "Rooms to create" +msgid "schedule.import.locations.add" +msgstr "Locations to create" msgid "schedule.import.shifts.add" msgstr "Shifts to create" @@ -136,23 +184,17 @@ msgstr "Shifts to update" msgid "schedule.import.shifts.delete" msgstr "Shifts to delete" -msgid "schedule.import.rooms.name" -msgstr "Name" - msgid "schedule.import.shift.dates" msgstr "Times" msgid "schedule.import.shift.type" msgstr "Type" -msgid "schedule.import.shift.title" +msgid "title.title" msgstr "Title" -msgid "schedule.import.shift.room" -msgstr "Room" - -msgid "shifts_history.schedule" -msgstr "Schedule: %s" +msgid "schedule.import.shift.location" +msgstr "Location" msgid "news.title" msgstr "News" @@ -193,8 +235,8 @@ msgstr "Subject" msgid "news.edit.is_meeting" msgstr "Meeting" -msgid "news.edit.is_important" -msgstr "Important" +msgid "news.edit.is_highlighted" +msgstr "Highlighted" msgid "news.edit.is_pinned" msgstr "Pin to top" @@ -205,12 +247,27 @@ msgstr "Message" msgid "news.edit.hint" msgstr "You can use Markdown and the [more] tag" +msgid "news.delete.title" +msgstr "Delete news \"%s\"" + +msgid "news.comments.delete.title" +msgstr "Delete comment \"%s\"" + +msgid "notification.news.updated.introduction" +msgstr "The news %1$s was updated" + +msgid "notification.news.updated.text" +msgstr "You can view it at %3$s" + msgid "form.search" msgstr "Search" msgid "log.log" msgstr "Logs" +msgid "log.only_own" +msgstr "You can view your own logs. The logs of other users can be checked by bureaucrats." + msgid "log.time" msgstr "Time" @@ -223,15 +280,15 @@ msgstr "Message" msgid "settings.settings" msgstr "Settings" -msgid "settings.profile.user_details.info" -msgstr "Here you can change your user details." - msgid "settings.profile.entry_required" -msgstr "Entry required!" +msgstr "Entry required" -msgid "settings.profile.nick" +msgid "general.nick" msgstr "Nick" +msgid "settings.profile.nick.already-taken" +msgstr "The nick is already taken." + msgid "settings.profile.pronoun" msgstr "Pronoun" @@ -250,17 +307,20 @@ msgstr "Planned date of arrival" msgid "settings.profile.planned_departure_date" msgstr "Planned date of departure" -msgid "settings.profile.dect" -msgstr "DECT" - msgid "settings.profile.mobile" msgstr "Mobile" msgid "settings.profile.mobile_show" msgstr "Show mobile number to other users to contact me." -msgid "settings.profile.email" -msgstr "E-Mail" +msgid "settings.profile.email-preferences" +msgstr "E-mail preferences" + +msgid "general.email" +msgstr "E-mail" + +msgid "settings.profile.email.already-taken" +msgstr "This e-mail address is already taken." msgid "settings.profile.email_shiftinfo" msgstr "The %s is allowed to send me an e-mail (e.g. when my shifts change)." @@ -272,17 +332,17 @@ msgid "settings.profile.email_messages" msgstr "Notify me on new private messages." msgid "settings.profile.email_by_human_allowed" -msgstr "Allow heaven angels to contact you by e-mail." +msgstr "Allow heaven angels to contact me by e-mail." msgid "settings.profile.email_goody" -msgstr "To receive vouchers, give consent that nick, e-mail address, worked hours and shirt size will be stored until " -"the next similar event." +msgstr "To possibly receive vouchers for the next similar event, I consent " +"that my nick, e-mail address, worked hours and T-shirt size will be stored until then." msgid "settings.profile.privacy" msgstr "To withdraw your approval, send an e-mail to %1$s." msgid "settings.profile.shirt_size" -msgstr "Shirt size" +msgstr "T-shirt size" msgid "settings.profile.angeltypes.info" msgstr "You can manage your Angeltypes on the Angeltypes page." @@ -293,11 +353,14 @@ msgstr "Password" msgid "settings.password.info" msgstr "Here you can change your password." +msgid "settings.password.confirmation-does-not-match" +msgstr "Password and password confirmation do not match." + msgid "settings.password.password" msgstr "Old password" msgid "settings.password.new_password" -msgstr "New passwort" +msgstr "New password" msgid "settings.password.new_password2" msgstr "Password confirmation" @@ -305,6 +368,21 @@ msgstr "Password confirmation" msgid "settings.password.success" msgstr "Password was changed successfully." +msgid "settings.sessions" +msgstr "Sessions" + +msgid "settings.sessions.info" +msgstr "Here you can see and delete your browser sessions." + +msgid "settings.sessions.current" +msgstr "Current session" + +msgid "settings.sessions.id" +msgstr "Session ID" + +msgid "settings.sessions.last_activity" +msgstr "Last activity" + msgid "settings.theme" msgstr "Theme" @@ -314,6 +392,79 @@ msgstr "Here you can change your theme." msgid "settings.theme.success" msgstr "Theme was changed successfully." +msgid "settings.certificates" +msgstr "Certificates" + +msgid "settings.certificates.info" +msgstr "This information is only visible for supporters and admins." + +msgid "settings.certificates.title.ifsg" +msgstr "Health instructions" + +msgid "settings.certificates.driving_license" +msgstr "Driving license" + +msgid "settings.certificates.has_car" +msgstr "" +"I have my own car with me and am willing to use it for the event (You'll get " +"reimbursed for fuel)" + +msgid "settings.certificates.drive_car" +msgstr "Car" + +msgid "settings.certificates.drive_3_5t" +msgstr "3.5t Transporter" + +msgid "settings.certificates.drive_7_5t" +msgstr "7.5t Truck" + +msgid "settings.certificates.drive_12t" +msgstr "12t Truck" + +msgid "settings.certificates.drive_forklift" +msgstr "Forklift" + +msgid "settings.certificates.ifsg_light" +msgstr "I was instructed about IfSG Β§43 (aka Frikadellendiplom light) on site." + +msgid "settings.certificates.ifsg" +msgstr "I have gotten the instruction about Β§43 IfSG (aka Frikadellendiplom) from my Health Department " +"and a second instruction from us or my employer/chef/assosiation within 3 months. " +"Additionally my second instruction is not older than 2 years." + +msgid "settings.certificates.success" +msgstr "Certificates were updated successfully." + +msgid "angeltype.ifsg.required" +msgstr "Requires health instruction" + +msgid "ifsg.certificate" +msgstr "health instruction" + +msgid "ifsg.certificate_light" +msgstr "health instruction on site" + +msgid "angeltype.ifsg.own" +msgstr "my health instruction" + +msgid "angeltype.ifsg.required.info" +msgstr "This angeltype requires a health instruction. Please enter your health instruction information!" + +msgid "angeltype.ifsg.required.info.here" +msgstr "You joined an angeltype which requires a health instruction. " +"Please edit your health instruction information here: %s." + +msgid "angeltype.driving_license.required.info.here" +msgstr "" +"You joined an angeltype which requires a driving license. " +"Please edit your driving license information here: %s." + +msgid "ifsg.info" +msgstr "Health instruction information" + +msgid "driving_license.info" +msgstr "driving license information" + msgid "settings.language" msgstr "Language" @@ -323,6 +474,35 @@ msgstr "Here you can change your language." msgid "settings.language.success" msgstr "Language was changed successfully." +msgid "settings.api" +msgstr "API" + +msgid "settings.api.about" +msgstr "" +"The API allows you to interact with the Engelsystem by using external programs. " +"It's not complete but we are working on extending it.\n" +"The API endpoint is located at `%s` and described in the [OpenAPI specification](%s).\n" +"Don't share your personal API key with anyone as it can be used to view your personal data " +"and do changes your behalf!" + +msgid "settings.api.shifts_json_show" +msgstr "Show JSON shifts export" + +msgid "settings.api.ical_show" +msgstr "Show iCal export" + +msgid "settings.api.news_show" +msgstr "Show news feeds" + +msgid "settings.api.key_show" +msgstr "Show API key" + +msgid "settings.api.key_reset" +msgstr "Reset API key" + +msgid "settings.api.key_reset_confirm" +msgstr "If you reset the API key you have to update it in all your applications." + msgid "settings.oauth" msgstr "Single Sign-On" @@ -359,6 +539,9 @@ msgstr "Question" msgid "faq.message" msgstr "Answer" +msgid "faq.delete.title" +msgstr "Delete FAQ \"%s\"" + msgid "question.questions" msgstr "Questions" @@ -377,17 +560,20 @@ msgstr "Question" msgid "question.answer" msgstr "Answer" +msgid "question.delete.title" +msgstr "Delete question \"%s\"" + msgid "user.edit.shirt" -msgstr "Edit shirt" +msgstr "Edit T-shirt" msgid "user.edit.goodie" msgstr "Edit goodie" msgid "form.shirt" -msgstr "Shirt" +msgstr "T-shirt" msgid "user.shirt_size" -msgstr "Shirt size" +msgstr "T-shirt size" msgid "user.active" msgstr "Active" @@ -398,8 +584,11 @@ msgstr "Active (forced)" msgid "user.arrived" msgstr "Arrived" +msgid "user.arrive" +msgstr "Arrive" + msgid "user.got_shirt" -msgstr "Got shirt" +msgstr "Got T-shirt" msgid "user.got_goodie" msgstr "Got goodie" @@ -419,9 +608,6 @@ msgstr "Message" msgid "angel" msgstr "Angel" -msgid "date" -msgstr "Date" - msgid "worklog.add" msgstr "Add work log" @@ -452,45 +638,308 @@ msgstr "Teams-/Job description" msgid "angeltypes.about.text" msgstr "Here you can find the list of teams and their tasks. If you have further questions, have a look at the FAQ." +msgid "angeltypes.restricted" +msgstr "Requires introduction" + +msgid "angeltypes.restricted.info" +msgstr "Angel types which require introduction can only be used by an angel " +"if enabled by a supporter (double opt-in)." + msgid "angeltypes.restricted.hint" msgstr "This angeltype requires the attendance at an introduction meeting. " "You might find additional information in the description." -msgid "angeltypes.name" -msgstr "Name" - -msgid "angeltypes.dect" -msgstr "DECT" +msgid "angeltypes.can-change-later" +msgstr "You can change your selection later in the settings." msgid "angeltypes.email" -msgstr "E-Mail" +msgstr "E-mail" + +msgid "angeltypes.shift.self_signup.info" +msgstr "Angel types which have shift self signup enabled allow angels to self sign up for there shifts, " +"if shift self signup is disabled only supporters and admins can sign angels into shifts of these angel types." + +msgid "shift.self_signup" +msgstr "Shift self signup" + +msgid "shift.self_signup.allowed" +msgstr "Shift self signup allowed" + +msgid "angeltypes.hide_on_shift_view" +msgstr "Hide on shift view" + +msgid "angeltypes.hide_on_shift_view.info" +msgstr "If checked only admins and members of the angeltype " +"can see the filter option for this angeltype on the shifts page" msgid "registration.register" msgstr "Register" -msgid "room.rooms" -msgstr "Rooms" +msgid "location.location" +msgstr "Location" -msgid "room.name" -msgstr "Name" - -msgid "room.dect" -msgstr "DECT" +msgid "location.locations" +msgstr "Locations" -msgid "room.map_url" +msgid "location.map_url" msgstr "Map" -msgid "room.description" -msgstr "Description" +msgid "location.required_angels" +msgstr "Required angels (on schedule import)" + +msgid "location.map_url.info" +msgstr "The map will be embedded on the location page as an iframe." + +msgid "location.create.title" +msgstr "Create location" + +msgid "location.edit.title" +msgstr "Edit location" + +msgid "location.delete.title" +msgstr "Delete location \"%s\"" + +msgid "shifttype.shifttypes" +msgstr "Shift types" + +msgid "shifttype.edit.title" +msgstr "Edit shift type" + +msgid "shifttype.create.title" +msgstr "Create shift type" + +msgid "shifttype.delete.title" +msgstr "Delete shift type \"%s\"" + +msgid "shifttype.required_angels" +msgstr "Required angels (on schedule import)" + +msgid "event.day" +msgstr "Day %1$d" + +msgid "dashboard.day" +msgstr "Day" + +msgid "registration.title" +msgstr "Angel registration" + +msgid "registration.login_data" +msgstr "Login data" + +msgid "registration.event_data" +msgstr "Event data" + +msgid "registration.what_todo" +msgstr "What do you want to do?" + +msgid "tshirt.required.hint" +msgstr "Please specify a T-shirt size in your settings!" + +msgid "dect.required.hint" +msgstr "" +"Please specify a DECT phone number in your settings! " +"If you don't have a DECT phone, just enter '-'." + +msgid "pronoun.required.hint" +msgstr "Please enter a pronoun in your settings!" + +msgid "firstname.required.hint" +msgstr "Please enter a firstname in your settings!" + +msgid "lastname.required.hint" +msgstr "Please enter a lastname in your settings!" + +msgid "mobile.required.hint" +msgstr "Please enter a mobile number in your settings!" + +msgid "confirmation.delete" +msgstr "Do you really want to delete it?" + +msgid "general.datetime" +msgstr "Y-m-d H:i" + +msgid "general.date" +msgstr "Y-m-d" + +msgid "general.id" +msgstr "ID" + +msgid "general.user" +msgstr "User" + +msgid "general.count" +msgstr "Count" + +msgid "general.created_at" +msgstr "Created at" + +msgid "shifts.random" +msgstr "ZufΓ€llige Schicht" + +msgid "shifts.history" +msgstr "Shifts history" + +msgid "shifts.history.schedule" +msgstr "Schedule: %s" + +msgid "shifts.history.delete_all.title" +msgstr "Delete %u shifts" + +msgid "shifts.start" +msgstr "Start" + +msgid "shifts.end" +msgstr "End" + +msgid "user.info" +msgstr "User info" + +msgid "user.info.hint" +msgstr "" +"Is displayed for shift coordinators and admins in the user view. " +"If an angel is not marked as arrived a user-info removes the not-arrived notification in the angels sysmenu." + +msgid "user_info.not_arrived_hint" +msgstr "If you want to sign up for shifts, feel free to drop by heaven." + +msgid "email.greeting" +msgstr "Hi %s," + +msgid "email.introduction" +msgstr "here is a message for you from the %s:" + +msgid "email.footer" +msgstr "" +"This e-mail is autogenerated and has not been signed. " +"You got this e-mail because you are registered in the %s." + +msgid "password.email.message" +msgstr "Please visit %s to recover your password." + +msgid "password.minimal_length" +msgstr "Minimal length %d characters" + +msgid "footer.eventinfo.name_start_end" +msgstr "%1$s, from %2$s to %3$s" + +msgid "footer.eventinfo.name_start" +msgstr "%1$s, starting %2$s" + +msgid "footer.eventinfo.start_end" +msgstr "Event from %1$s to %2$s" + +msgid "footer.issues" +msgstr "Bugs / Features" + +msgid "footer.github" +msgstr "Development Platform" + +msgid "credits.title" +msgstr "Credits" + +msgid "general.register" +msgstr "Register" + +msgid "shift.next" +msgstr "Next shift" + +msgid "general.logout" +msgstr "Logout" + +msgid "credits.source" +msgstr "Source code" + +msgid "credits.version" +msgstr "Version: _%s_" + +msgid "design.title" +msgstr "Design" + +msgid "login.welcome" +msgstr "Welcome to the %s!" + +msgid "event.buildup.start" +msgstr "Buildup starts" + +msgid "event.starts" +msgstr "Event starts" + +msgid "event.ends" +msgstr "Event ends" + +msgid "event.teardown.ends" +msgstr "Teardown ends" + +msgid "login.password.reset" +msgstr "I forgot my password" + +msgid "login.registration" +msgstr "Please sign up, if you want to help us!" + +msgid "login.registration.external" +msgstr "Registration is only available via external login." + +msgid "login.registration.disabled" +msgstr "Registration is disabled." + +msgid "login.do" +msgstr "What can I do?" + +msgid "login.jobs" +msgstr "Read about the jobs you can do to help us." + +msgid "login.cookies" +msgstr "Please note: You have to activate cookies!" + +msgid "general.angel" +msgstr "Angel" + +msgid "title.date" +msgstr "Date" + +msgid "password.reset.confirm" +msgstr "Confirm password" + +msgid "password.recovery.success" +msgstr "We sent you an e-mail containing your password recovery link." + +msgid "password.reset.title" +msgstr "Password recovery" + +msgid "password.recovery.text" +msgstr "" +"We will send you an e-mail with a password recovery link. " +"Please use the e-mail address you used for registration." + +msgid "form.recover" +msgstr "Recover" + +msgid "notification.shift.updated.introduction" +msgstr "Your shift has changed:" + +msgid "notification.shift.updated.type" +msgstr "Shift type changed from %s to %s" + +msgid "notification.shift.updated.title" +msgstr "Shift title changed from %s to %s" + +msgid "notification.shift.updated.description" +msgstr "Shift description changed" + +msgid "notification.shift.updated.start" +msgstr "Shift start changed from %s to %s" + +msgid "notification.shift.updated.end" +msgstr "Shift end changed from %s to %s" -msgid "room.required_angels" -msgstr "Required angels" +msgid "notification.shift.updated.location" +msgstr "Shift location moved from %s to %s" -msgid "room.map_url.info" -msgstr "The map will be embedded on the room page as an iframe." +msgid "notification.shift.updated.shift" +msgstr "The updated Shift:" -msgid "room.create.title" -msgstr "Create room" +msgid "general.actions" +msgstr "Actions" -msgid "room.edit.title" -msgstr "Edit room" +msgid "general.back" +msgstr "Back" diff --git a/resources/lang/pt_BR/default.po b/resources/lang/pt_BR/default.po deleted file mode 100644 index 9f48b4ce9..000000000 --- a/resources/lang/pt_BR/default.po +++ /dev/null @@ -1,2610 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: Engelsystem 2.0\n" -"POT-Creation-Date: 2017-04-25 05:23+0200\n" -"PO-Revision-Date: 2018-11-27 00:29+0100\n" -"Last-Translator: samba \n" -"Language-Team: \n" -"Language: pt_BR\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.0.1\n" -"X-Poedit-KeywordsList: _;gettext;gettext_noop\n" -"X-Poedit-Basepath: ../../..\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Poedit-SourceCharset: UTF-8\n" -"X-Poedit-SearchPath-0: .\n" - -#: includes/controller/angeltypes_controller.php:7 -#: includes/pages/user_shifts.php:164 includes/view/AngelTypes_view.php:62 -#: includes/view/AngelTypes_view.php:86 includes/view/User_view.php:356 -msgid "Angeltypes" -msgstr "Tipo de Anjo" - -#: includes/controller/angeltypes_controller.php:53 -#: includes/pages/guest_login.php:377 includes/view/AngelTypes_view.php:265 -#: includes/view/AngelTypes_view.php:326 includes/view/User_view.php:110 -msgid "Teams/Job description" -msgstr "Time/Descrição do trabalho" - -#: includes/controller/angeltypes_controller.php:72 -#, php-format -msgid "Angeltype %s deleted." -msgstr "Tipo de anjo %s apagado." - -#: includes/controller/angeltypes_controller.php:77 -#: includes/view/AngelTypes_view.php:41 -#, php-format -msgid "Delete angeltype %s" -msgstr "Apagar tipo de anjo %s" - -#: includes/controller/angeltypes_controller.php:116 -msgid "Please check the name. Maybe it already exists." -msgstr "Por favor verifique o nome. Pode ser que jΓ‘ exista." - -#: includes/controller/angeltypes_controller.php:141 -#: includes/view/AngelTypes_view.php:60 -#, php-format -msgid "Edit %s" -msgstr "Editar %s" - -#: includes/controller/angeltypes_controller.php:162 -#: includes/view/AngelTypes_view.php:252 -#, php-format -msgid "Team %s" -msgstr "Time %s" - -#: includes/controller/angeltypes_controller.php:181 -#: includes/view/User_view.php:274 -msgid "view" -msgstr "ver" - -#: includes/controller/angeltypes_controller.php:185 -#: includes/pages/admin_free.php:71 includes/pages/admin_groups.php:23 -#: includes/pages/admin_rooms.php:16 includes/view/AngelTypes_view.php:107 -#: includes/view/ShiftTypes_view.php:55 includes/view/ShiftTypes_view.php:67 -#: includes/view/Shifts_view.php:55 includes/view/User_view.php:277 -#: includes/view/User_view.php:328 -msgid "edit" -msgstr "editar" - -#: includes/controller/angeltypes_controller.php:186 -#: includes/controller/shifts_controller.php:178 -#: includes/pages/admin_questions.php:42 includes/pages/admin_questions.php:56 -#: includes/pages/admin_rooms.php:17 includes/pages/admin_user.php:120 -#: includes/view/AngelTypes_view.php:45 includes/view/AngelTypes_view.php:110 -#: includes/view/Questions_view.php:5 includes/view/Questions_view.php:12 -#: includes/view/ShiftTypes_view.php:16 includes/view/ShiftTypes_view.php:56 -#: includes/view/ShiftTypes_view.php:68 includes/view/Shifts_view.php:56 -msgid "delete" -msgstr "deletar" - -#: includes/controller/angeltypes_controller.php:191 -#: includes/view/AngelTypes_view.php:103 includes/view/AngelTypes_view.php:288 -msgid "leave" -msgstr "sair" - -#: includes/controller/angeltypes_controller.php:193 -#: includes/view/AngelTypes_view.php:94 includes/view/AngelTypes_view.php:290 -msgid "join" -msgstr "entrar" - -#: includes/controller/angeltypes_controller.php:220 -#: includes/controller/user_angeltypes_controller.php:29 -#: includes/controller/user_angeltypes_controller.php:35 -#: includes/controller/user_angeltypes_controller.php:65 -#: includes/controller/user_angeltypes_controller.php:71 -#: includes/controller/user_angeltypes_controller.php:119 -#: includes/controller/user_angeltypes_controller.php:170 -#: includes/controller/user_angeltypes_controller.php:235 -msgid "Angeltype doesn't exist." -msgstr "Esse tipo de anjo nΓ£o existe." - -#: includes/controller/event_config_controller.php:4 -msgid "Event config" -msgstr "Configuração do evento" - -#: includes/controller/event_config_controller.php:48 -msgid "Please enter buildup start date." -msgstr "Por favor digite a data de inΓ­cio da montagem." - -#: includes/controller/event_config_controller.php:52 -msgid "Please enter event start date." -msgstr "Por favor digite a data de inΓ­cio do evento." - -#: includes/controller/event_config_controller.php:56 -msgid "Please enter event end date." -msgstr "Por favor digite a data de tΓ©rmino do evento." - -#: includes/controller/event_config_controller.php:60 -msgid "Please enter teardown end date." -msgstr "Por favor digite a data de desmontagem do evento" - -#: includes/controller/event_config_controller.php:66 -msgid "The buildup start date has to be before the event start date." -msgstr "A data de montagem deve ser anterior a data de inΓ­cio do evento." - -#: includes/controller/event_config_controller.php:71 -msgid "The event start date has to be before the event end date." -msgstr "" -"A data de inΓ­cio do evento deve ser anterior a data de tΓ©rmino do evento." - -#: includes/controller/event_config_controller.php:76 -msgid "The event end date has to be before the teardown end date." -msgstr "A data de tΓ©rmino deve ser anterior a data de desmontagem do evento." - -#: includes/controller/event_config_controller.php:81 -msgid "The buildup start date has to be before the teardown end date." -msgstr "A data de montagem deve ser anterior a data de desmontagem do evento." - -#: includes/controller/event_config_controller.php:92 -msgid "Settings saved." -msgstr "Configuraçáes salvas." - -#: includes/controller/shift_entries_controller.php:56 -msgid "" -"You are not allowed to sign up for this shift. Maybe shift is full or " -"already running." -msgstr "" -"VocΓͺ nΓ£o tem permissΓ£o para se inscrever nesse turno. Talvez o turno esteja " -"lotado ou jΓ‘ esteja em andamento." - -#: includes/controller/shift_entries_controller.php:103 -msgid "You are subscribed. Thank you!" -msgstr "VocΓͺ jΓ‘ estΓ‘ inscrito. Obrigado!" - -#: includes/controller/shift_entries_controller.php:103 -#: includes/pages/user_myshifts.php:4 -msgid "My shifts" -msgstr "Meus turnos" - -#: includes/controller/shift_entries_controller.php:111 -#: includes/view/User_view.php:348 -msgid "Freeloader" -msgstr "Freeloader" - -#: includes/controller/shift_entries_controller.php:180 -msgid "Shift entry deleted." -msgstr "O turno foi deletado." - -#: includes/controller/shift_entries_controller.php:182 -msgid "Entry not found." -msgstr "Entrada nΓ£o encontrada." - -#: includes/controller/shifts_controller.php:63 -msgid "Please select a room." -msgstr "Por favor selecione uma sala." - -#: includes/controller/shifts_controller.php:70 -msgid "Please select a shifttype." -msgstr "Por favor selecione um tssipo de turno." - -#: includes/controller/shifts_controller.php:77 -msgid "Please enter a valid starting time for the shifts." -msgstr "Por favor entre com um horΓ‘rio de inΓ­cio vΓ‘lido para os turnos." - -#: includes/controller/shifts_controller.php:84 -msgid "Please enter a valid ending time for the shifts." -msgstr "Por favor entre com um horΓ‘rio de tΓ©rmino vΓ‘lido para os turnos." - -#: includes/controller/shifts_controller.php:89 -msgid "The ending time has to be after the starting time." -msgstr "O horΓ‘rio de tΓ©rmino deve ser apΓ³s o horΓ‘rio de inΓ­cio." - -#: includes/controller/shifts_controller.php:97 -#, php-format -msgid "Please check your input for needed angels of type %s." -msgstr "Por favor verifique seu input para os anjos de tipo %s necessΓ‘rios." - -#: includes/controller/shifts_controller.php:120 -msgid "Shift updated." -msgstr "Turno atualizado." - -#: includes/controller/shifts_controller.php:135 -msgid "This page is much more comfortable with javascript." -msgstr "Esta pΓ‘gina Γ© muito mais confortΓ‘vel com javascript" - -#: includes/controller/shifts_controller.php:137 -#: includes/pages/admin_import.php:98 includes/pages/admin_shifts.php:319 -msgid "Shifttype" -msgstr "Tipo de turno" - -#: includes/controller/shifts_controller.php:138 -#: includes/pages/admin_import.php:158 includes/pages/admin_import.php:167 -#: includes/pages/admin_import.php:176 includes/pages/admin_shifts.php:320 -#: includes/view/Shifts_view.php:62 -msgid "Title" -msgstr "TΓ­tulo" - -#: includes/controller/shifts_controller.php:139 -msgid "Room:" -msgstr "Sala:" - -#: includes/controller/shifts_controller.php:140 -msgid "Start:" -msgstr "InΓ­cio:" - -#: includes/controller/shifts_controller.php:141 -msgid "End:" -msgstr "Fim:" - -#: includes/controller/shifts_controller.php:142 -#: includes/pages/admin_shifts.php:270 includes/pages/admin_shifts.php:334 -#: includes/view/Shifts_view.php:88 -msgid "Needed angels" -msgstr "Anjos necessΓ‘rios" - -#: includes/controller/shifts_controller.php:144 -#: includes/pages/admin_groups.php:54 includes/pages/admin_news.php:35 -#: includes/pages/admin_questions.php:40 includes/pages/admin_rooms.php:157 -#: includes/pages/admin_shifts.php:272 includes/pages/user_messages.php:44 -#: includes/pages/user_news.php:107 includes/pages/user_news.php:164 -#: includes/view/AngelTypes_view.php:76 includes/view/EventConfig_view.php:122 -#: includes/view/Questions_view.php:32 includes/view/ShiftEntry_view.php:32 -#: includes/view/ShiftTypes_view.php:39 -#: includes/view/UserDriverLicenses_view.php:34 includes/view/User_view.php:56 -#: includes/view/User_view.php:65 includes/view/User_view.php:70 -#: includes/view/User_view.php:75 includes/view/User_view.php:146 -#: includes/view/User_view.php:402 -msgid "Save" -msgstr "Salvar" - -#: includes/controller/shifts_controller.php:172 -msgid "Shift deleted." -msgstr "Turno deletado." - -#: includes/controller/shifts_controller.php:177 -#, php-format -msgid "Do you want to delete the shift %s from %s to %s?" -msgstr "VocΓͺ quer deletar o turno %s de %s para %s?" - -#: includes/controller/shifts_controller.php:195 -msgid "Shift could not be found." -msgstr "O turno nΓ£o pΓ΄de ser encontrado." - -#: includes/controller/shifttypes_controller.php:31 -#, php-format -msgid "Shifttype %s deleted." -msgstr "Tipo de turno %s deletado." - -#: includes/controller/shifttypes_controller.php:36 -#: includes/view/ShiftTypes_view.php:12 -#, php-format -msgid "Delete shifttype %s" -msgstr "Apagar tipo de turno %s" - -#: includes/controller/shifttypes_controller.php:58 -msgid "Shifttype not found." -msgstr "Tipo de turno nΓ£o encontrado." - -#: includes/controller/shifttypes_controller.php:74 -#: includes/pages/admin_rooms.php:71 -msgid "Please enter a name." -msgstr "Por favor digite um nome." - -#: includes/controller/shifttypes_controller.php:94 -msgid "Updated shifttype." -msgstr "Tipo de turno atualizado." - -#: includes/controller/shifttypes_controller.php:101 -msgid "Created shifttype." -msgstr "Tipo de turno criado" - -#: includes/controller/shifttypes_controller.php:155 -msgid "Shifttypes" -msgstr "Tipos de turno" - -#: includes/controller/user_angeltypes_controller.php:19 -#, php-format -msgid "There is %d unconfirmed angeltype." -msgid_plural "There are %d unconfirmed angeltypes." -msgstr[0] "HΓ‘ %d anjo nΓ£o confirmado." -msgstr[1] "There are %d unconfirmed angeltypes." - -#: includes/controller/user_angeltypes_controller.php:19 -msgid "Angel types which need approvals:" -msgstr "Tipos de anjo que precisam de aprovaçáes:" - -#: includes/controller/user_angeltypes_controller.php:40 -msgid "You are not allowed to delete all users for this angeltype." -msgstr "" -"VocΓͺ nΓ£o tΓͺm permissΓ£o para apagar todos os usuΓ‘rios desse tipo de anjo." - -#: includes/controller/user_angeltypes_controller.php:48 -#, php-format -msgid "Denied all users for angeltype %s." -msgstr "Todos os usuΓ‘rios com tipo de anjo %s negados." - -#: includes/controller/user_angeltypes_controller.php:53 -#: includes/view/UserAngelTypes_view.php:15 -msgid "Deny all users" -msgstr "Negar todos os usuΓ‘rios" - -#: includes/controller/user_angeltypes_controller.php:77 -#: includes/controller/user_angeltypes_controller.php:107 -#: includes/controller/user_angeltypes_controller.php:113 -#: includes/controller/user_angeltypes_controller.php:158 -#: includes/controller/user_angeltypes_controller.php:164 -#: includes/controller/user_angeltypes_controller.php:216 -#: includes/controller/user_angeltypes_controller.php:229 -msgid "User angeltype doesn't exist." -msgstr "O tipo de anjo deste usuΓ‘rio nΓ£o existe." - -#: includes/controller/user_angeltypes_controller.php:82 -msgid "You are not allowed to confirm all users for this angeltype." -msgstr "" -"VocΓͺ nΓ£o tem permissΓ£o para confirmar todos os usuΓ‘rios com este tipo de " -"anjo." - -#: includes/controller/user_angeltypes_controller.php:90 -#, php-format -msgid "Confirmed all users for angeltype %s." -msgstr "Todos os usuΓ‘rios com tipo de anjo %s confirmados." - -#: includes/controller/user_angeltypes_controller.php:95 -#: includes/view/UserAngelTypes_view.php:26 -msgid "Confirm all users" -msgstr "Confirmar todos usuΓ‘rios" - -#: includes/controller/user_angeltypes_controller.php:124 -msgid "You are not allowed to confirm this users angeltype." -msgstr "VocΓͺ nΓ£o tem permissΓ£o para confirmar o tipo de anjo deste usuΓ‘rio." - -#: includes/controller/user_angeltypes_controller.php:130 -#: includes/controller/user_angeltypes_controller.php:176 -#: includes/controller/user_angeltypes_controller.php:241 -#: includes/controller/users_controller.php:312 -msgid "User doesn't exist." -msgstr "UsuΓ‘rio nΓ£o existente." - -#: includes/controller/user_angeltypes_controller.php:141 -#, php-format -msgid "%s confirmed for angeltype %s." -msgstr "%s confirmado para o tipo de anjo %s." - -#: includes/controller/user_angeltypes_controller.php:146 -#: includes/view/UserAngelTypes_view.php:37 -msgid "Confirm angeltype for user" -msgstr "Confirme o tipo de anjo para o usuΓ‘rio" - -#: includes/controller/user_angeltypes_controller.php:181 -msgid "You are not allowed to delete this users angeltype." -msgstr "VocΓͺ nΓ£o tem permissΓ£o para deletar o tipo de anjo deste usuΓ‘rio." - -#: includes/controller/user_angeltypes_controller.php:191 -#, php-format -msgid "User %s removed from %s." -msgstr "UsuΓ‘rio %s removido de %s." - -#: includes/controller/user_angeltypes_controller.php:199 -#: includes/view/UserAngelTypes_view.php:48 -msgid "Remove angeltype" -msgstr "Remover esse tipo de anjo" - -#: includes/controller/user_angeltypes_controller.php:211 -msgid "You are not allowed to set supporter rights." -msgstr "VocΓͺ nΓ£o tem autorização para definir permissΓ΅es de apoiadores." - -#: includes/controller/user_angeltypes_controller.php:223 -msgid "No supporter update given." -msgstr "Nenhuma atualização de apoiador informada." - -#: includes/controller/user_angeltypes_controller.php:248 -#, php-format -msgid "Added supporter rights for %s to %s." -msgstr "PermissΓ΅es de apoiador incluΓ­dos para %s a %s." - -#: includes/controller/user_angeltypes_controller.php:248 -#, php-format -msgid "Removed supporter rights for %s from %s." -msgstr "PermissΓ΅es de apoiador removidos para %s a %s." - -#: includes/controller/user_angeltypes_controller.php:256 -#: includes/view/AngelTypes_view.php:156 -#: includes/view/UserAngelTypes_view.php:4 -msgid "Add supporter rights" -msgstr "Adicionar permissΓ£o ao apoiador" - -#: includes/controller/user_angeltypes_controller.php:256 -#: includes/view/AngelTypes_view.php:147 -#: includes/view/UserAngelTypes_view.php:4 -msgid "Remove supporter rights" -msgstr "Remover permissΓ΅es de apoiador" - -#: includes/controller/user_angeltypes_controller.php:289 -#, php-format -msgid "User %s added to %s." -msgstr "UsuΓ‘rio %s adicionado a %s." - -#: includes/controller/user_angeltypes_controller.php:299 -#: includes/view/UserAngelTypes_view.php:64 -msgid "Add user to angeltype" -msgstr "Adicionar usuΓ‘rio a tipo de anjo" - -#: includes/controller/user_angeltypes_controller.php:312 -#, php-format -msgid "You are already a %s." -msgstr "VocΓͺ jΓ‘ Γ© %s." - -#: includes/controller/user_angeltypes_controller.php:319 -#, php-format -msgid "You joined %s." -msgstr "VocΓͺ se juntou a %s." - -#: includes/controller/user_angeltypes_controller.php:332 -#: includes/view/UserAngelTypes_view.php:78 -#, php-format -msgid "Become a %s" -msgstr "Torne-se %s" - -#: includes/controller/user_driver_licenses_controller.php:19 -#, php-format -msgid "" -"You joined an angeltype which requires a driving license. Please edit your " -"driving license information here: %s." -msgstr "" -"VocΓͺ se tornou um tipo de anjo que requer carteira de motorista. Por favor " -"inclua \n" -"seus dados aqui: %s." - -#: includes/controller/user_driver_licenses_controller.php:19 -msgid "driving license information" -msgstr "dados da carteira de motorista" - -#: includes/controller/user_driver_licenses_controller.php:113 -msgid "Your driver license information has been saved." -msgstr "Dados da carteira de motorista salvos." - -#: includes/controller/user_driver_licenses_controller.php:116 -msgid "Please select at least one driving license." -msgstr "Selecione pelo menos uma carteira de motorista." - -#: includes/controller/user_driver_licenses_controller.php:121 -msgid "Your driver license information has been removed." -msgstr "Seus dados de carteira de motorista foram removidos." - -#: includes/controller/user_driver_licenses_controller.php:127 -#: includes/view/UserDriverLicenses_view.php:15 -#, php-format -msgid "Edit %s driving license information" -msgstr "Editar dados da carteira de motorista de %s" - -#: includes/controller/users_controller.php:52 -msgid "You cannot delete yourself." -msgstr "VocΓͺ nΓ£o pode se deletar." - -#: includes/controller/users_controller.php:61 -#: includes/pages/guest_login.php:315 -msgid "Your password is incorrect. Please try it again." -msgstr "Sua senha estΓ‘ incorreta. Por favor, tente novamente." - -#: includes/controller/users_controller.php:71 -msgid "User deleted." -msgstr "UsuΓ‘rio deletado." - -#: includes/controller/users_controller.php:79 includes/view/User_view.php:121 -#, php-format -msgid "Delete %s" -msgstr "Apagar %s" - -#: includes/controller/users_controller.php:120 -msgid "Please enter a valid number of vouchers." -msgstr "Por favor, entre com um nΓΊmero vΓ‘lido de vouchers." - -#: includes/controller/users_controller.php:131 -msgid "Saved the number of vouchers." -msgstr "NΓΊmero de vouchers salvo." - -#: includes/controller/users_controller.php:139 includes/view/User_view.php:138 -#, php-format -msgid "%s's vouchers" -msgstr "Vouchers de %s" - -#: includes/controller/users_controller.php:151 -msgid "User not found." -msgstr "UsuΓ‘rio nΓ£o encontrado." - -#: includes/controller/users_controller.php:205 includes/view/User_view.php:175 -msgid "All users" -msgstr "Todos usuΓ‘rios" - -#: includes/controller/users_controller.php:217 -msgid "Token is not correct." -msgstr "O token nΓ£o estΓ‘ correto." - -#: includes/controller/users_controller.php:227 -#: includes/pages/guest_login.php:102 -msgid "Your passwords don't match." -msgstr "Suas senhas nΓ£o correspondem." - -#: includes/controller/users_controller.php:231 -msgid "Your password is to short (please use at least 6 characters)." -msgstr "Sua senha Γ© muito curta (por favor use no mΓ­nimo 6 caracteres)." - -#: includes/controller/users_controller.php:236 -msgid "Password saved." -msgstr "Sua senha foi salva." - -#: includes/controller/users_controller.php:257 -#: includes/controller/users_controller.php:261 -#: includes/pages/guest_login.php:67 -msgid "E-mail address is not correct." -msgstr "E-mail nΓ£o estΓ‘ correto." - -#: includes/controller/users_controller.php:265 -#: includes/pages/guest_login.php:71 -msgid "Please enter your e-mail." -msgstr "Por favor digite seu e-mail." - -#: includes/controller/users_controller.php:270 -#: includes/controller/users_controller.php:295 -msgid "Password recovery" -msgstr "Recuperação de senha" - -#: includes/controller/users_controller.php:270 -#, php-format -msgid "Please visit %s to recover your password." -msgstr "Por favor visite %s para recuperar sua senha" - -#: includes/controller/users_controller.php:271 -msgid "We sent you an email containing your password recovery link." -msgstr "NΓ³s enviamos um email com o link para recuperação da sua senha." - -#: includes/helper/email_helper.php:12 -#, php-format -msgid "Hi %s," -msgstr "Oi %s," - -#: includes/helper/email_helper.php:12 -#, php-format -msgid "here is a message for you from the %s:" -msgstr "aqui estΓ‘ uma mensagem do %s para vocΓͺ:" - -#: includes/helper/email_helper.php:12 -#, php-format -msgid "" -"This email is autogenerated and has not been signed. You got this email " -"because you are registered in the %s." -msgstr "VocΓͺ recebeu esse email porque estΓ‘ registrado no %s." - -#: includes/mailer/shifts_mailer.php:10 -msgid "A Shift you are registered on has changed:" -msgstr "Um turno em que vocΓͺ estava registrado foi modificado:" - -#: includes/mailer/shifts_mailer.php:14 -#, php-format -msgid "* Shift type changed from %s to %s" -msgstr "* Tipo de turno alterado de %s para %s" - -#: includes/mailer/shifts_mailer.php:19 -#, php-format -msgid "* Shift title changed from %s to %s" -msgstr "* TΓ­tulo do turno alterado de %s para %s" - -#: includes/mailer/shifts_mailer.php:24 -#, php-format -msgid "* Shift Start changed from %s to %s" -msgstr "* InΓ­cio do turno alterado de %s para %s" - -#: includes/mailer/shifts_mailer.php:29 -#, php-format -msgid "* Shift End changed from %s to %s" -msgstr "* TΓ©rmino do turno alterado de %s para %s" - -#: includes/mailer/shifts_mailer.php:34 -#, php-format -msgid "* Shift Location changed from %s to %s" -msgstr "* Local do turno alterado de %s para %s" - -#: includes/mailer/shifts_mailer.php:44 -msgid "The updated Shift:" -msgstr "Turno atualizado:" - -#: includes/mailer/shifts_mailer.php:53 -msgid "Your Shift has changed" -msgstr "O seu turno foi modificado" - -#: includes/mailer/shifts_mailer.php:62 -msgid "A Shift you are registered on was deleted:" -msgstr "Um turno em que vocΓͺ estava registrado foi apagado:" - -#: includes/mailer/shifts_mailer.php:71 -msgid "Your Shift was deleted" -msgstr "Seu turno foi apagado" - -#: includes/mailer/shifts_mailer.php:80 -msgid "You have been assigned to a Shift:" -msgstr "VocΓͺ foi alocado a um turno:" - -#: includes/mailer/shifts_mailer.php:86 -msgid "Assigned to Shift" -msgstr "Alocado ao turno" - -#: includes/mailer/shifts_mailer.php:94 -msgid "You have been removed from a Shift:" -msgstr "VocΓͺ foi removido de um turno:" - -#: includes/mailer/shifts_mailer.php:100 -msgid "Removed from Shift" -msgstr "Removido do turno" - -#: includes/mailer/users_mailer.php:7 -msgid "Your account has been deleted" -msgstr "A sua conta foi deletada." - -#: includes/mailer/users_mailer.php:7 -msgid "" -"Your angelsystem account has been deleted. If you have any questions " -"regarding your account deletion, please contact heaven." -msgstr "" -"Sua conta engelsystem foi deletada. Se vocΓͺ tiver questΓ΅es sobre a deleção " -"da sua conta, por favor entre em contato com o paraΓ­so." - -#: includes/pages/admin_active.php:4 -msgid "Active angels" -msgstr "Anjos ativos" - -#: includes/pages/admin_active.php:29 -#, php-format -msgid "" -"At least %s angels are forced to be active. The number has to be greater." -msgstr "No mΓ­nimo %s anjos precisam estar ativos. O nΓΊmero deve ser maior." - -#: includes/pages/admin_active.php:34 -msgid "Please enter a number of angels to be marked as active." -msgstr "Por favor insira o nΓΊmero de anjos a marcar como ativos." - -#: includes/pages/admin_active.php:59 -msgid "Marked angels." -msgstr "Anjos marcados." - -#: includes/pages/admin_active.php:61 includes/pages/admin_rooms.php:137 -#: includes/pages/admin_rooms.php:173 includes/pages/admin_shifts.php:266 -#: includes/view/UserAngelTypes_view.php:67 includes/view/User_view.php:124 -#: includes/view/User_view.php:141 -msgid "back" -msgstr "voltar" - -#: includes/pages/admin_active.php:61 -msgid "apply" -msgstr "aplicar" - -#: includes/pages/admin_active.php:71 -msgid "Angel has been marked as active." -msgstr "Anjo marcado como ativo." - -#: includes/pages/admin_active.php:73 includes/pages/admin_active.php:83 -#: includes/pages/admin_active.php:103 includes/pages/admin_arrive.php:23 -#: includes/pages/admin_arrive.php:34 -msgid "Angel not found." -msgstr "Anjo nΓ£o encontrado." - -#: includes/pages/admin_active.php:81 -msgid "Angel has been marked as not active." -msgstr "Anjo marcado como nΓ£o ativo." - -#: includes/pages/admin_active.php:91 -msgid "Angel has got a t-shirt." -msgstr "Anjo tem uma camiseta." - -#: includes/pages/admin_active.php:101 -msgid "Angel has got no t-shirt." -msgstr "Anjo nΓ£o tem camiseta." - -#: includes/pages/admin_active.php:142 -msgid "set active" -msgstr "definir ativo" - -#: includes/pages/admin_active.php:145 -msgid "remove active" -msgstr "remover ativo" - -#: includes/pages/admin_active.php:146 -msgid "got t-shirt" -msgstr "pegou camiseta" - -#: includes/pages/admin_active.php:149 -msgid "remove t-shirt" -msgstr "remover camiseta" - -#: includes/pages/admin_active.php:168 includes/pages/admin_arrive.php:165 -#: includes/pages/admin_arrive.php:180 includes/pages/admin_arrive.php:195 -#: includes/view/AngelTypes_view.php:221 includes/view/AngelTypes_view.php:229 -#: includes/view/User_view.php:165 -msgid "Sum" -msgstr "SomatΓ³ria" - -#: includes/pages/admin_active.php:175 -msgid "Search angel:" -msgstr "Buscar Anjo:" - -#: includes/pages/admin_active.php:176 -msgid "Show all shifts" -msgstr "Mostrar todos os turnos" - -#: includes/pages/admin_active.php:177 includes/pages/admin_arrive.php:141 -#: includes/pages/admin_arrive.php:142 includes/pages/admin_free.php:78 -#: includes/pages/admin_free.php:87 includes/pages/admin_log.php:23 -#: includes/pages/admin_log.php:24 -msgid "Search" -msgstr "Buscar" - -#: includes/pages/admin_active.php:180 -msgid "How much angels should be active?" -msgstr "Quantos anjos deverΓ£o estar ativos?" - -#: includes/pages/admin_active.php:181 includes/pages/admin_shifts.php:254 -#: includes/pages/admin_shifts.php:342 -msgid "Preview" -msgstr "PrΓ©-visualizar" - -#: includes/pages/admin_active.php:185 includes/pages/admin_arrive.php:145 -msgid "Nickname" -msgstr "Apelido" - -#: includes/pages/admin_active.php:186 includes/pages/admin_active.php:196 -#: includes/view/User_view.php:191 -msgid "Size" -msgstr "Tamanho" - -#: includes/pages/admin_active.php:187 includes/pages/user_shifts.php:5 -#: includes/view/User_view.php:364 -msgid "Shifts" -msgstr "Turnos" - -#: includes/pages/admin_active.php:188 includes/pages/admin_shifts.php:329 -msgid "Length" -msgstr "Duração" - -#: includes/pages/admin_active.php:189 -msgid "Active?" -msgstr "Ativo?" - -#: includes/pages/admin_active.php:190 includes/view/User_view.php:189 -msgid "Forced" -msgstr "ForΓ§ados" - -#: includes/pages/admin_active.php:191 -msgid "T-shirt?" -msgstr "Camiseta?" - -#: includes/pages/admin_active.php:194 -msgid "Shirt statistics" -msgstr "EstatΓ­sticas de camiseta" - -#: includes/pages/admin_active.php:197 -msgid "Needed shirts" -msgstr "Camisetas necessΓ‘rias" - -#: includes/pages/admin_active.php:198 -msgid "Given shirts" -msgstr "Camisetas entregues" - -#: includes/pages/admin_arrive.php:4 -msgid "Arrive angels" -msgstr "Anjos que chegaram" - -#: includes/pages/admin_arrive.php:20 -msgid "Reset done. Angel has not arrived." -msgstr "Reset realizado. Anjo nΓ£o chegou." - -#: includes/pages/admin_arrive.php:31 -msgid "Angel has been marked as arrived." -msgstr "Chegada do anjo registrada." - -#: includes/pages/admin_arrive.php:71 includes/view/UserAngelTypes_view.php:9 -#: includes/view/UserAngelTypes_view.php:20 -#: includes/view/UserAngelTypes_view.php:31 -#: includes/view/UserAngelTypes_view.php:42 -#: includes/view/UserAngelTypes_view.php:53 -msgid "yes" -msgstr "sim" - -#: includes/pages/admin_arrive.php:72 -msgid "reset" -msgstr "resetar" - -#: includes/pages/admin_arrive.php:72 includes/pages/admin_arrive.php:156 -#: includes/pages/admin_arrive.php:171 includes/pages/admin_arrive.php:186 -#: includes/view/User_view.php:330 -msgid "arrived" -msgstr "chegou" - -#: includes/pages/admin_arrive.php:146 -msgid "Planned arrival" -msgstr "Chegada planejada" - -#: includes/pages/admin_arrive.php:147 -msgid "Arrived?" -msgstr "Chegou?" - -#: includes/pages/admin_arrive.php:148 -msgid "Arrival date" -msgstr "Data de chegada" - -#: includes/pages/admin_arrive.php:149 -msgid "Planned departure" -msgstr "SaΓ­da planejada" - -#: includes/pages/admin_arrive.php:154 -msgid "Planned arrival statistics" -msgstr "EstatΓ­sticas de chegadas planejadas" - -#: includes/pages/admin_arrive.php:157 includes/pages/admin_arrive.php:172 -#: includes/pages/admin_arrive.php:187 -msgid "arrived sum" -msgstr "soma dos que chegaram" - -#: includes/pages/admin_arrive.php:163 includes/pages/admin_arrive.php:178 -#: includes/pages/admin_arrive.php:193 includes/pages/admin_news.php:30 -#: includes/pages/user_messages.php:76 -msgid "Date" -msgstr "Data" - -#: includes/pages/admin_arrive.php:164 includes/pages/admin_arrive.php:179 -#: includes/pages/admin_arrive.php:194 -msgid "Count" -msgstr "Contar" - -#: includes/pages/admin_arrive.php:169 -msgid "Arrival statistics" -msgstr "EstatΓ­sticas de chegadas" - -#: includes/pages/admin_arrive.php:184 -msgid "Planned departure statistics" -msgstr "EstatΓ­sticas de saΓ­das planejadas" - -#: includes/pages/admin_free.php:4 -msgid "Free angels" -msgstr "Anjos livres" - -#: includes/pages/admin_free.php:81 includes/view/ShiftTypes_view.php:36 -#: includes/view/UserAngelTypes_view.php:70 -msgid "Angeltype" -msgstr "Tipo de anjo" - -#: includes/pages/admin_free.php:84 -msgid "Only confirmed" -msgstr "Somente confirmados" - -#: includes/pages/admin_free.php:92 includes/pages/guest_login.php:225 -#: includes/pages/guest_login.php:354 includes/view/AngelTypes_view.php:177 -#: includes/view/AngelTypes_view.php:190 includes/view/User_view.php:40 -#: includes/view/User_view.php:97 includes/view/User_view.php:181 -msgid "Nick" -msgstr "Apelido" - -#: includes/pages/admin_free.php:94 includes/pages/guest_login.php:255 -#: includes/view/AngelTypes_view.php:178 includes/view/AngelTypes_view.php:191 -#: includes/view/User_view.php:47 includes/view/User_view.php:184 -msgid "DECT" -msgstr "DECT" - -#: includes/pages/admin_free.php:95 includes/pages/guest_login.php:264 -#: includes/view/User_view.php:52 -msgid "Jabber" -msgstr "Jabber" - -#: includes/pages/admin_free.php:96 includes/pages/guest_login.php:228 -#: includes/view/User_view.php:49 includes/view/User_view.php:386 -msgid "E-Mail" -msgstr "E-Mail" - -#: includes/pages/admin_groups.php:4 -msgid "Grouprights" -msgstr "Direitos de grupo" - -#: includes/pages/admin_groups.php:29 includes/pages/admin_import.php:145 -#: includes/pages/admin_import.php:149 includes/pages/admin_rooms.php:143 -#: includes/pages/admin_rooms.php:189 includes/view/AngelTypes_view.php:66 -#: includes/view/AngelTypes_view.php:268 includes/view/ShiftTypes_view.php:35 -#: includes/view/ShiftTypes_view.php:78 includes/view/User_view.php:183 -msgid "Name" -msgstr "Nome" - -#: includes/pages/admin_groups.php:30 -msgid "Privileges" -msgstr "PrivilΓ©gios" - -#: includes/pages/admin_groups.php:55 -msgid "Edit group" -msgstr "Editar grupo" - -#: includes/pages/admin_import.php:4 includes/pages/admin_rooms.php:144 -#: includes/pages/admin_rooms.php:190 -msgid "Frab import" -msgstr "Importação Frab" - -#: includes/pages/admin_import.php:26 -msgid "Webserver has no write-permission on import directory." -msgstr "" -"O servidor web nΓ£o possui permissΓ£o de escrita no diretΓ³rio de importação." - -#: includes/pages/admin_import.php:54 includes/pages/admin_import.php:118 -#: includes/pages/admin_import.php:196 includes/pages/admin_shifts.php:53 -#: includes/pages/admin_shifts.php:59 -msgid "Please select a shift type." -msgstr "Por favor selecione um tipo de turno." - -#: includes/pages/admin_import.php:61 includes/pages/admin_import.php:125 -#: includes/pages/admin_import.php:203 -msgid "Please enter an amount of minutes to add to a talk's begin." -msgstr "" -"Por favor insira um nΓΊmero de minutos para adicionar ao inΓ­cio de uma " -"palestra." - -#: includes/pages/admin_import.php:68 includes/pages/admin_import.php:132 -#: includes/pages/admin_import.php:210 -msgid "Please enter an amount of minutes to add to a talk's end." -msgstr "" -"Por favor insira um nΓΊmero de minutos para adicionar ao tΓ©rmino de uma " -"palestra." - -#: includes/pages/admin_import.php:76 -msgid "No valid xml/xcal file provided." -msgstr "Nenhum arquivo xml/xcal vΓ‘lido foi fornecido." - -#: includes/pages/admin_import.php:81 -msgid "File upload went wrong." -msgstr "Falha no upload do arquivo." - -#: includes/pages/admin_import.php:85 -msgid "Please provide some data." -msgstr "Por favor insira alguns dados." - -#: includes/pages/admin_import.php:93 includes/pages/admin_import.php:140 -#: includes/pages/admin_import.php:253 -msgid "File Upload" -msgstr "Enviar arquivo" - -#: includes/pages/admin_import.php:93 includes/pages/admin_import.php:140 -#: includes/pages/admin_import.php:253 -msgid "Validation" -msgstr "Validação" - -#: includes/pages/admin_import.php:93 includes/pages/admin_import.php:102 -#: includes/pages/admin_import.php:140 includes/pages/admin_import.php:179 -#: includes/pages/admin_import.php:253 -msgid "Import" -msgstr "Importar" - -#: includes/pages/admin_import.php:97 -msgid "" -"This import will create/update/delete rooms and shifts by given FRAB-export " -"file. The needed file format is xcal." -msgstr "" -"Esta importação irΓ‘ criar/atualizar/deletar salas e turnos a partir do " -"arquivo FRAB-Export. O formato necessΓ‘rio Γ© xcal." - -#: includes/pages/admin_import.php:99 -msgid "Add minutes to start" -msgstr "Adicionar minutos ao inΓ­cio" - -#: includes/pages/admin_import.php:100 -msgid "Add minutes to end" -msgstr "Adicionar minutos ao fim" - -#: includes/pages/admin_import.php:101 -msgid "xcal-File (.xcal)" -msgstr "Adicionar minutos ao fim" - -#: includes/pages/admin_import.php:111 includes/pages/admin_import.php:185 -msgid "Missing import file." -msgstr "Arquivo para importar nΓ£o encontrado." - -#: includes/pages/admin_import.php:144 -msgid "Rooms to create" -msgstr "Salas para criar" - -#: includes/pages/admin_import.php:148 -msgid "Rooms to delete" -msgstr "Salas para apagar" - -#: includes/pages/admin_import.php:152 -msgid "Shifts to create" -msgstr "Turnos para criar" - -#: includes/pages/admin_import.php:154 includes/pages/admin_import.php:163 -#: includes/pages/admin_import.php:172 includes/view/User_view.php:366 -msgid "Day" -msgstr "Dia" - -#: includes/pages/admin_import.php:155 includes/pages/admin_import.php:164 -#: includes/pages/admin_import.php:173 includes/pages/admin_shifts.php:324 -#: includes/view/Shifts_view.php:66 -msgid "Start" -msgstr "InΓ­cio" - -#: includes/pages/admin_import.php:156 includes/pages/admin_import.php:165 -#: includes/pages/admin_import.php:174 includes/pages/admin_shifts.php:325 -#: includes/view/Shifts_view.php:74 -msgid "End" -msgstr "Fim" - -#: includes/pages/admin_import.php:157 includes/pages/admin_import.php:166 -#: includes/pages/admin_import.php:175 -msgid "Shift type" -msgstr "Tipo de turno" - -#: includes/pages/admin_import.php:159 includes/pages/admin_import.php:168 -#: includes/pages/admin_import.php:177 includes/pages/admin_shifts.php:321 -msgid "Room" -msgstr "Sala" - -#: includes/pages/admin_import.php:161 -msgid "Shifts to update" -msgstr "Turnos para atualizar" - -#: includes/pages/admin_import.php:170 -msgid "Shifts to delete" -msgstr "Turnos para deletar" - -#: includes/pages/admin_import.php:254 -msgid "It's done!" -msgstr "EstΓ‘ feito!" - -#: includes/pages/admin_log.php:4 -msgid "Log" -msgstr "Log" - -#: includes/pages/admin_news.php:10 -msgid "Edit news entry" -msgstr "Editar notΓ­cia" - -#: includes/pages/admin_news.php:31 -msgid "Author" -msgstr "Autor" - -#: includes/pages/admin_news.php:32 includes/pages/user_news.php:161 -msgid "Subject" -msgstr "Assunto" - -#: includes/pages/admin_news.php:34 includes/pages/user_news.php:163 -msgid "Meeting" -msgstr "ReuniΓ£o" - -#: includes/pages/admin_news.php:38 includes/pages/admin_rooms.php:177 -#: includes/view/User_view.php:129 -msgid "Delete" -msgstr "Apagar" - -#: includes/pages/admin_news.php:52 -msgid "News entry updated." -msgstr "NotΓ­cia atualizada." - -#: includes/pages/admin_news.php:61 -msgid "News entry deleted." -msgstr "NotΓ­cia deletada." - -#: includes/pages/admin_questions.php:4 -msgid "Answer questions" -msgstr "Responder perguntas" - -#: includes/pages/admin_questions.php:18 -msgid "There are unanswered questions!" -msgstr "Existem perguntas nΓ£o respondidas!" - -#: includes/pages/admin_questions.php:61 -msgid "Unanswered questions" -msgstr "Perguntas nΓ£o respondidas" - -#: includes/pages/admin_questions.php:63 includes/pages/admin_questions.php:70 -msgid "From" -msgstr "De" - -#: includes/pages/admin_questions.php:64 includes/pages/admin_questions.php:71 -#: includes/view/Questions_view.php:19 includes/view/Questions_view.php:24 -msgid "Question" -msgstr "QuestΓ£o" - -#: includes/pages/admin_questions.php:65 includes/pages/admin_questions.php:73 -#: includes/view/Questions_view.php:26 -msgid "Answer" -msgstr "Resposta" - -#: includes/pages/admin_questions.php:68 includes/view/Questions_view.php:22 -msgid "Answered questions" -msgstr "Perguntas respondidas" - -#: includes/pages/admin_questions.php:72 includes/view/Questions_view.php:25 -msgid "Answered by" -msgstr "Respondido por" - -#: includes/pages/admin_rooms.php:4 includes/pages/user_shifts.php:159 -#: includes/sys_menu.php:176 -msgid "Rooms" -msgstr "Salas" - -#: includes/pages/admin_rooms.php:67 -msgid "This name is already in use." -msgstr "Este nome jΓ‘ estΓ‘ em uso." - -#: includes/pages/admin_rooms.php:97 -#, php-format -msgid "Please enter needed angels for type %s." -msgstr "Por favor insira os anjos necessΓ‘rios de tipo %s." - -#: includes/pages/admin_rooms.php:124 -msgid "Room saved." -msgstr "Sala salva" - -#: includes/pages/admin_rooms.php:145 includes/pages/admin_rooms.php:191 -msgid "Public" -msgstr "PΓΊblico" - -#: includes/pages/admin_rooms.php:146 -msgid "Room number" -msgstr "NΓΊmero da sala" - -#: includes/pages/admin_rooms.php:151 -msgid "Needed angels:" -msgstr "Anjos necessΓ‘rios:" - -#: includes/pages/admin_rooms.php:167 -#, php-format -msgid "Room %s deleted." -msgstr "Sala %s deletada." - -#: includes/pages/admin_rooms.php:175 -#, php-format -msgid "Do you want to delete room %s?" -msgstr "VocΓͺ quer deletar as salas %s?" - -#: includes/pages/admin_rooms.php:185 -msgid "add" -msgstr "adicionar" - -#: includes/pages/admin_shifts.php:4 -msgid "Create shifts" -msgstr "Criar turnos" - -#: includes/pages/admin_shifts.php:71 -msgid "Please select a location." -msgstr "Por favor, selecione uma localização." - -#: includes/pages/admin_shifts.php:78 -msgid "Please select a start time." -msgstr "Por favor, selecione um horΓ‘rio de inΓ­cio." - -#: includes/pages/admin_shifts.php:85 -msgid "Please select an end time." -msgstr "Por favor, selecione um horΓ‘rio de tΓ©rmino." - -#: includes/pages/admin_shifts.php:90 -msgid "The shifts end has to be after its start." -msgstr "O tΓ©rmino do turno deve ser posterior ao seu inΓ­cio." - -#: includes/pages/admin_shifts.php:102 -msgid "Please enter a shift duration in minutes." -msgstr "Por favor insira a duração do turno em minutos." - -#: includes/pages/admin_shifts.php:110 -msgid "Please split the shift-change hours by colons." -msgstr "Por favor divida os horΓ‘rios de mudanΓ§a de turno por vΓ­rgulas." - -#: includes/pages/admin_shifts.php:115 -msgid "Please select a mode." -msgstr "Por favor selecione um modo." - -#: includes/pages/admin_shifts.php:128 -#, php-format -msgid "Please check the needed angels for team %s." -msgstr "Por favor confira os anjos necessΓ‘rios para o time %s." - -#: includes/pages/admin_shifts.php:133 -msgid "There are 0 angels needed. Please enter the amounts of needed angels." -msgstr "" -"HΓ‘ 0 anjos necessΓ‘rios. Por favor insira o nΓΊmero de anjos necessΓ‘rios." - -#: includes/pages/admin_shifts.php:137 -msgid "Please select a mode for needed angels." -msgstr "Por favor escolha um modo para os anjos necessΓ‘rios." - -#: includes/pages/admin_shifts.php:141 -msgid "Please select needed angels." -msgstr "Por favor selecione os anjos necessΓ‘rios." - -#: includes/pages/admin_shifts.php:268 -msgid "Time and location" -msgstr "HorΓ‘rio e localização" - -#: includes/pages/admin_shifts.php:269 -msgid "Type and title" -msgstr "Tipo e tΓ­tulo" - -#: includes/pages/admin_shifts.php:326 -msgid "Mode" -msgstr "Modo" - -#: includes/pages/admin_shifts.php:327 -msgid "Create one shift" -msgstr "Criar um turno" - -#: includes/pages/admin_shifts.php:328 -msgid "Create multiple shifts" -msgstr "Criar mΓΊltiplos turnos" - -#: includes/pages/admin_shifts.php:330 -msgid "Create multiple shifts with variable length" -msgstr "Criar mΓΊltiplos turnos com duração variΓ‘vel" - -#: includes/pages/admin_shifts.php:331 -msgid "Shift change hours" -msgstr "MudanΓ§a de horΓ‘rio do turno" - -#: includes/pages/admin_shifts.php:335 -msgid "Take needed angels from room settings" -msgstr "Pegar os anjos necessΓ‘rios a partir das configuraçáes das salas" - -#: includes/pages/admin_shifts.php:336 -msgid "The following angels are needed" -msgstr "Os seguintes anjos sΓ£o necessΓ‘rios" - -#: includes/pages/admin_user.php:4 -msgid "All Angels" -msgstr "Todos os anjos" - -#: includes/pages/admin_user.php:20 -msgid "This user does not exist." -msgstr "Esse usuΓ‘rio nΓ£o existe." - -#: includes/pages/admin_user.php:46 includes/view/AngelTypes_view.php:67 -#: includes/view/AngelTypes_view.php:68 includes/view/AngelTypes_view.php:69 -msgid "Yes" -msgstr "Sim" - -#: includes/pages/admin_user.php:47 includes/view/AngelTypes_view.php:67 -#: includes/view/AngelTypes_view.php:68 includes/view/AngelTypes_view.php:69 -msgid "No" -msgstr "NΓ£o" - -#: includes/pages/admin_user.php:60 -msgid "Force active" -msgstr "ForΓ§ar ativação" - -#: includes/pages/admin_user.php:79 -msgid "" -"Please visit the angeltypes page or the users profile to manage users " -"angeltypes." -msgstr "" -"Por favor visite a pΓ‘gina de tipos de anjo ou o perfil do usuΓ‘rio para " -"gerenciar\n" -"seus tipos de anjo." - -#: includes/pages/admin_user.php:204 -msgid "Edit user" -msgstr "Editar usuΓ‘rio" - -#: includes/pages/guest_credits.php:3 -msgid "Credits" -msgstr "CrΓ©ditos" - -#: includes/pages/guest_login.php:4 includes/pages/guest_login.php:349 -#: includes/pages/guest_login.php:356 includes/view/User_view.php:95 -#: includes/view/User_view.php:99 -msgid "Login" -msgstr "Login" - -#: includes/pages/guest_login.php:8 includes/pages/guest_login.php:285 -msgid "Register" -msgstr "Registrar" - -#: includes/pages/guest_login.php:12 -msgid "Logout" -msgstr "Logout" - -#: includes/pages/guest_login.php:56 -#, php-format -msgid "Your nick \"%s\" already exists." -msgstr "Seu apelido \"%s\" jΓ‘ existe." - -#: includes/pages/guest_login.php:60 -#, php-format -msgid "Your nick "%s" is too short (min. 2 characters)." -msgstr "Seu apelido "%s" Γ© muito pequeno (mΓ­nimo 2 caracteres)." - -#: includes/pages/guest_login.php:86 -msgid "Please check your jabber account information." -msgstr "Por favor verifique a informação da sua conta jabber." - -#: includes/pages/guest_login.php:95 -msgid "Please select your shirt size." -msgstr "Por favor escolha o tamanho da camisa." - -#: includes/pages/guest_login.php:106 -#, php-format -msgid "Your password is too short (please use at least %s characters)." -msgstr "Sua senha Γ© muito curta (por favor use pelo menos %s caracteres)." - -#: includes/pages/guest_login.php:115 -msgid "" -"Please enter your planned date of arrival. It should be after the buildup " -"start date and before teardown end date." -msgstr "" -"Por favor insira a data planejada para sua chegada. Ela deve ser posterior Γ  " -"data de montagem e anterior Γ  data de desmontagem." - -#: includes/pages/guest_login.php:189 -msgid "Angel registration successful!" -msgstr "Conta criada com sucesso!" - -#: includes/pages/guest_login.php:217 -msgid "" -"By completing this form you're registering as a Chaos-Angel. This script " -"will create you an account in the angel task scheduler." -msgstr "" -"Ao completar esse formulΓ‘rio vocΓͺ estarΓ‘ se registrando como um voluntΓ‘rio. " -"Esse script criarΓ‘ uma conta no sistema." - -#: includes/pages/guest_login.php:229 includes/view/User_view.php:50 -#, php-format -msgid "" -"The %s is allowed to send me an email (e.g. when my shifts change)" -msgstr "" -"Permito que o %s me envie emails (por exemplo, quando meus turnos " -"mudam)" - -#: includes/pages/guest_login.php:230 includes/view/User_view.php:51 -msgid "Allow heaven angels to contact you by e-mail." -msgstr "Permito que humanos me enviem emails" - -#: includes/pages/guest_login.php:235 includes/view/User_view.php:43 -msgid "Planned date of arrival" -msgstr "Data planejada de chegada" - -#: includes/pages/guest_login.php:238 includes/view/User_view.php:54 -msgid "Shirt size" -msgstr "Tamanho da camiseta" - -#: includes/pages/guest_login.php:243 includes/pages/guest_login.php:355 -#: includes/view/User_view.php:98 includes/view/User_view.php:400 -msgid "Password" -msgstr "Senha" - -#: includes/pages/guest_login.php:246 includes/view/User_view.php:401 -msgid "Confirm password" -msgstr "Confirme a senha" - -#: includes/pages/guest_login.php:249 -msgid "What do you want to do?" -msgstr "O que vocΓͺ gostaria de fazer?" - -#: includes/pages/guest_login.php:249 -msgid "Description of job types" -msgstr "Descrição dos trabalhos" - -#: includes/pages/guest_login.php:250 -msgid "" -"Restricted angel types need will be confirmed later by a supporter. You can " -"change your selection in the options section." -msgstr "" -"Tipos de anjo restritos precisam de confirmação posterior de um apoiador. " -"VocΓͺ pode \n" -"mudar sua seleção na seção 'Opçáes'." - -#: includes/pages/guest_login.php:258 includes/view/User_view.php:48 -msgid "Mobile" -msgstr "Celular" - -#: includes/pages/guest_login.php:261 includes/view/User_view.php:46 -msgid "Phone" -msgstr "Telefone" - -#: includes/pages/guest_login.php:267 includes/view/User_view.php:42 -msgid "First name" -msgstr "Nome" - -#: includes/pages/guest_login.php:270 includes/view/User_view.php:41 -msgid "Last name" -msgstr "Sobrenome" - -#: includes/pages/guest_login.php:275 includes/view/User_view.php:45 -msgid "Age" -msgstr "Idade" - -#: includes/pages/guest_login.php:278 includes/view/User_view.php:53 -msgid "Hometown" -msgstr "Cidade" - -#: includes/pages/guest_login.php:281 includes/view/User_view.php:39 -msgid "Entry required!" -msgstr "Campo necessΓ‘rio!" - -#: includes/pages/guest_login.php:319 -msgid "auth.no-password" -msgstr "Por favor digite uma senha." - -#: includes/pages/guest_login.php:323 -msgid "auth.not-found" -msgstr "" -"Nenhum usuΓ‘rio foi encontrado. Por favor tente novamente. \n" -"Se vocΓͺ continuar com problemas, pergunte a um Dispatcher." - -#: includes/pages/guest_login.php:327 -msgid "auth.no-nickname" -msgstr "Por favor digite um apelido." - -#: includes/pages/guest_login.php:358 includes/view/User_view.php:101 -msgid "I forgot my password" -msgstr "Esqueci minha senha" - -#: includes/pages/guest_login.php:363 includes/view/User_view.php:103 -msgid "Please note: You have to activate cookies!" -msgstr "Nota: vocΓͺ precisa habilitar cookies!" - -#: includes/pages/guest_login.php:374 includes/view/User_view.php:107 -msgid "What can I do?" -msgstr "O que posso fazer?" - -#: includes/pages/guest_login.php:375 includes/view/User_view.php:108 -msgid "Please read about the jobs you can do to help us." -msgstr "Por favor leia sobre as tarefas que pode realizar para nos ajudar." - -#: includes/pages/guest_login.php:390 -msgid "Please sign up, if you want to help us!" -msgstr "Se vocΓͺ quer nos ajudar, por favor se registre!" - -#: includes/pages/user_messages.php:26 -msgid "Select recipient..." -msgstr "Selecionar recipiente..." - -#: includes/pages/user_messages.php:62 -msgid "mark as read" -msgstr "marcar como lido" - -#: includes/pages/user_messages.php:65 -msgid "delete message" -msgstr "apagar mensagem" - -#: includes/pages/user_messages.php:72 -#, php-format -msgid "Hello %s, here can you leave messages for other angels" -msgstr "Oi %s, aqui vocΓͺ pode deixar mensagens para outros anjos." - -#: includes/pages/user_messages.php:75 -msgid "New" -msgstr "Nova" - -#: includes/pages/user_messages.php:77 -msgid "Transmitted" -msgstr "Enviado" - -#: includes/pages/user_messages.php:78 -msgid "Recipient" -msgstr "Recipiente" - -#: includes/pages/user_messages.php:90 includes/pages/user_messages.php:106 -msgid "Incomplete call, missing Message ID." -msgstr "Chamada incompleta, falta o ID da Mensagem." - -#: includes/pages/user_messages.php:98 includes/pages/user_messages.php:114 -msgid "No Message found." -msgstr "Nenhuma mensagem encontrada." - -#: includes/pages/user_messages.php:122 -msgid "Transmitting was terminated with an Error." -msgstr "TransmissΓ£o encerrada com um erro." - -#: includes/pages/user_messages.php:127 -msgid "Wrong action." -msgstr "Ação errada." - -#: includes/pages/user_myshifts.php:23 -msgid "Key changed." -msgstr "Chave modificada." - -#: includes/pages/user_myshifts.php:26 includes/view/User_view.php:335 -msgid "Reset API key" -msgstr "Resetar a chave API" - -#: includes/pages/user_myshifts.php:27 -msgid "" -"If you reset the key, the url to your iCal- and JSON-export and your atom/rss " -"feed changes! You have to update it in every application using one of these " -"exports." -msgstr "" -"Se vocΓͺ reconfigurar a chave, as urls para seu iCal, a exportação em formato " -"JSON \n" -"e seu feed atom/rss serΓ‘ alterada! VocΓͺ precisarΓ‘ atualizΓ‘-las em todas as " -"aplicaçáes que estiverem \n" -"usando uma delas." - -#: includes/pages/user_myshifts.php:28 -msgid "Continue" -msgstr "Continuar" - -#: includes/pages/user_myshifts.php:60 -msgid "Please enter a freeload comment!" -msgstr "Por favor insira um comentΓ‘rio freeload!" - -#: includes/pages/user_myshifts.php:79 -msgid "Shift saved." -msgstr "Turno salvo." - -#: includes/pages/user_myshifts.php:107 -msgid "Shift canceled." -msgstr "Turno cancelado." - -#: includes/pages/user_myshifts.php:109 -msgid "" -"It's too late to sign yourself off the shift. If neccessary, ask the " -"dispatcher to do so." -msgstr "" -"EstΓ‘ muito tarde para se retirar deste turno. Se necessΓ‘rio, peΓ§a a \n" -"um dispatcher para removΓͺ-lo." - -#: includes/pages/user_news.php:4 -msgid "News comments" -msgstr "Novos comentΓ‘rios" - -#: includes/pages/user_news.php:8 -msgid "News" -msgstr "Novidades" - -#: includes/pages/user_news.php:12 -msgid "Meetings" -msgstr "Encontros" - -#: includes/pages/user_news.php:68 -msgid "Comments" -msgstr "ComentΓ‘rios" - -#: includes/pages/user_news.php:86 includes/pages/user_news.php:127 -msgid "Entry saved." -msgstr "Entrada salva." - -#: includes/pages/user_news.php:104 -msgid "New Comment:" -msgstr "Novo comentΓ‘rio:" - -#: includes/pages/user_news.php:110 -msgid "Invalid request." -msgstr "Requisição invΓ‘lida." - -#: includes/pages/user_news.php:158 -msgid "Create news:" -msgstr "Criar novidades:" - -#: includes/pages/user_questions.php:4 includes/view/Questions_view.php:29 -msgid "Ask the Heaven" -msgstr "Pergunte ao paraΓ­so" - -#: includes/pages/user_questions.php:27 -msgid "Unable to save question." -msgstr "NΓ£o foi possΓ­vel salvar a sua pergunta." - -#: includes/pages/user_questions.php:29 -msgid "You question was saved." -msgstr "Sua pergunta foi salva." - -#: includes/pages/user_questions.php:33 -msgid "Please enter a question!" -msgstr "Por favor digite sua dΓΊvida!" - -#: includes/pages/user_questions.php:41 -msgid "Incomplete call, missing Question ID." -msgstr "Chamada incompletada, falta o ID da pergunta." - -#: includes/pages/user_questions.php:50 -msgid "No question found." -msgstr "Nenhuma dΓΊvida encontrada." - -#: includes/view/User_view.php:332 -msgid "Settings" -msgstr "Configuraçáes" - -msgid "" -"Please enter your planned date of departure. It should be after your planned " -"arrival date and after buildup start date and before teardown end date." -msgstr "" -"Por favor digite sua data de saΓ­da. Ela deve ser posterior Γ  data de " -"chegada\n" -"e ao inΓ­cio da montagem, e anterior Γ  data de desmontagem." - -msgid "-> not OK. Please try again." -msgstr "-> nΓ£o OK. Por favor tente novamente." - -msgid "Failed setting password." -msgstr "A alteração da senha falhaou." - -#: includes/pages/user_shifts.php:82 -msgid "The administration has not configured any rooms yet." -msgstr "O administrador nΓ£o configurou nenhuma sala ainda." - -#: includes/pages/user_shifts.php:94 -msgid "The administration has not configured any shifts yet." -msgstr "O administrador nΓ£o configurou nenhum turno ainda." - -#: includes/pages/user_shifts.php:104 -msgid "" -"The administration has not configured any angeltypes yet - or you are not " -"subscribed to any angeltype." -msgstr "" -"O administrador ainda nΓ£o configurou os tipos de anjos - ou vocΓͺ nΓ£o estΓ‘\n" -"inscrito em nenhum tipo de anjo." - -#: includes/pages/user_shifts.php:142 -msgid "occupied" -msgstr "ocupado" - -#: includes/pages/user_shifts.php:146 -msgid "free" -msgstr "livre" - -#: includes/pages/user_shifts.php:165 -msgid "Occupancy" -msgstr "Ocupação" - -#: includes/pages/user_shifts.php:166 -msgid "" -"The tasks shown here are influenced by the angeltypes you joined already!" -msgstr "" -"As tarefas mostradas aqui sΓ£o influenciadas pelos tipos de anjos de que vocΓͺ " -"jΓ‘ faz parte!" - -#: includes/pages/user_shifts.php:166 -msgid "Description of the jobs." -msgstr "Descrição dos trabalhos." - -#: includes/pages/user_shifts.php:168 -msgid "iCal export" -msgstr "Exportar iCal" - -#: includes/pages/user_shifts.php:168 -#, php-format -msgid "" -"Export of shown shifts. iCal format or JSON format available (please keep secret, otherwise reset the api key)." -msgstr "" -"Exportar os turnos mostrados. formato iCal ou formato JSON disponΓ­veis (por favor mantenha secreto, ou resete a chave API)." - -#: includes/pages/user_shifts.php:169 -msgid "Filter" -msgstr "Filtro" - -#: includes/pages/user_shifts.php:191 includes/view/ShiftTypes_view.php:23 -msgid "All" -msgstr "Todos" - -#: includes/pages/user_shifts.php:192 -msgid "None" -msgstr "Nenhum" - -#: includes/sys_menu.php:139 -msgid "Admin" -msgstr "Admin" - -#: includes/sys_menu.php:163 -msgid "Manage rooms" -msgstr "Gerenciar salas" - -#: includes/sys_template.php:166 -msgid "No data found." -msgstr "Nenhum dado encontrado." - -#: includes/view/AngelTypes_view.php:27 includes/view/AngelTypes_view.php:244 -msgid "Unconfirmed" -msgstr "NΓ£o confirmado" - -#: includes/view/AngelTypes_view.php:29 includes/view/AngelTypes_view.php:33 -msgid "Supporter" -msgstr "Apoiador" - -#: includes/view/AngelTypes_view.php:31 includes/view/AngelTypes_view.php:35 -msgid "Member" -msgstr "Membro" - -#: includes/view/AngelTypes_view.php:42 -#, php-format -msgid "Do you want to delete angeltype %s?" -msgstr "VocΓͺ deseja apagar o tipo de anjo %s?" - -#: includes/view/AngelTypes_view.php:44 includes/view/ShiftTypes_view.php:15 -#: includes/view/UserAngelTypes_view.php:8 -#: includes/view/UserAngelTypes_view.php:19 -#: includes/view/UserAngelTypes_view.php:30 -#: includes/view/UserAngelTypes_view.php:41 -#: includes/view/UserAngelTypes_view.php:52 -#: includes/view/UserAngelTypes_view.php:82 -msgid "cancel" -msgstr "cancelar" - -#: includes/view/AngelTypes_view.php:67 includes/view/AngelTypes_view.php:269 -msgid "Restricted" -msgstr "Restrito" - -#: includes/view/AngelTypes_view.php:68 -msgid "No Self Sign Up" -msgstr "Auto inscrição nΓ£o permitida" - -#: includes/view/AngelTypes_view.php:69 -msgid "Requires driver license" -msgstr "Requer carteira de motorista" - -#: includes/view/AngelTypes_view.php:73 -msgid "" -"Restricted angel types can only be used by an angel if enabled by a " -"supporter (double opt-in)." -msgstr "" -"Tipos de anjo restritos sΓ³ podem ser usados por um anjo quando autorizados " -"por \n" -"um apoiador (duplo opt-in)." - -#: includes/view/AngelTypes_view.php:74 includes/view/AngelTypes_view.php:205 -#: includes/view/ShiftTypes_view.php:37 includes/view/ShiftTypes_view.php:58 -#: includes/view/Shifts_view.php:92 -msgid "Description" -msgstr "Descrição" - -#: includes/view/AngelTypes_view.php:75 includes/view/ShiftTypes_view.php:38 -msgid "Please use markdown for the description." -msgstr "Por favor use markdown para a descrição." - -#: includes/view/AngelTypes_view.php:90 -msgid "my driving license" -msgstr "Minha carteira de motorista" - -#: includes/view/AngelTypes_view.php:97 -msgid "" -"This angeltype requires a driver license. Please enter your driver license " -"information!" -msgstr "" -"Esse tipo de anjo requer carteira de motorista. Por favor digite sua " -"carteira de motorista." - -#: includes/view/AngelTypes_view.php:101 -#, php-format -msgid "" -"You are unconfirmed for this angeltype. Please go to the introduction for %s " -"to get confirmed." -msgstr "" -"VocΓͺ nΓ£o estΓ‘ confirmado para esse tipo de anjo. Por favor vΓ‘ para a " -"apresentação para %s\n" -"para ser confirmado." - -#: includes/view/AngelTypes_view.php:140 -msgid "confirm" -msgstr "confirmar" - -#: includes/view/AngelTypes_view.php:141 -msgid "deny" -msgstr "rejeitar" - -#: includes/view/AngelTypes_view.php:157 -msgid "remove" -msgstr "remover" - -#: includes/view/AngelTypes_view.php:179 -msgid "Driver" -msgstr "Motorista" - -#: includes/view/AngelTypes_view.php:180 -msgid "Has car" -msgstr "Tem carro" - -#: includes/view/AngelTypes_view.php:181 -#: includes/view/UserDriverLicenses_view.php:27 -msgid "Car" -msgstr "Carro" - -#: includes/view/AngelTypes_view.php:182 -msgid "3,5t Transporter" -msgstr "Transporte 3,5t" - -#: includes/view/AngelTypes_view.php:183 -msgid "7,5t Truck" -msgstr "CaminhΓ£o 7,5t" - -#: includes/view/AngelTypes_view.php:184 -msgid "12t Truck" -msgstr "CaminhΓ£o 12t" - -#: includes/view/AngelTypes_view.php:185 -#: includes/view/UserDriverLicenses_view.php:31 -msgid "Forklift" -msgstr "Empilhadeira" - -#: includes/view/AngelTypes_view.php:215 -msgid "Supporters" -msgstr "Apoiadores" - -#: includes/view/AngelTypes_view.php:235 -msgid "Members" -msgstr "Membros" - -#: includes/view/AngelTypes_view.php:238 -#: includes/view/UserAngelTypes_view.php:72 -msgid "Add" -msgstr "Adicionar" - -#: includes/view/AngelTypes_view.php:246 -msgid "confirm all" -msgstr "confirmar todos" - -#: includes/view/AngelTypes_view.php:247 -msgid "deny all" -msgstr "rejeitar todos" - -#: includes/view/AngelTypes_view.php:264 -msgid "New angeltype" -msgstr "Novo tipo de anjo" - -#: includes/view/AngelTypes_view.php:270 -msgid "Self Sign Up Allowed" -msgstr "Auto Inscrição autorizada" - -#: includes/view/AngelTypes_view.php:271 -msgid "Membership" -msgstr "Membro" - -#: includes/view/AngelTypes_view.php:296 -msgid "" -"This angeltype is restricted by double-opt-in by a team supporter. Please " -"show up at the according introduction meetings." -msgstr "" -"Esse tipo de anjo Γ© restrito por um opt-in duplo por um time de apoio. Por " -"favor\n" -"compareΓ§a de acordo com os encontros de introdução." - -#: includes/view/AngelTypes_view.php:317 -msgid "FAQ" -msgstr "FAQ" - -#: includes/view/AngelTypes_view.php:319 -msgid "" -"Here is the list of teams and their tasks. If you have questions, read the " -"FAQ." -msgstr "" -"Aqui estΓ‘ uma lista dos times e suas tarefas. Se vocΓͺ tem dΓΊvidas, leia a\n" -"FAQ." - -#: includes/view/EventConfig_view.php:10 includes/view/EventConfig_view.php:18 -#, php-format -msgid "Welcome to the %s!" -msgstr "Bem vindo a %s!" - -#: includes/view/EventConfig_view.php:24 -msgid "Buildup starts" -msgstr "InΓ­cio da montagem" - -#: includes/view/EventConfig_view.php:26 includes/view/EventConfig_view.php:34 -#: includes/view/EventConfig_view.php:42 includes/view/EventConfig_view.php:50 -#: includes/view/EventConfig_view.php:67 includes/view/EventConfig_view.php:72 -#: includes/view/EventConfig_view.php:77 includes/view/Shifts_view.php:68 -#: includes/view/Shifts_view.php:76 -msgid "Y-m-d" -msgstr "d/m/Y" - -#: includes/view/EventConfig_view.php:32 -msgid "Event starts" -msgstr "O evento comeΓ§a em" - -#: includes/view/EventConfig_view.php:40 -msgid "Event ends" -msgstr "O evento termina em" - -#: includes/view/EventConfig_view.php:48 -msgid "Teardown ends" -msgstr "Desmontagem termina" - -#: includes/view/EventConfig_view.php:67 -#, php-format -msgid "%s, from %s to %s" -msgstr "%s, de %s para %s" - -#: includes/view/EventConfig_view.php:72 -#, php-format -msgid "%s, starting %s" -msgstr "%s, comeΓ§ando %s" - -#: includes/view/EventConfig_view.php:77 -#, php-format -msgid "Event from %s to %s" -msgstr "Evento de %s para %s" - -#: includes/view/EventConfig_view.php:106 -msgid "Event Name" -msgstr "Nome do evento" - -#: includes/view/EventConfig_view.php:107 -msgid "Event Name is shown on the start page." -msgstr "Nome do evento Γ© mostrado na pΓ‘gina inicial." - -#: includes/view/EventConfig_view.php:108 -msgid "Event Welcome Message" -msgstr "Mensagem de boas vindas do evento" - -#: includes/view/EventConfig_view.php:109 -msgid "" -"Welcome message is shown after successful registration. You can use markdown." -msgstr "" -"A mensagem de boas vindas Γ© mostrada apΓ³s a inscrição. VocΓͺ pode usar " -"markdown." - -#: includes/view/EventConfig_view.php:112 -msgid "Buildup date" -msgstr "Data da montagem" - -#: includes/view/EventConfig_view.php:113 -msgid "Event start date" -msgstr "Data de inΓ­cio do evento" - -#: includes/view/EventConfig_view.php:116 -msgid "Teardown end date" -msgstr "Data da desmontagem" - -#: includes/view/EventConfig_view.php:117 -msgid "Event end date" -msgstr "Data de tΓ©rmino do evento" - -#: includes/view/Questions_view.php:17 -msgid "Open questions" -msgstr "DΓΊvidas abertas" - -#: includes/view/Questions_view.php:31 -msgid "Your Question:" -msgstr "Sua dΓΊvida:" - -#: includes/view/ShiftCalendarRenderer.php:209 includes/view/User_view.php:367 -msgid "Time" -msgstr "Hora" - -#: includes/view/ShiftCalendarRenderer.php:248 -msgid "Your shift" -msgstr "Seu turno" - -#: includes/view/ShiftCalendarRenderer.php:249 -msgid "Help needed" -msgstr "Necessita ajuda" - -#: includes/view/ShiftCalendarRenderer.php:250 -msgid "Other angeltype needed / collides with my shifts" -msgstr "Outro tipo de anjo necessΓ‘rio / colide com seus turnos" - -#: includes/view/ShiftCalendarRenderer.php:251 -msgid "Shift is full" -msgstr "O turno estΓ‘ lotado" - -#: includes/view/ShiftCalendarRenderer.php:252 -msgid "Shift running/ended" -msgstr "Turno em andamento/finalizado" - -#: includes/view/ShiftCalendarShiftRenderer.php:96 -msgid "Add more angels" -msgstr "Adicionar mais anjos" - -#: includes/view/ShiftCalendarShiftRenderer.php:127 -#, php-format -msgid "%d helper needed" -msgid_plural "%d helpers needed" -msgstr[0] "%d necessita de ajuda" -msgstr[1] "%d necessitam de ajuda" - -#: includes/view/ShiftCalendarShiftRenderer.php:132 -#: includes/view/Shifts_view.php:23 -msgid "Sign up" -msgstr "Registrar" - -#: includes/view/ShiftCalendarShiftRenderer.php:137 -msgid "ended" -msgstr "encerrado" - -#: includes/view/ShiftCalendarShiftRenderer.php:146 -#: includes/view/Shifts_view.php:25 -#, php-format -msgid "Become %s" -msgstr "Tornar-se %s" - -#: includes/view/ShiftEntry_view.php:18 includes/view/User_view.php:267 -#: includes/view/User_view.php:269 -msgid "Freeloaded" -msgstr "Freeloaded" - -#: includes/view/ShiftEntry_view.php:19 -msgid "Freeload comment (Only for shift coordination):" -msgstr "ComentΓ‘rio freeload (Apenas para coordenação de turno):" - -#: includes/view/ShiftEntry_view.php:22 -msgid "Edit shift entry" -msgstr "Editar entrada de turno" - -#: includes/view/ShiftEntry_view.php:25 -msgid "Angel:" -msgstr "Anjo:" - -#: includes/view/ShiftEntry_view.php:26 -msgid "Date, Duration:" -msgstr "Data, Duração:" - -#: includes/view/ShiftEntry_view.php:27 -msgid "Location:" -msgstr "Local:" - -#: includes/view/ShiftEntry_view.php:28 -msgid "Title:" -msgstr "TΓ­tulo:" - -#: includes/view/ShiftEntry_view.php:29 -msgid "Type:" -msgstr "Tipo:" - -#: includes/view/ShiftEntry_view.php:30 -msgid "Comment (for your eyes only):" -msgstr "ComentΓ‘rio (apenas para ler):" - -#: includes/view/ShiftTypes_view.php:13 -#, php-format -msgid "Do you want to delete shifttype %s?" -msgstr "VocΓͺ quer apagar o turno %s?" - -#: includes/view/ShiftTypes_view.php:29 -msgid "Edit shifttype" -msgstr "Editar tipo de turno" - -#: includes/view/ShiftTypes_view.php:29 -msgid "Create shifttype" -msgstr "Criar tipo de turno" - -#: includes/view/ShiftTypes_view.php:48 -#, php-format -msgid "for team %s" -msgstr "para o time %s" - -#: includes/view/ShiftTypes_view.php:75 -msgid "New shifttype" -msgstr "Novo tipo de turno" - -#: includes/view/Shifts_view.php:7 -#, php-format -msgid "created at %s by %s" -msgstr "criado em %s por %s" - -#: includes/view/Shifts_view.php:10 -#, php-format -msgid "edited at %s by %s" -msgstr "editado em %s por %s" - -#: includes/view/Shifts_view.php:52 -msgid "This shift collides with one of your shifts." -msgstr "Esse turno colide com um dos seus outros turnos." - -#: includes/view/Shifts_view.php:53 -msgid "You are signed up for this shift." -msgstr "VocΓͺ foi designado para esse turno." - -#: includes/view/Shifts_view.php:82 includes/view/User_view.php:368 -msgid "Location" -msgstr "Local" - -#: includes/view/UserAngelTypes_view.php:6 -#, php-format -msgid "Do you really want to add supporter rights for %s to %s?" -msgstr "VocΓͺ realmente quer dar permissΓ£o de apoiador de %s para %s?" - -#: includes/view/UserAngelTypes_view.php:6 -#, php-format -msgid "Do you really want to remove supporter rights for %s from %s?" -msgstr "VocΓͺ realmente quer remover a permissΓ£o de apoiador de %s para %s?" - -#: includes/view/UserAngelTypes_view.php:17 -#, php-format -msgid "Do you really want to deny all users for %s?" -msgstr "VocΓͺ realmente quer negar todos os usuΓ‘rios para %s?" - -#: includes/view/UserAngelTypes_view.php:28 -#, php-format -msgid "Do you really want to confirm all users for %s?" -msgstr "VocΓͺ realmente quer confirmar todos os usuΓ‘rios para %s?" - -#: includes/view/UserAngelTypes_view.php:39 -#, php-format -msgid "Do you really want to confirm %s for %s?" -msgstr "VocΓͺ realmente quer confirmar %s para %s?" - -#: includes/view/UserAngelTypes_view.php:50 -#, php-format -msgid "Do you really want to delete %s from %s?" -msgstr "VocΓͺ realmente quer apagar %s de %s?" - -#: includes/view/UserAngelTypes_view.php:71 -msgid "User" -msgstr "UsuΓ‘rio" - -#: includes/view/UserAngelTypes_view.php:80 -#, php-format -msgid "Do you really want to add %s to %s?" -msgstr "VocΓͺ realmente quer adicionar %s em %s?" - -#: includes/view/UserAngelTypes_view.php:83 -msgid "save" -msgstr "salvar" - -#: includes/view/UserDriverLicenses_view.php:17 -msgid "Back to profile" -msgstr "Voltar para o perfil" - -#: includes/view/UserDriverLicenses_view.php:21 -msgid "Privacy" -msgstr "Privacidade" - -#: includes/view/UserDriverLicenses_view.php:21 -msgid "" -"Your driving license information is only visible for supporters and admins." -msgstr "" -"Os dados da sua carteira de motorista sΓ£o apenas visΓ­veis para os apoiadores " -"e os admins." - -#: includes/view/UserDriverLicenses_view.php:22 -msgid "I am willing to drive a car for the event" -msgstr "Eu desejo dirigir carros para o evento" - -#: includes/view/UserDriverLicenses_view.php:25 -msgid "" -"I have my own car with me and am willing to use it for the event (You'll get " -"reimbursed for fuel)" -msgstr "" -"Eu tenho meu prΓ³prio carro e estou disposto a usΓ‘-lo no evento\n" -"(VocΓͺ terΓ‘ o combustΓ­vel reembolsado)" - -#: includes/view/UserDriverLicenses_view.php:26 -msgid "Driver license" -msgstr "Carteira de motorista" - -#: includes/view/UserDriverLicenses_view.php:28 -msgid "Transporter 3,5t" -msgstr "Transportador 3,5t" - -#: includes/view/UserDriverLicenses_view.php:29 -msgid "Truck 7,5t" -msgstr "CaminhΓ£o 7,5t" - -#: includes/view/UserDriverLicenses_view.php:30 -msgid "Truck 12t" -msgstr "CaminhΓ£o 12t" - -#: includes/view/User_view.php:7 -msgid "Please select..." -msgstr "Por favor selecione..." - -#: includes/view/User_view.php:38 -msgid "Here you can change your user details." -msgstr "Aqui vocΓͺ pode mudar os seus detalhes de usuΓ‘rio." - -#: includes/view/User_view.php:44 -msgid "Planned date of departure" -msgstr "Data planejada para saΓ­da" - -#: includes/view/User_view.php:55 -msgid "Please visit the angeltypes page to manage your angeltypes." -msgstr "" -"Por favor visite a pΓ‘gina de tipos de anjo para gerenciar os tipos de anjo" - -#: includes/view/User_view.php:61 -msgid "Here you can change your password." -msgstr "Aqui vocΓͺ pode mudar sua senha." - -#: includes/view/User_view.php:62 -msgid "Old password:" -msgstr "Senha antiga:" - -#: includes/view/User_view.php:63 -msgid "New password:" -msgstr "Nova senha:" - -#: includes/view/User_view.php:64 -msgid "Password confirmation:" -msgstr "Confirmação de senha:" - -#: includes/view/User_view.php:88 -msgid "Registration successful" -msgstr "Registrado com sucesso" - -#: includes/view/User_view.php:126 -msgid "" -"Do you really want to delete the user including all his shifts and every " -"other piece of his data?" -msgstr "" -"VocΓͺ realmente quer apagar o usuΓ‘rio, incluindo todos seus turnos e todos\n" -"os seus dados?" - -#: includes/view/User_view.php:128 -msgid "Your password" -msgstr "Sua senha" - -#: includes/view/User_view.php:143 -#, php-format -msgid "Angel should receive at least %d vouchers." -msgstr "Um anjo deve receber no mΓ­nimo %d vouchers." - -#: includes/view/User_view.php:145 -msgid "Number of vouchers given out" -msgstr "NΓΊmero de vouchers dados" - -#: includes/view/User_view.php:159 -msgid "m/d/Y h:i a" -msgstr "d/m/Y H:i" - -#: includes/view/User_view.php:178 -msgid "New user" -msgstr "Novo usuΓ‘rio" - -#: includes/view/User_view.php:182 -msgid "Prename" -msgstr "Nome" - -#: includes/view/User_view.php:185 includes/view/User_view.php:350 -msgid "Arrived" -msgstr "Chegou" - -#: includes/view/User_view.php:186 -msgid "Voucher" -msgstr "Voucher" - -#: includes/view/User_view.php:187 -msgid "Freeloads" -msgstr "Freeloads" - -#: includes/view/User_view.php:188 includes/view/User_view.php:352 -msgid "Active" -msgstr "Ativo" - -#: includes/view/User_view.php:190 includes/view/User_view.php:353 -msgid "T-Shirt" -msgstr "Camiseta" - -#: includes/view/User_view.php:192 -msgid "Last login" -msgstr "Último login" - -#: includes/view/User_view.php:209 -msgid "Free" -msgstr "Livre" - -#: includes/view/User_view.php:214 includes/view/User_view.php:216 -#, php-format -msgid "Next shift %c" -msgstr "PrΓ³ximo turno %c" - -#: includes/view/User_view.php:221 -#, php-format -msgid "Shift starts %c" -msgstr "Turno inicia em %c" - -#: includes/view/User_view.php:223 -#, php-format -msgid "Shift ends %c" -msgstr "Turno termina em %c" - -#: includes/view/User_view.php:280 -msgid "sign off" -msgstr "sair" - -#: includes/view/User_view.php:305 -msgid "Sum:" -msgstr "Soma:" - -#: includes/view/User_view.php:329 -msgid "driving license" -msgstr "carteira de motorista" - -#: includes/view/User_view.php:331 -msgid "Edit vouchers" -msgstr "Editar vouchers" - -#: includes/view/User_view.php:333 -msgid "iCal Export" -msgstr "Exportar iCal" - -#: includes/view/User_view.php:334 -msgid "JSON Export" -msgstr "Exportar em formato JSON" - -#: includes/view/User_view.php:347 -msgid "User state" -msgstr "Status" - -#: includes/view/User_view.php:350 -#, php-format -msgid "Arrived at %s" -msgstr "Chegous Γ s %s" - -#: includes/view/User_view.php:350 -#, php-format -msgid "Not arrived (Planned: %s)" -msgstr "NΓ£o chegou (Planejado: %s)" - -#: includes/view/User_view.php:350 -msgid "Not arrived" -msgstr "NΓ£o chegou" - -#: includes/view/User_view.php:351 -#, php-format -msgid "Got %s voucher" -msgid_plural "Got %s vouchers" -msgstr[0] "Einen Voucher bekommen" -msgstr[1] "%s Voucher bekommen" - -#: includes/view/User_view.php:351 -msgid "Got no vouchers" -msgstr "Pegou voucher %s" - -#: includes/view/User_view.php:360 -msgid "Rights" -msgstr "PermissΓ΅es" - -#: includes/view/User_view.php:369 -msgid "Name & workmates" -msgstr "Nome & colegas" - -#: includes/view/User_view.php:370 -msgid "Comment" -msgstr "Comentar" - -#: includes/view/User_view.php:371 -msgid "Action" -msgstr "Ação" - -#: includes/view/User_view.php:373 -#, php-format -msgid "Your night shifts between %d and %d am count twice." -msgstr "Os seus turnos noturnos entre %dh e %dh contam como dois." - -#: includes/view/User_view.php:374 -#, php-format -msgid "" -"Go to the shifts table to sign yourself up for some " -"shifts." -msgstr "" -"VΓ‘ para a tabela de turnos para se inscrever em alguns\n" -"turnos." - -#: includes/view/User_view.php:384 -msgid "" -"We will send you an e-mail with a password recovery link. Please use the " -"email address you used for registration." -msgstr "" -"NΓ³s enviaremos um email com um link para recuperação de senha. Por favor use " -"o \n" -"endereΓ§o de email informado no seu registro." - -#: includes/view/User_view.php:387 -msgid "Recover" -msgstr "Recuperar" - -#: includes/view/User_view.php:398 -msgid "Please enter a new password." -msgstr "Por favor digite uma nova senha." - -#: includes/view/User_view.php:447 -msgid "" -"Please enter your planned date of departure on your settings page to give us " -"a feeling for teardown capacities." -msgstr "" -"Por favor digite sua data planejada de saΓ­da na sua pΓ‘gina de configuraçáes " -"para\n" -"termos uma noção da nossa capacidade de desmontagem." - -#: includes/view/User_view.php:457 -#, php-format -msgid "" -"You freeloaded at least %s shifts. Shift signup is locked. Please go to " -"heavens desk to be unlocked again." -msgstr "" -"VocΓͺ deixou de participar de pelo menos %s dos turnos. O registro em turnos " -"estΓ‘ suspenso.\n" -"Por favor vΓ‘ atΓ© a mesa do paraΓ­so para ser desbloqueado novamente." - -#: includes/view/User_view.php:468 -msgid "" -"You are not marked as arrived. Please go to heaven's desk, get your angel " -"badge and/or tell them that you arrived already." -msgstr "" -"Sua chegada nΓ£o foi marcada. Por favor vΓ‘ atΓ© a mesa do paraΓ­so, pegue sua " -"credencial\n" -"de anjo e/ou informe que vocΓͺ jΓ‘ chegou." - -#: includes/view/User_view.php:478 -msgid "You need to specify a tshirt size in your settings!" -msgstr "VocΓͺ precisa especificar o tamanho de camiseta nas suas configuraçáes!" - -#: includes/view/User_view.php:488 -msgid "" -"You need to specify a DECT phone number in your settings! If you don't have " -"a DECT phone, just enter \"-\"." -msgstr "" -"VocΓͺ precisa especificar um nΓΊmero DECT de telefone nas suas configuracΓ΅es!\n" -"Se vocΓͺ nΓ£o tem um telefone DECT, apenas digite \\-\\." - -#: public/index.php:155 -msgid "No Access" -msgstr "Sem acesso" - -#: public/index.php:156 -msgid "" -"You don't have permission to view this page. You probably have to sign in or " -"register in order to gain access!" -msgstr "" -"VocΓͺ nΓ£o tem permissΓ£o para ver essa pΓ‘gina. VocΓͺ provavelmente terΓ‘ que " -"fazer login ou se registrar para ganhar acesso!" - -#~ msgid "Registration is disabled." -#~ msgstr "Registros estΓ£o desabilitados." - -#~ msgid "Please enter your planned date of arrival." -#~ msgstr "Por favor digite seu horario planificado de chagada " - -#~ msgid "Password could not be updated." -#~ msgstr "Nao foi possivel atualizar a senha" - -#~ msgid "We got no information about the event right now." -#~ msgstr "Nao tem info sobre o evento agora" - -#~ msgid "from %s to %s" -#~ msgstr "desde %s ate %s" - -#~ msgid "Please enter your planned date of departure." -#~ msgstr "Por favor pone seu horario planificado de ida" - -#~ msgid "Please select a user." -#~ msgstr "Seleciona um usuario" - -#~ msgid "Unable to load user." -#~ msgstr "Nao foi possivel carregar o usuario" - -#~ msgid "Entries" -#~ msgstr "Entradas" - -#~ msgid "Use new style if possible" -#~ msgstr "Usa umo estilo novo se possivel" - -#~ msgid "Coordinator" -#~ msgstr "Coordinador" - -#~ msgid "Coordinators" -#~ msgstr "Coordinadores" - -#~ msgid " vouchers." -#~ msgstr " vouchers." - -#~ msgid "" -#~ "This is a automatically calculated MINIMUM value, you can of course give " -#~ "out more if appropriate!" -#~ msgstr "" -#~ "Esso e' calucula automaticamente o valor MINIMO, voce pode claramente " -#~ "mudar isso com umo mais appropriado! " - -#~ msgid "You have been signed off from the shift." -#~ msgstr "Voce se desconnecto do seu turno" - -#~ msgid "planned departure" -#~ msgstr "saida planejada" - -#~ msgid "Tasks" -#~ msgstr "tarefas" - -#~ msgid "User didnt got vouchers." -#~ msgstr "O usuario nao tem vouchers." - -#~ msgid "Remove vouchers" -#~ msgstr "Apaga voucher" - -#~ msgid "" -#~ "This shift is running now or ended already. Please contact a dispatcher " -#~ "to join the shift." -#~ msgstr "" -#~ "Esse turno esta ativo agora o ja acabou. Por favor contata o coordinador " -#~ "para partecipar nesse turno" - -#~ msgid "" -#~ "You already subscribed to shift in the same timeslot. Please contact a " -#~ "dispatcher to join the shift." -#~ msgstr "" -#~ "Voce ja se registrou al turno no mesmo timeslot. Por favor contata o " -#~ "coordinador para partecipar a esse turno." - -#~ msgid "" -#~ "Hello %s, here you can change your personal settings i.e. password, color " -#~ "settings etc." -#~ msgstr "" -#~ "Oi %s, aqui pode mudar as tuas configuraΓ§eos pessoal (i.e. senha, " -#~ "cor, ...)" - -#~ msgid "Name/Description:" -#~ msgstr "Nome/DescriΓ§ao:" - -#~ msgid "Timeslot" -#~ msgstr "Timeslot" - -#~ msgid "The first wants to join %s." -#~ msgstr "O primeiro que quer partecipar %s" - -#~ msgid "Groups" -#~ msgstr "Grupos" - -#~ msgid "ICQ" -#~ msgstr "ICQ" - -#~ msgid "You are not confirmed for this angel type." -#~ msgstr "Voce nao ta confirmado por esse tipo de anjo." - -#~ msgid "Exports" -#~ msgstr "Esporta" - -#~ msgid "Add new angeltype" -#~ msgstr "Adicione um novo tipo de anjo" - -#~ msgid "Language" -#~ msgstr "Idioma" - -#~ msgid "You have %s new message." -#~ msgid_plural "You have %s new messages." -#~ msgstr[0] "Voce tem %s novo message" -#~ msgstr[1] "" - -#~ msgid "" -#~ "These are your shifts.
    Please try to appear 15 minutes before " -#~ "your shift begins!
    You can remove yourself from a shift up to %d " -#~ "hours before it starts." -#~ msgstr "" -#~ "Esses som teu turnos.
    Por favor tenta chegar 15 minudos antes " -#~ "que seu turno comença!
    Voce pode se tirar desse turno ate %d horas " -#~ "antes dele començar." - -#~ msgid "Page:" -#~ msgstr "Pagina" - -#~ msgid "Message:" -#~ msgstr "Messagem:" - -#~ msgid "Wakeup" -#~ msgstr "Wakeup" - -#~ msgid "Incomplete call, missing wake-up ID." -#~ msgstr "chamada incompleta, falta o wake-up ID" - -#~ msgid "Wake-up call deleted." -#~ msgstr "wake-up chamada apagada" - -#~ msgid "No wake-up found." -#~ msgstr "Nao encontrei nenhum wake-up" - -#~ msgid "" -#~ "Hello %s, here you can register for a wake-up call. Simply say when and " -#~ "where the angel should come to wake you." -#~ msgstr "" -#~ "Oi %s, aqui voce pode se registrar para a chamada wake-up. So tem que " -#~ "dizer quando e onde os anjos tem que chegar para te acordar (wake up)" - -#~ msgid "All ordered wake-up calls, next first." -#~ msgstr "Todos os ordem wake-up, o proximo primeiro" - -#~ msgid "Place" -#~ msgstr "Lugar" - -#~ msgid "Notes" -#~ msgstr "Notas" - -#~ msgid "Schedule a new wake-up here:" -#~ msgstr "Planifica um novo wake-up aqui:" - -#~ msgid "User %s confirmed as %s." -#~ msgstr "Usuario %s confirmado como %s" - -#~ msgid "" -#~ "Resistance is futile! Your biological and physical parameters will be " -#~ "added to our collectiv! Assimilating angel:" -#~ msgstr "" -#~ "Resistir e' inútil! Seus parÒmetros biológico e físico serão adicionados " -#~ "dentro do nosso coletivo! Anjo assimilado: " - -#~ msgid "Confirmed all." -#~ msgstr "Tudo confirmado." - -#~ msgid "asdf" -#~ msgstr "asdf" diff --git a/resources/views/admin/locations/edit.twig b/resources/views/admin/locations/edit.twig new file mode 100644 index 000000000..f30fb4377 --- /dev/null +++ b/resources/views/admin/locations/edit.twig @@ -0,0 +1,66 @@ +{% extends 'admin/locations/index.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ location ? __('location.edit.title') : __('location.create.title') }}{% endblock %} + +{% block row_content %} +
    + {{ csrf() }} + {{ f.hidden('id', location ? location.id : '') }} + +
    +
    + {{ f.input('name', __('general.name'), { + 'required': true, + 'required_icon': true, + 'value': f.formData('location', location ? location.name : ''), + }) }} + + {{ f.input('dect', __('general.dect'), { + 'value': f.formData('dect', location ? location.dect : ''), + }) }} + {{ f.input('map_url', __('location.map_url'), { + 'type': 'url', + 'value': f.formData('map_url', location ? location.map_url : ''), + 'info': __('location.map_url.info'), + }) }} + + {{ f.textarea('description', __('general.description'), { + 'value': f.formData('description', location ? location.description : ''), + 'rows': 5, + 'info': __('form.markdown') + }) }} +
    + +
    +

    {{ __('location.required_angels') }}

    + {% for types in angel_types.chunk(3) %} +
    + {% for angel_type in types %} + {% set needed = needed_angel_types ? needed_angel_types.where('angel_type_id', angel_type.id).first() : null %} + {% set name = 'angel_type_' ~ angel_type.id %} +
    + {{ f.number(name, angel_type.name, { + 'value': f.formData(name, needed ? needed.count : 0), + 'min': 0, + 'step': 1, + }) }} +
    + {% endfor %} +
    + {% endfor %} +
    +
    + +
    +
    + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} + {% if location %} + {{ f.delete(__('form.delete'), {'confirm_title': __('location.delete.title', [location.name|e])}) }} + {% endif %} +
    +
    + +
    +{% endblock %} diff --git a/resources/views/admin/locations/index.twig b/resources/views/admin/locations/index.twig new file mode 100644 index 000000000..b32dc4a30 --- /dev/null +++ b/resources/views/admin/locations/index.twig @@ -0,0 +1,81 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('location.locations') }}{% endblock %} + +{% block content %} +
    +

    + {% if not is_index|default(false) %} + {{ m.button(m.icon('chevron-left'), location + ? url('/locations', {'action': 'view', 'location_id': location.id}) + : url('/admin/locations'), 'secondary', 'sm', __('general.back')) }} + {% endif %} + + {{ block('title') }} + + {% if is_index|default(false) %} + {{ m.button(m.icon('plus-lg'), url('/admin/locations/edit'), 'secondary') }} + {% endif %} +

    + + {% include 'layouts/parts/messages.twig' %} + +
    + + {% block row_content %} +
    +
    + + + + + + + + + + + + {% for location in locations %} + + + + + + + + + + {% endfor %} + +
    {{ __('general.name') }}{{ __('general.dect') }}{{ __('location.map_url') }}
    + {{ m.icon('pin-map-fill') }} + + {{ location.name }} + + {{ m.iconBool(location.dect) }}{{ m.iconBool(location.map_url) }} +
    + + {{ m.button(m.icon('pencil'), url('/admin/locations/edit/' ~ location.id), null, 'sm', __('form.edit')) }} + +
    + {{ csrf() }} + {{ f.hidden('id', location.id) }} + {{ f.delete(null, { + 'size': 'sm', + 'confirm_title': __('location.delete.title', [location.name|e]), + 'confirm_button_text': __('form.delete'), + }) }} +
    + +
    +
    +
    +
    + {% endblock %} + +
    +
    +{% endblock %} diff --git a/resources/views/admin/log.twig b/resources/views/admin/log.twig index e7bc240c2..87ea55cbd 100644 --- a/resources/views/admin/log.twig +++ b/resources/views/admin/log.twig @@ -2,11 +2,11 @@ {% import 'macros/base.twig' as m %} {% import 'macros/form.twig' as f %} -{% set title %}{% block title %}{{ __('log.log') }}{% endblock %}{% endset %} +{% block title %}{{ __('log.log') }}{% endblock %} {% block content %}
    -

    {{ block('title') }}

    +

    {{ block('title') }} ({{ entries|length }})

    @@ -14,16 +14,39 @@
    {{ csrf() }} - {{ f.input('search', __('form.search'), 'text', {'value': search, 'hide_label': true}) }} +
    +
    + {{ f.input('search', __('form.search'), { + 'value': search, + 'hide_label': true, + }) }} +
    - {{ f.submit(__('form.search')) }} + {% if has_permission_to('logs.all') %} +
    + {{ f.select('search_user_id', __('general.user'), users, { + 'default_option': __('form.user_select'), + 'selected': search_user_id, + }) }} +
    + {% endif %} +
    + + {{ f.submit(__('form.search'), {'icon_left': 'search'}) }}
    + {% if not has_permission_to('logs.all') %} +
    + {{ m.alert(__('log.only_own')) }} +
    + {% endif %} + + {% for entry in entries %} @@ -44,10 +67,11 @@ {%- endif %} - - + + {% endfor %} diff --git a/resources/views/admin/rooms/edit.twig b/resources/views/admin/rooms/edit.twig deleted file mode 100644 index 43df6d23e..000000000 --- a/resources/views/admin/rooms/edit.twig +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'admin/rooms/index.twig' %} -{% import 'macros/base.twig' as m %} -{% import 'macros/form.twig' as f %} - -{% block title %}{{ room ? __('room.edit.title') : __('room.create.title') }}{% endblock %} - -{% block row_content %} - - {{ csrf() }} - {{ f.hidden('id', room ? room.id : '') }} - -
    -
    - {{ f.input( - 'name', - __('room.name'), - null, - {'required': true, 'entry_required_icon': true, 'value': f.formData('room', room ? room.name : '')} - ) }} - - {{ f.input('dect', __('room.dect'), null, {'value': f.formData('dect', room ? room.dect : '')}) }} - {{ f.input( - 'map_url', - __('room.map_url'), - 'url', - {'value': f.formData('map_url', room ? room.map_url : ''), 'info': __('room.map_url.info')} - ) }} - - {{ f.textarea( - 'description', - __('room.description'), - {'value': f.formData('description', room ? room.description : ''), 'rows': 5, 'info': __('form.markdown')} - ) }} -
    - -
    -

    {{ __('room.required_angels') }}

    - {% for types in angel_types.chunk(3) %} -
    - {% for angel_type in types %} - {% set needed = needed_angel_types ? needed_angel_types.where('angel_type_id', angel_type.id).first() : null %} - {% set name = 'angel_type_' ~ angel_type.id %} -
    - {{ f.number( - name, - angel_type.name, - {'value': f.formData(name, needed ? needed.count : 0), 'min': 0, 'step': 1} - ) }} -
    - {% endfor %} -
    - {% endfor %} -
    -
    - -
    -
    - {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} - {% if room %} - {{ f.submit(__('form.delete'), {'name': 'delete', 'btn_type': 'danger', 'icon_left': 'trash'}) }} - {% endif %} -
    -
    - - -{% endblock %} diff --git a/resources/views/admin/schedule/edit.twig b/resources/views/admin/schedule/edit.twig index 335b34e98..f30f9a4fe 100644 --- a/resources/views/admin/schedule/edit.twig +++ b/resources/views/admin/schedule/edit.twig @@ -4,26 +4,75 @@ {% block title %}{{ schedule ? __('schedule.edit.title') : __('schedule.import.title') }}{% endblock %} +{% block content_title %} + {{ m.button(m.icon('chevron-left'), url('/admin/schedule'), 'secondary', 'sm', __('general.back')) }} + {{ block('title') }} +{% endblock %} + {% block row_content %} {% if schedule and schedule.updated_at %}
    -

    {{ __('schedule.last_update', [schedule.updated_at.format(__('Y-m-d H:i'))]) }}

    +

    {{ __('schedule.last_update', [schedule.updated_at.format(__('general.datetime'))]) }}

    {% endif %} {{ csrf() }} -
    - {{ f.input('name', __('schedule.name'), null, {'required': true, 'value': schedule ? schedule.name : ''}) }} - {{ f.input('url', __('schedule.url'), 'url', {'required': true, 'value': schedule ? schedule.url : ''}) }} +
    +
    + {{ f.input('name', __('schedule.name'), { + 'required': true, + 'value': schedule ? schedule.name : '', + }) }} + {{ f.input('url', __('schedule.url'), { + 'type': 'url', + 'required': true, + 'value': schedule ? schedule.url : '' + }) }} + + {{ f.select('shift_type', __('schedule.shift-type'), shift_types|default([]), { + 'selected': schedule ? schedule.shift_type : '', + }) }} + + {{ f.checkbox('needed_from_shift_type', __('schedule.needed-from-shift-type'), { + 'checked': schedule ? schedule.needed_from_shift_type : '', + }) }} + + {{ f.input('minutes_before', __('schedule.minutes-before'), { + 'type': 'number', + 'required': true, + 'value': schedule ? schedule.minutes_before : 15 + }) }} + {{ f.input('minutes_after', __('schedule.minutes-after'), { + 'type': 'number', + 'required': true, + 'value': schedule ? schedule.minutes_after : 15 + }) }} - {{ f.select('shift_type', shift_types|default([]), __('schedule.shift-type'), schedule ? schedule.shift_type : '') }} + {{ f.save(__('form.save')) }} - {{ f.input('minutes_before', __('schedule.minutes-before'), 'number', {'required': true, 'value': schedule ? schedule.minutes_before : 15}) }} - {{ f.input('minutes_after', __('schedule.minutes-after'), 'number', {'required': true, 'value': schedule ? schedule.minutes_after : 15}) }} + {% if schedule %} + {{ f.delete(__('form.delete'), { + 'confirm_title': __('schedule.delete.title', [schedule.shifts|length]) + }) }} + {% endif %} +
    +
    +

    {{ __('schedule.for_locations') }}

    - {{ f.submit(__('form.save')) }} +
    + {% for id,name in locations %} +
    + {{ f.checkbox( + 'location_' ~ id, + name, + {'checked': schedule and id in schedule.activeLocations.pluck('id')} + ) }} +
    + {% endfor %} +
    +
    {% endblock %} diff --git a/resources/views/admin/schedule/index.twig b/resources/views/admin/schedule/index.twig index abb12e287..2e7352c7f 100644 --- a/resources/views/admin/schedule/index.twig +++ b/resources/views/admin/schedule/index.twig @@ -2,12 +2,12 @@ {% import 'macros/base.twig' as m %} {% import 'macros/form.twig' as f %} -{% set title %}{% block title %}{{ __('schedule.import.title') }}{% endblock %}{% endset %} +{% block title %}{{ __('schedule.import.title') }}{% endblock %} {% block content %}

    - {% block content_title %}{{ title }}{% endblock %} + {% block content_title %}{{ block('title') }}{% endblock %} {% if is_index|default(false) %} {{ m.button(m.icon('plus-lg'), url('/admin/schedule/edit'), 'secondary') }} @@ -37,18 +37,19 @@

    {% endfor %} diff --git a/resources/views/admin/schedule/load.twig b/resources/views/admin/schedule/load.twig index 4592af3c7..723f04e7a 100644 --- a/resources/views/admin/schedule/load.twig +++ b/resources/views/admin/schedule/load.twig @@ -1,8 +1,14 @@ {% extends 'admin/schedule/index.twig' %} +{% import 'macros/base.twig' as m %} {% import 'macros/form.twig' as f %} {% block title %}{{ __('schedule.import.load.title') }}{% endblock %} +{% block content_title %} + {{ m.button(m.icon('chevron-left'), url('/admin/schedule'), 'secondary', 'sm', __('general.back')) }} + {{ block('title') }} +{% endblock %} + {% block row_content %} {{ csrf() }} @@ -10,8 +16,8 @@

    {{ __('schedule.import.load.info', [schedule.conference.title, schedule.version]) }}

    -

    {{ __('schedule.import.rooms.add') }}

    - {{ _self.roomsTable(rooms.add) }} +

    {{ __('schedule.import.locations.add') }}

    + {{ _self.locationsTable(locations.add) }}

    {{ __('schedule.import.shifts.add') }}

    {{ _self.shiftsTable(shifts.add) }} @@ -22,25 +28,25 @@

    {{ __('schedule.import.shifts.delete') }}

    {{ _self.shiftsTable(shifts.delete) }} - {{ f.submit(__('form.import')) }} + {{ f.submit(__('form.import'), {'icon_left': 'box-arrow-in-down'}) }}
    {% endblock %} -{% macro roomsTable(rooms) %} +{% macro locationsTable(locations) %}
    {{ __('log.time') }} {{ __('log.level') }}{{ __('general.user') }} {{ __('log.message') }}
    {{ entry.created_at.format(__('Y-m-d H:i')) }} + {{ entry.created_at.format(__('general.datetime')) }} {{ entry.level|capitalize }} {% if entry.user %}{{ m.user(entry.user) }}{% endif %} {{ entry.message|nl2br }}
    {{ schedule.name }} {{ schedule.url }} - +
    + {{ csrf() }} +
    + {{ m.button(m.icon('box-arrow-in-down'), url('/admin/schedule/load/' ~ schedule.id), 'secondary', 'sm', __('form.import')) }} + {{ m.button(m.icon('pencil'), url('/admin/schedule/edit/' ~ schedule.id), 'secondary', 'sm', __('form.edit')) }} + {{ f.delete(null, { + 'title': __('form.delete'), + 'confirm_button_text': __('form.delete'), + 'confirm_title': __('schedule.delete.title', [schedule.shifts|length]), + 'size': 'sm' + }) }} +
    +
    - + - {% for room in rooms %} + {% for location in locations %} - + {% endfor %} @@ -55,15 +61,15 @@ - - + + {% for shift in shifts %} - + diff --git a/resources/views/admin/shifts/history.twig b/resources/views/admin/shifts/history.twig new file mode 100644 index 000000000..6077458ed --- /dev/null +++ b/resources/views/admin/shifts/history.twig @@ -0,0 +1,74 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('shifts.history') }}{% endblock %} + +{% block content %} +
    +

    + {{ m.button(m.icon('chevron-left'), url('/admin-shifts'), null, 'sm', __('general.back')) }} + {% block content_title %}{{ block('title') }}{% endblock %} +

    + + {% include 'layouts/parts/messages.twig' %} + +
    +{% block row_content %} +
    +
    +
    {{ __('schedule.import.rooms.name') }}{{ __('general.name') }}
    {{ room.name }}{{ location.name }}
    {{ __('schedule.import.shift.dates') }} {{ __('schedule.import.shift.type') }}{{ __('schedule.import.shift.title') }}{{ __('schedule.import.shift.room') }}{{ __('title.title') }}{{ __('schedule.import.shift.location') }}
    {{ shift.date.format(__('Y-m-d H:i')) }} - {{ shift.endDate.format(__('H:i')) }}{{ shift.date.format(__('general.datetime')) }} - {{ shift.endDate.format(__('H:i')) }} {{ shift.type }} {{ shift.title }}{% if shift.subtitle %}
    {{ shift.subtitle }}{% endif %}
    {{ shift.room.name }}
    + + + + + + + + + + + + + + + + {% for shift in shifts %} + + + + + + + + + + + + {% endfor %} + +
    {{ __('general.id') }}{{ __('title.title') }}{{ __('location.location') }}{{ __('general.count') }}{{ __('shifts.start') }}{{ __('shifts.end') }}{{ __('general.user') }}{{ __('general.created_at') }}
    {{ shift.transaction_id }} + {% if shift.schedule %} + {{ __('shifts.history.schedule', [shift.schedule.name]) }} + {% else %} + {{ shift.title }} + {% endif %}
    + {{ shift.shiftType.name }} +
    {{ shift.location.name }}{{ shift.count }}{{ shift.start.format(__('general.datetime')) }}{{ shift.end.format(__('general.datetime')) }}{{ m.user(shift.createdBy) }}{{ shift.created_at.format(__('general.datetime')) }} +
    + {{ csrf() }} + {{ f.hidden('transaction_id', shift.transaction_id) }} + {{ f.delete(null, { + 'size': 'sm', + 'title': __('form.delete_all'), + 'confirm_title': __('shifts.history.delete_all.title', [shift.count]), + 'confirm_button_text': __('form.delete_all'), + }) }} +
    +
    +
    +
    +{% endblock %} + + +{% endblock %} diff --git a/resources/views/admin/shifttypes/edit.twig b/resources/views/admin/shifttypes/edit.twig new file mode 100644 index 000000000..e29935470 --- /dev/null +++ b/resources/views/admin/shifttypes/edit.twig @@ -0,0 +1,60 @@ +{% extends 'admin/shifttypes/index.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ shifttype ? __('shifttype.edit.title') : __('shifttype.create.title') }}{% endblock %} + +{% block row_content %} +
    + {{ csrf() }} + {{ f.hidden('id', shifttype ? shifttype.id : '') }} + +
    +
    + {{ f.input('name', __('general.name'), { + 'required': true, + 'required_icon': true, + 'value': f.formData('shifttype', shifttype ? shifttype.name : ''), + }) }} + + {{ f.textarea('description', __('general.description'), { + 'value': f.formData('description', shifttype ? shifttype.description : ''), + 'rows': 5, + 'info': __('form.markdown') + }) }} +
    + +
    +

    {{ __('shifttype.required_angels') }}

    + {% for types in angel_types.chunk(3) %} +
    + {% for angel_type in types %} + {% set needed = shifttype + ? shifttype.neededAngelTypes.where('angel_type_id', angel_type.id).first() + : null %} + {% set name = 'angel_type_' ~ angel_type.id %} +
    + {{ f.number(name, angel_type.name, { + 'value': f.formData(name, needed ? needed.count : 0), + 'min': 0, + 'step': 1, + }) }} +
    + {% endfor %} +
    + {% endfor %} +
    +
    + +
    +
    + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} + {% if shifttype %} + {{ f.delete(__('form.delete'), + {'title' : '', 'confirm_title': __('shifttype.delete.title', [shifttype.name|e])}) }} + {% endif %} +
    +
    + +
    +{% endblock %} diff --git a/resources/views/admin/rooms/index.twig b/resources/views/admin/shifttypes/index.twig similarity index 55% rename from resources/views/admin/rooms/index.twig rename to resources/views/admin/shifttypes/index.twig index ac439c085..42ae512ab 100644 --- a/resources/views/admin/rooms/index.twig +++ b/resources/views/admin/shifttypes/index.twig @@ -2,15 +2,21 @@ {% import 'macros/base.twig' as m %} {% import 'macros/form.twig' as f %} -{% block title %}{{ __('room.rooms') }}{% endblock %} +{% block title %}{{ __('shifttype.shifttypes') }}{% endblock %} {% block content %}

    + {% if not is_index|default(false) %} + {{ m.button(m.icon('chevron-left'), url('/admin/shifttypes'), 'secondary', 'sm', __('general.back')) }} + {% endif %} + {{ block('title') }} - {% if is_index|default(false) %} - {{ m.button(m.icon('plus-lg'), url('/admin/rooms/edit'), 'secondary') }} + {% if is_index|default(false) and has_permission_to('shifttypes.edit') %} + {{ m.button(m.icon('plus-lg'), url('/admin/shifttypes/edit'), 'secondary') }} + {% elseif is_view|default(false) and has_permission_to('shifttypes.edit') %} + {{ m.button(m.icon('pencil'), url('admin/shifttypes/edit/' ~ shifttype.id), null, 'sm', __('form.edit')) }} {% endif %}

    @@ -24,39 +30,31 @@ - - - + - {% for room in rooms %} + {% for shifttype in shifttypes %} - - - - {% endfor %} diff --git a/resources/views/admin/shifttypes/view.twig b/resources/views/admin/shifttypes/view.twig new file mode 100644 index 000000000..d27781516 --- /dev/null +++ b/resources/views/admin/shifttypes/view.twig @@ -0,0 +1,28 @@ +{% extends 'admin/shifttypes/index.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ shifttype.name }}{% endblock %} + +{% block row_content %} +
    +

    {{ __('general.description') }}

    + {{ shifttype.description|md }} +
    + {% if shifttype.neededAngelTypes.isNotEmpty() %} +
    +

    {{ __('location.required_angels') }}

    + +
    + {% endif %} +{% endblock %} diff --git a/resources/views/admin/user/delete-worklog.twig b/resources/views/admin/user/delete-worklog.twig index 97df9e1e9..fc5813b1e 100644 --- a/resources/views/admin/user/delete-worklog.twig +++ b/resources/views/admin/user/delete-worklog.twig @@ -12,8 +12,13 @@
    {{ m.alert(__('worklog.delete.info', [m.user(user)]), 'danger', true) }} - {{ m.button(__('form.cancel'), url('/users?action=view&user_id=' ~ user.id)) }} - {{ f.submit(__('form.delete'), {'btn_type': 'danger'}) }} + {{ m.button(__('form.cancel'), + url('/users?action=view&user_id=' ~ user.id), + null, + null, + null, + 'x-lg') }} + {{ f.delete(__('form.delete')) }}
    diff --git a/resources/views/admin/user/edit-shirt.twig b/resources/views/admin/user/edit-shirt.twig index 85ced8d36..f1640bb4b 100644 --- a/resources/views/admin/user/edit-shirt.twig +++ b/resources/views/admin/user/edit-shirt.twig @@ -4,11 +4,14 @@ {% block title %} {{ is_tshirt ? __('user.edit.shirt') : __('user.edit.goodie') }} -{% endblock %} +{%- endblock %} {% block content %}
    -

    {{ block('title') }}

    +

    + {{ m.button(m.icon('chevron-left'), url('/admin-active'), null, 'sm', __('general.back')) }} + {{ block('title') }}: {{ m.user(userdata) }} +

    {% include 'layouts/parts/messages.twig' %} @@ -18,22 +21,37 @@
    {% if is_tshirt %}
    - {{ f.select('shirt_size', config('tshirt_sizes'), __('user.shirt_size'), userdata.personalData.shirt_size) }} + {{ f.select('shirt_size', __('user.shirt_size'), config('tshirt_sizes'), { + 'selected': userdata.personalData.shirt_size, + 'required': true, + 'required_icon': true, + 'default_option': __('form.select_placeholder'), + }) }}
    {% endif %}
    - {{ f.switch('arrived', __('user.arrived'), userdata.state.arrived, {'disabled': not has_permission_to('admin_arrive')}) }} + {{ f.switch('arrived', __('user.arrived'), { + 'checked': userdata.state.arrived, + 'disabled': not has_permission_to('admin_arrive'), + }) }} {% if userdata.state.force_active %} - {{ f.switch('force_active', __('user.force_active'), true, {'disabled': true}) }} + {{ f.switch('force_active', __('user.force_active'), { + 'checked': true, + 'disabled': true, + }) }} {% endif %} - {{ f.switch('active', __('user.active'), userdata.state.active) }} + {{ f.switch('active', __('user.active'), { + 'checked': userdata.state.active, + }) }} - {{ f.switch('got_shirt', is_tshirt ? __('user.got_shirt') : __('user.got_goodie'), userdata.state.got_shirt) }} + {{ f.switch('got_shirt', is_tshirt ? __('user.got_shirt') : __('user.got_goodie'), { + 'checked': userdata.state.got_shirt, + }) }}
    - {{ f.submit(__('form.save')) }} + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }}
    diff --git a/resources/views/admin/user/edit-worklog.twig b/resources/views/admin/user/edit-worklog.twig index 65239924a..e7bad20a8 100644 --- a/resources/views/admin/user/edit-worklog.twig +++ b/resources/views/admin/user/edit-worklog.twig @@ -2,46 +2,46 @@ {% import 'macros/base.twig' as m %} {% import 'macros/form.twig' as f %} -{% block title %}{{ __(is_edit ? 'worklog.edit' : 'worklog.add') }}{% endblock %} +{% block title %}{{ is_edit ? __('worklog.edit') : __('worklog.add') }}{% endblock %} {% block content %}
    -

    {{ block('title') }}

    +

    + {{ m.button(m.icon('chevron-left'), url('/users', {action: 'view', user_id: user.id}), + null, 'sm', __('general.back')) }} + {{ block('title') }} +

    {% include 'layouts/parts/messages.twig' %}
    {{ csrf() }}
    -
    - {{ m.button(__('back'), url('/users?action=view&user_id=' ~ user.id)) }} -
    - +
    {{ m.user(user, {'pronoun': true}) }}
    - {{ f.input( - 'work_date', - __('worklog.date'), - 'date', - {'value': work_date.format('Y-m-d'), 'required': true} - ) }} - {{ f.input( - 'work_hours', - __('worklog.hours'), - 'number', - {'value': work_hours, 'required': true, 'step': '0.01', 'min': 0} - ) }} - {{ f.input( - 'comment', - __('worklog.comment'), - 'text', - {'value': comment, 'required': true, 'max_length': 200} - ) }} - {{ f.submit(__('form.save')) }} + {{ f.input('work_date', __('worklog.date'), { + 'type': 'date', + 'value': work_date.format('Y-m-d'), + 'required': true, + }) }} + {{ f.input('work_hours', __('worklog.hours'), { + 'type': 'number', + 'value': work_hours, + 'required': true, + 'step': '0.01', + 'min': 0, + }) }} + {{ f.input('comment', __('worklog.comment'), { + 'value': comment, + 'required': true, + 'max_length': 200, + }) }} + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }}
    diff --git a/resources/views/api/ical.twig b/resources/views/api/ical.twig index 3b0d20a17..745d484f3 100644 --- a/resources/views/api/ical.twig +++ b/resources/views/api/ical.twig @@ -1,8 +1,8 @@ {% set dateFormat = 'Ymd\\THis\\Z' %} -{% set replacement = {'\n': '\\n'} %} +{% set replacement = {'\\': '\\\\', ';': '\\;', ',': '\\,', '\n': '\\n'} %} BEGIN:VCALENDAR VERSION:2.0 -PRODID:-//-/{{ config('app_name') }}//DE +PRODID:-//-/{{ config('app_name') | replace(replacement) | raw }}//DE CALSCALE:GREGORIAN {% for entry in shiftEntries %} BEGIN:VEVENT @@ -12,12 +12,14 @@ DTSTART:{{ entry.shift.start.utc().format(dateFormat) }} DTEND:{{ entry.shift.end.utc().format(dateFormat) }} STATUS:CONFIRMED TRANSP:OPAQUE -SUMMARY:{{ entry.shift.shiftType.name ~ ' (' ~ entry.shift.title ~ ')' | replace(replacement) | raw }} -LOCATION:{{ entry.shift.room.name | replace(replacement) | raw }} +SUMMARY:{{ (entry.shift.shiftType.name ~ ' (' ~ entry.shift.title ~ ')') | replace(replacement) | raw }} +LOCATION:{{ entry.shift.location.name | replace(replacement) | raw }} DESCRIPTION:{{ - entry.shift.shiftType.description - ~ '\\n' ~ entry.shift.description - ~ '\\n' ~ entry.user_comment + ( + entry.shift.shiftType.description + ~ '\n' ~ entry.shift.description + ~ '\n' ~ entry.user_comment + ) | replace(replacement) | raw }} URL:{{ url('/shifts', {'action': 'view', 'shift_id': entry.shift.id}) | raw }} diff --git a/resources/views/emails/angeltype-added.twig b/resources/views/emails/angeltype-added.twig index 5ce3fc831..b1c061575 100644 --- a/resources/views/emails/angeltype-added.twig +++ b/resources/views/emails/angeltype-added.twig @@ -3,9 +3,10 @@ {% set url=url('/angeltypes', {'action': 'view', 'angeltype_id': angeltype.id}) %} {% block introduction %} -{{ __('notification.angeltype.added.introduction', [angeltype.name, url])|raw }} + +{{ __('notification.angeltype.added.introduction', [angeltype.name|e, url])|raw }} {% endblock %} {% block message %} -{{ __('notification.angeltype.added.text', [angeltype.name, url])|raw }} +{{ __('notification.angeltype.added.text', [angeltype.name|e, url])|raw }} {% endblock %} diff --git a/resources/views/emails/angeltype-confirmed.twig b/resources/views/emails/angeltype-confirmed.twig index 1fd2d0b19..8f2c3e179 100644 --- a/resources/views/emails/angeltype-confirmed.twig +++ b/resources/views/emails/angeltype-confirmed.twig @@ -3,9 +3,9 @@ {% set url=url('/angeltypes', {'action': 'view', 'angeltype_id': angeltype.id}) %} {% block introduction %} -{{ __('notification.angeltype.confirmed.introduction', [angeltype.name, url])|raw }} +{{ __('notification.angeltype.confirmed.introduction', [angeltype.name|e, url])|raw }} {% endblock %} {% block message %} -{{ __('notification.angeltype.confirmed.text', [angeltype.name, url])|raw }} +{{ __('notification.angeltype.confirmed.text', [angeltype.name|e, url])|raw }} {% endblock %} diff --git a/resources/views/emails/mail.twig b/resources/views/emails/mail.twig index 3c9f75377..f84d9f969 100644 --- a/resources/views/emails/mail.twig +++ b/resources/views/emails/mail.twig @@ -1,8 +1,15 @@ -{% block title %}{{ __('Hi %s,', [username]) }}{% endblock %} +{% block title %}{{ __('email.greeting', [username]) }}{% endblock %} -{% block introduction %}{{ __('here is a message for you from the %s:', [config('app_name')]) }}{% endblock %} +{% block introduction %}{{ __('email.introduction', [config('app_name')]) }}{% endblock %} {% block message %}{{ message|raw }}{% endblock %} {{ '-- ' }} -{% block footer %}{{ __('This email is autogenerated and has not been signed. You got this email because you are registered in the %s.', [config('app_name')]) }}{% endblock %} +{% block footer %} +{{ config('name') }} +{%- if config('enable_show_day_of_event') and day_of_event is defined -%} +{% if config('name') %}, {% endif %}{{ __('event.day', [day_of_event]) }} +{%- endif %} + +{{ __('email.footer', [config('app_name')]) }} +{% endblock %} diff --git a/resources/views/emails/news-updated.twig b/resources/views/emails/news-updated.twig new file mode 100644 index 000000000..dd51f5f66 --- /dev/null +++ b/resources/views/emails/news-updated.twig @@ -0,0 +1,9 @@ +{% extends "emails/mail.twig" %} + +{% block introduction %} +{{ __('notification.news.updated.introduction', [news.title, news.text, url('/news/' ~ news.id)]) }} +{% endblock %} + +{% block message %} +{{ __('notification.news.updated.text', [news.title, news.text, url('/news/' ~ news.id)]) }} +{% endblock %} diff --git a/resources/views/emails/password-reset.twig b/resources/views/emails/password-reset.twig index 30b613b4d..f97299342 100644 --- a/resources/views/emails/password-reset.twig +++ b/resources/views/emails/password-reset.twig @@ -1,3 +1,3 @@ {% extends "emails/mail.twig" %} -{% block message %}{{ __('Please visit %s to recover your password.', [url('/password/reset/') ~ reset.token]) }}{% endblock %} +{% block message %}{{ __('password.email.message', [url('/password/reset/') ~ reset.token]) }}{% endblock %} diff --git a/resources/views/emails/updated-shift.twig b/resources/views/emails/updated-shift.twig new file mode 100644 index 000000000..4528273e5 --- /dev/null +++ b/resources/views/emails/updated-shift.twig @@ -0,0 +1,42 @@ +{% extends "emails/mail.twig" %} + +{% block introduction %} +{{ __('notification.shift.updated.introduction') }} +{% endblock %} + +{% block message %} +{%- if oldShift.shift_type_id != shift.shift_type_id -%} +* {{ __('notification.shift.updated.type', [oldShift.shiftType.name, shift.shiftType.name]) }} +{% endif %} +{%- if oldShift.title != shift.title -%} +* {{ __('notification.shift.updated.title', [oldShift.title, shift.title]) }} +{% endif %} +{%- if oldShift.description != shift.description -%} +* {{ __('notification.shift.updated.description', [oldShift.description, shift.description]) }} +{% endif %} +{%- if oldShift.start != shift.start -%} +* {{ __( + 'notification.shift.updated.start', + [oldShift.start.format(__('general.datetime')), shift.start.format(__('general.datetime'))] +) }} +{% endif %} +{%- if oldShift.end != shift.end -%} +* {{ __( + 'notification.shift.updated.end', + [oldShift.end.format(__('general.datetime')), shift.end.format(__('general.datetime'))] +) }} +{% endif %} +{%- if oldShift.location_id != shift.location_id -%} +* {{ __('notification.shift.updated.location', [oldShift.location.name, shift.location.name]) }} +{% endif %} + +{{ __('notification.shift.updated.shift') }} + +{{ shift.shiftType.name }} +{{ shift.title }} +{{ shift.description }} +{{ shift.start.format(__('general.datetime')) }} - {{ shift.end.format(__('H:i')) }} +{{ shift.location.name }} + +{{ url('/shifts', {'action': 'view', 'shift_id': shift.id})|raw }} +{% endblock %} diff --git a/resources/views/emails/worklog-from-shift.twig b/resources/views/emails/worklog-from-shift.twig index b85239a6a..d19dab45a 100644 --- a/resources/views/emails/worklog-from-shift.twig +++ b/resources/views/emails/worklog-from-shift.twig @@ -7,8 +7,8 @@ {% block message %} {{ name }} {{ title }} -{{ start.format(__('Y-m-d H:i')) }} - {{ end.format(__('Y-m-d H:i')) }} -{{ room.name }} +{{ start.format(__('general.datetime')) }} - {{ end.format(__('general.datetime')) }} +{{ location.name }} {% if start <= date() and not freeloaded %} {{ __('notification.shift.deleted.worklog') }} diff --git a/resources/views/errors/403.twig b/resources/views/errors/403.twig index 03a96ed71..f2782b950 100644 --- a/resources/views/errors/403.twig +++ b/resources/views/errors/403.twig @@ -1,15 +1,15 @@ {% extends "errors/default.twig" %} {% import 'macros/base.twig' as m %} -{% block title %}{{ __("Forbidden") }}{% endblock %} +{% block title %}{{ __('page.403.title') }}{% endblock %} -{% block content_headline_text %}{{ __("You are not allowed to access this page") }}{% endblock %} +{% block content_headline_text %}{{ __('page.403.headline') }}{% endblock %} {% block content_text %} {% if is_guest() %} {% do session_set('previous_page', request.url) %}

    {{ __('page.403.login') }}

    -

    {{ m.button(__('login.login'), url('login')) }}

    +

    {{ m.button(__('general.login'), url('/login')) }}

    {% endif %} {% endblock %} diff --git a/resources/views/errors/404.twig b/resources/views/errors/404.twig index 620aaa2d4..2f44226bc 100644 --- a/resources/views/errors/404.twig +++ b/resources/views/errors/404.twig @@ -1,13 +1,13 @@ {% extends "errors/default.twig" %} -{% block title %}{{ __("Page not found") }}{% endblock %} +{% block title %}{{ __('page.404.title') }}{% endblock %} {% block content_container %}

    4:{{ status|slice(1, 2) }} - {{ __("No sleep found") }} + {{ __('page.404.not_found') }}

    {% block content_text %} diff --git a/resources/views/errors/405.twig b/resources/views/errors/405.twig index cbbb94eaa..84a12b167 100644 --- a/resources/views/errors/405.twig +++ b/resources/views/errors/405.twig @@ -1,5 +1,5 @@ {% extends "errors/default.twig" %} -{% block title %}{{ __("405: Method not allowed") }}{% endblock %} +{% block title %}{{ __("page.405.title") }}{% endblock %} -{% block content_headline_text %}{{ __("405: Method not allowed") }}{% endblock %} +{% block content_headline_text %}{{ __("page.405.title") }}{% endblock %} diff --git a/resources/views/errors/419.twig b/resources/views/errors/419.twig index a9545af2b..47cf12e18 100644 --- a/resources/views/errors/419.twig +++ b/resources/views/errors/419.twig @@ -1,7 +1,7 @@ {% extends "errors/default.twig" %} -{% block title %}{{ __("Authentication expired") }}{% endblock %} +{% block title %}{{ __('page.419.title') }}{% endblock %} {% block content %} - + {% endblock %} diff --git a/resources/views/errors/default.twig b/resources/views/errors/default.twig index e3c76d156..60770611e 100644 --- a/resources/views/errors/default.twig +++ b/resources/views/errors/default.twig @@ -1,6 +1,6 @@ {% extends "layouts/app.twig" %} -{% block title %}Error {{ status }}{% endblock %} +{% block title %}{{ __('page.error.title', [status]) }}{% endblock %} {% block content %}
    @@ -8,7 +8,7 @@
    {{ __('room.name') }}{{ __('room.dect') }}{{ __('room.map_url') }}{{ __('general.name') }}
    - {{ m.icon('pin-map-fill') }} - - {{ room.name }} - + {{ shifttype.name }} {{ m.iconBool(room.dect) }}{{ m.iconBool(room.map_url) }} + {% if has_permission_to('shifttypes.edit') %}
    - - {{ m.button(m.icon('pencil'), url('admin/rooms/edit/' ~ room.id), null, 'sm', __('form.edit')) }} + {{ m.button(m.icon('pencil'), url('admin/shifttypes/edit/' ~ shifttype.id), null, 'sm', __('form.edit')) }}
    {{ csrf() }} - {{ f.hidden('id', room.id) }} - {{ f.button(m.icon('trash'), {'title': __('form.delete'), 'name': 'delete', 'type': 'submit', 'btn_type': 'danger', 'size': 'sm'}) }} + {{ f.hidden('id', shifttype.id) }} + {{ f.delete(null, {'size': 'sm', 'confirm_title': __('shifttype.delete.title', [shifttype.name|e])}) }}
    + {% endif %}
    - + - + {% for c in conversations %} {% endfor %} diff --git a/resources/views/pages/news/edit.twig b/resources/views/pages/news/edit.twig index 3ca73e7a4..639dd9d7a 100644 --- a/resources/views/pages/news/edit.twig +++ b/resources/views/pages/news/edit.twig @@ -6,7 +6,10 @@ {% block content %}
    -

    {{ block('title') }}

    +

    + {{ m.button(m.icon('chevron-left'), url('/news'), null, 'sm', __('general.back')) }} + {{ block('title') }} +

    {% include 'layouts/parts/messages.twig' %} @@ -14,12 +17,12 @@

    - {{ m.icon('clock') }} {{ news.updated_at.format(__('Y-m-d H:i')) }} + {{ m.icon('clock') }} {{ news.updated_at.format(__('general.datetime')) }} {% if news.updated_at != news.created_at %}  {{ __('news.updated') }}
    - {{ m.icon('clock') }} {{ news.created_at.format(__('Y-m-d H:i')) }} + {{ m.icon('clock') }} {{ news.created_at.format(__('general.datetime')) }} {% endif %}  {{ m.user(news.user) }} @@ -36,30 +39,46 @@ {{ f.input( 'title', __('news.edit.subject'), - null, - {'required': true, 'value': news ? news.title : ''} + { + 'required': true, + 'value': news ? news.title : '', + } ) }}

    - {{ f.checkbox('is_meeting', __('news.edit.is_meeting'), is_meeting) }} - {{ f.checkbox('is_pinned', __('news.edit.is_pinned'), is_pinned) }} - {% if has_permission_to('news.important') %} {{ f.checkbox('is_important', __('news.edit.is_important'), is_important) }}{% endif %} + {{ f.checkbox('is_meeting', __('news.edit.is_meeting'), { + 'checked': is_meeting, + }) }} + {{ f.checkbox('is_pinned', __('news.edit.is_pinned'), { + 'checked': is_pinned, + }) }} + {% if has_permission_to('news.highlight') %} + {{ f.checkbox('is_highlighted', __('news.edit.is_highlighted'), { + 'checked': is_highlighted, + }) }} + {% endif %}
    - {{ f.textarea('text', __('news.edit.message'), {'required': true, 'rows': 10, 'value': news ? news.text : ''}) }} + {{ f.textarea('text', __('news.edit.message'), { + 'required': true, + 'rows': 10, + 'value': news ? news.text : '', + }) }}

    {{ m.info(__('news.edit.hint')) }}

    - {{ f.submit() }} + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} {{ f.submit(m.icon('eye'), {'name': 'preview', 'btn_type': 'info', 'title': __('form.preview')}) }} {% if news and news.id %} - {{ f.submit(m.icon('trash'), {'name': 'delete', 'btn_type': 'danger', 'title': __('form.delete')}) }} + {{ f.delete(__('form.delete'), {'confirm_title': __('news.delete.title', [news.title[:40]|e])}) }} {% endif %} + + {{ f.checkbox('send_notification', __('form.send_notification'), {'checked': send_notification, 'class': 'ms-2 form-check-inline'}) }}
    @@ -68,7 +87,7 @@

    {{ __('form.preview') }}

    -
    +
    {% if news.is_meeting %}{{ __('news.is_meeting') }}{% endif %} {{ news.title }} diff --git a/resources/views/pages/news/news.twig b/resources/views/pages/news/news.twig index f604deaa0..f8cef9ae9 100644 --- a/resources/views/pages/news/news.twig +++ b/resources/views/pages/news/news.twig @@ -17,10 +17,10 @@
    {{ comment.text|nl2br }}
    - diff --git a/resources/views/pages/news/overview.twig b/resources/views/pages/news/overview.twig index be8961e30..e22305cc7 100644 --- a/resources/views/pages/news/overview.twig +++ b/resources/views/pages/news/overview.twig @@ -1,5 +1,6 @@ {% extends 'layouts/app.twig' %} {% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} {% set only_meetings = only_meetings|default(false) %} {% block title %}{{ not only_meetings ? __('news.title') : __('news.title.meetings') }}{% endblock %} @@ -7,9 +8,12 @@ {% block content %}

    + {% if not is_overview|default(false) %} + {{ m.button(m.icon('chevron-left'), url('/news'), null, 'sm', __('general.back')) }} + {% endif %} {{ block('title') }} {%- if has_permission_to('admin_news') and is_overview|default(false) -%} - {{ m.button(m.icon('plus-lg'), url('admin/news', only_meetings ? {'meeting': 1} : {}), 'secondary') }} + {{ m.button(m.icon('plus-lg'), url('/admin/news', only_meetings ? {'meeting': 1} : {}), 'secondary') }} {%- endif %}

    @@ -33,7 +37,7 @@ @@ -48,10 +52,10 @@ {% endblock %} {% macro news(news, show_comments_link, is_overview) %} -
    +
    {% if is_overview|default(false) %}
    - + {% if news.is_pinned %}{{ m.icon('pin-angle') }}{% endif %} {% if news.is_meeting %}{{ __('news.is_meeting') }}{% endif %} {{ news.title }} @@ -62,39 +66,51 @@
    {{ news.text(not is_overview)|markdown }} {% if is_overview and news.text != news.text(false) %} - {{ m.button(__('news.read_more'), url('news/' ~ news.id), null, 'sm', null, null, 'chevron-double-right') }} + {{ m.button(__('news.read_more'), url('/news/' ~ news.id), null, 'sm', null, null, 'chevron-double-right') }} {% endif %}
    -
    {{ __('angel') }}{{ __('general.angel') }} {{ __('message.message') }}{{ __('date') }}{{ __('title.date') }}
    - {{ m.user(c.other_user, {'pronoun': true, 'url': url('messages/' ~ c.other_user.id ~ '#newest')}) }} + {{ m.user(c.other_user, {'pronoun': true, 'url': url('/messages/' ~ c.other_user.id ~ '#newest')}) }} {% if c.unread_messages > 0 %} {{ c.unread_messages }} {% endif %} - + {{ c.latest_message.text|length > 100 ? c.latest_message.text|slice(0, 100) ~ '…' : c.latest_message.text }} - {{ c.latest_message.created_at.format(__('Y-m-d H:i')) }} + {{ c.latest_message.created_at.format(__('general.datetime')) }}
    + + + + + + + + + + {% for session in sessions %} + + + + + + {% endfor %} + +
    {{ __('settings.sessions.id') }}{{ __('settings.sessions.last_activity') }} + {% if sessions|length > 1 %} +
    + {{ csrf() }} + {{ f.hidden('id', 'all') }} + {{ f.delete(__('form.delete_all'), {'size': 'sm', 'confirm_title': __('form.delete_all'), 'title': ''}) }} +
    + {% endif %} +
    +
    {{ session.id[:15] }}…
    +
    {{ session.last_activity.format(__('general.datetime')) }} + {% if session.id != current_session %} +
    + {{ csrf() }} + {{ f.hidden('id', session.id[:15]) }} + {{ f.submit( + ' ', + {'name': 'delete', 'btn_type': 'danger', 'size': 'sm', 'icon_left': 'trash', 'title': __('form.delete'),} + ) }} +
    + {% else %} + {{ __('settings.sessions.current') }} + {% endif %} +
    +
    + + +{% endblock %} diff --git a/resources/views/pages/settings/settings.twig b/resources/views/pages/settings/settings.twig index 656022f43..35034ee32 100644 --- a/resources/views/pages/settings/settings.twig +++ b/resources/views/pages/settings/settings.twig @@ -18,6 +18,7 @@ {% for url,title in settings_menu %} diff --git a/resources/views/pages/settings/theme.twig b/resources/views/pages/settings/theme.twig index 37e0bce91..4faf8aec5 100644 --- a/resources/views/pages/settings/theme.twig +++ b/resources/views/pages/settings/theme.twig @@ -11,8 +11,10 @@
    {{ m.info(__('settings.theme.info')) }} - {{ f.select('select_theme', themes, __('settings.theme'), current_theme) }} - {{ f.submit(__('form.save')) }} + {{ f.select('select_theme', __('settings.theme'), themes, { + 'selected': current_theme, + }) }} + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }}
    diff --git a/resources/views/pages/user-shifts.html b/resources/views/pages/user-shifts.html index 75cf148a6..39f31ea4e 100644 --- a/resources/views/pages/user-shifts.html +++ b/resources/views/pages/user-shifts.html @@ -2,7 +2,7 @@
    -

    %title%

    +

    %title% %add_link%

    %start_select%
    @@ -34,7 +34,7 @@

    %title%

    -
    +
    @@ -47,12 +47,15 @@

    %title%

    - %buttons% + %random% +
    +
    + %dashboard%
    - +
    @@ -66,13 +69,13 @@

    %title%

    -
    %room_select%
    +
    %location_select%
    %type_select%
    %filled_select%
    -
    -

    %task_notice%

    +
    +
    diff --git a/src/Application.php b/src/Application.php index cbc014e28..87cfacfb2 100644 --- a/src/Application.php +++ b/src/Application.php @@ -101,6 +101,7 @@ protected function registerPaths(): void $this->instance('path', $appPath); $this->instance('path.config', $appPath . DIRECTORY_SEPARATOR . 'config'); $this->instance('path.resources', $appPath . DIRECTORY_SEPARATOR . 'resources'); + $this->instance('path.resources.api', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'api'); $this->instance('path.public', $appPath . DIRECTORY_SEPARATOR . 'public'); $this->instance('path.assets', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'assets'); $this->instance('path.assets.public', $this->get('path.public') . DIRECTORY_SEPARATOR . 'assets'); diff --git a/src/Config/ConfigServiceProvider.php b/src/Config/ConfigServiceProvider.php index 42099caa7..51f691b42 100644 --- a/src/Config/ConfigServiceProvider.php +++ b/src/Config/ConfigServiceProvider.php @@ -14,6 +14,10 @@ class ConfigServiceProvider extends ServiceProvider { protected array $configFiles = ['app.php', 'config.default.php', 'config.php']; + # remember to update ConfigServiceProviderTest, config.default.php, and README.md + protected array $configVarsToPruneNulls + = ['themes', 'tshirt_sizes', 'headers', 'header_items', 'footer_items', 'locales']; + public function __construct(Application $app, protected ?EventConfig $eventConfig = null) { parent::__construct($app); @@ -42,6 +46,12 @@ public function register(): void if (empty($config->get(null))) { throw new Exception('Configuration not found'); } + + foreach ($this->configVarsToPruneNulls as $key) { + $config->set($key, array_filter($config->get($key), function ($v) { + return !is_null($v); + })); + } } public function boot(): void diff --git a/src/Controllers/Admin/FaqController.php b/src/Controllers/Admin/FaqController.php index a01c740e3..a7f8cd3a8 100644 --- a/src/Controllers/Admin/FaqController.php +++ b/src/Controllers/Admin/FaqController.php @@ -46,14 +46,7 @@ public function save(Request $request): Response /** @var Faq $faq */ $faq = $this->faq->findOrNew($faqId); - $data = $this->validate($request, [ - 'question' => 'required', - 'text' => 'required', - 'delete' => 'optional|checked', - 'preview' => 'optional|checked', - ]); - - if (!is_null($data['delete'])) { + if ($request->request->has('delete')) { $faq->delete(); $this->log->info('Deleted faq "{question}"', ['question' => $faq->question]); @@ -63,6 +56,13 @@ public function save(Request $request): Response return $this->redirect->to('/faq'); } + $data = $this->validate($request, [ + 'question' => 'required', + 'text' => 'required', + 'delete' => 'optional|checked', + 'preview' => 'optional|checked', + ]); + $faq->question = $data['question']; $faq->text = $data['text']; diff --git a/src/Controllers/Admin/RoomsController.php b/src/Controllers/Admin/LocationsController.php similarity index 61% rename from src/Controllers/Admin/RoomsController.php rename to src/Controllers/Admin/LocationsController.php index 85e6b2813..23a03ae08 100644 --- a/src/Controllers/Admin/RoomsController.php +++ b/src/Controllers/Admin/LocationsController.php @@ -12,23 +12,23 @@ use Engelsystem\Http\Response; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\AngelType; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\NeededAngelType; use Illuminate\Database\Eloquent\Collection; use Psr\Log\LoggerInterface; -class RoomsController extends BaseController +class LocationsController extends BaseController { use HasUserNotifications; /** @var array */ protected array $permissions = [ - 'admin_rooms', + 'admin_locations', ]; public function __construct( protected LoggerInterface $log, - protected Room $room, + protected Location $location, protected Redirector $redirect, protected Response $response ) { @@ -36,31 +36,31 @@ public function __construct( public function index(): Response { - $rooms = $this->room + $locations = $this->location ->orderBy('name') ->get(); return $this->response->withView( - 'admin/rooms/index', - ['rooms' => $rooms, 'is_index' => true] + 'admin/locations/index', + ['locations' => $locations, 'is_index' => true] ); } public function edit(Request $request): Response { - $roomId = (int) $request->getAttribute('room_id'); + $locationId = (int) $request->getAttribute('location_id'); - $room = $this->room->find($roomId); + $location = $this->location->find($locationId); - return $this->showEdit($room); + return $this->showEdit($location); } public function save(Request $request): Response { - $roomId = (int) $request->getAttribute('room_id'); + $locationId = (int) $request->getAttribute('location_id'); - /** @var Room $room */ - $room = $this->room->findOrNew($roomId); + /** @var Location $location */ + $location = $this->location->findOrNew($locationId); /** @var Collection|AngelType[] $angelTypes */ $angelTypes = AngelType::all(); $validation = []; @@ -82,17 +82,17 @@ public function save(Request $request): Response ] + $validation ); - if (Room::whereName($data['name'])->where('id', '!=', $room->id)->exists()) { + if (Location::whereName($data['name'])->where('id', '!=', $location->id)->exists()) { throw new ValidationException((new Validator())->addErrors(['name' => ['validation.name.exists']])); } - $room->name = $data['name']; - $room->description = $data['description']; - $room->dect = $data['dect']; - $room->map_url = $data['map_url']; + $location->name = $data['name']; + $location->description = $data['description']; + $location->dect = $data['dect']; + $location->map_url = $data['map_url']; - $room->save(); - $room->neededAngelTypes()->getQuery()->delete(); + $location->save(); + $location->neededAngelTypes()->getQuery()->delete(); $angelsInfo = ''; foreach ($angelTypes as $angelType) { @@ -103,7 +103,7 @@ public function save(Request $request): Response $neededAngelType = new NeededAngelType(); - $neededAngelType->room()->associate($room); + $neededAngelType->location()->associate($location); $neededAngelType->angelType()->associate($angelType); $neededAngelType->count = $data['angel_type_' . $angelType->id]; @@ -114,19 +114,19 @@ public function save(Request $request): Response } $this->log->info( - 'Updated room "{name}": {description} {dect} {map_url} {angels}', + 'Updated location "{name}": {description} {dect} {map_url} {angels}', [ - 'name' => $room->name, - 'description' => $room->description, - 'dect' => $room->dect, - 'map_url' => $room->map_url, + 'name' => $location->name, + 'description' => $location->description, + 'dect' => $location->dect, + 'map_url' => $location->map_url, 'angels' => $angelsInfo, ] ); - $this->addNotification('room.edit.success'); + $this->addNotification('location.edit.success'); - return $this->redirect->to('/admin/rooms'); + return $this->redirect->to('/admin/locations'); } public function delete(Request $request): Response @@ -136,9 +136,9 @@ public function delete(Request $request): Response 'delete' => 'checked', ]); - $room = $this->room->findOrFail($data['id']); + $location = $this->location->findOrFail($data['id']); - $shifts = $room->shifts; + $shifts = $location->shifts; foreach ($shifts as $shift) { foreach ($shift->shiftEntries as $entry) { event('shift.entry.deleting', [ @@ -148,27 +148,31 @@ public function delete(Request $request): Response 'name' => $shift->shiftType->name, 'title' => $shift->title, 'type' => $entry->angelType->name, - 'room' => $room, + 'location' => $location, 'freeloaded' => $entry->freeloaded, ]); } } - $room->delete(); + $location->delete(); - $this->log->info('Deleted room {room}', ['room' => $room->name]); - $this->addNotification('room.delete.success'); + $this->log->info('Deleted location {location}', ['location' => $location->name]); + $this->addNotification('location.delete.success'); - return $this->redirect->to('/admin/rooms'); + return $this->redirect->to('/admin/locations'); } - protected function showEdit(?Room $room): Response + protected function showEdit(?Location $location): Response { $angeltypes = AngelType::all() ->sortBy('name'); return $this->response->withView( - 'admin/rooms/edit', - ['room' => $room, 'angel_types' => $angeltypes, 'needed_angel_types' => $room?->neededAngelTypes] + 'admin/locations/edit', + [ + 'location' => $location, + 'angel_types' => $angeltypes, + 'needed_angel_types' => $location?->neededAngelTypes, + ] ); } } diff --git a/src/Controllers/Admin/LogsController.php b/src/Controllers/Admin/LogsController.php index 77469238f..10f25240c 100644 --- a/src/Controllers/Admin/LogsController.php +++ b/src/Controllers/Admin/LogsController.php @@ -5,9 +5,12 @@ namespace Engelsystem\Controllers\Admin; use Engelsystem\Controllers\BaseController; +use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\LogEntry; +use Engelsystem\Models\User\User; +use Illuminate\Support\Collection; class LogsController extends BaseController { @@ -16,18 +19,33 @@ class LogsController extends BaseController 'admin_log', ]; - public function __construct(protected LogEntry $log, protected Response $response) + public function __construct(protected LogEntry $log, protected Response $response, protected Authenticator $auth) { } public function index(Request $request): Response { + $searchUserId = (int) $request->input('search_user_id') ?: null; $search = $request->input('search'); - $entries = $this->log->filter($search); + $userId = $this->auth->user()?->id; + + if ($this->auth->can('logs.all')) { + $userId = $searchUserId; + } + + $entries = $this->log->filter($search, $userId); + + /** @var Collection $users */ + $users = User::with('personalData') + ->orderBy('name') + ->get() + ->mapWithKeys(function (User $u) { + return [$u->id => $u->displayName]; + }); return $this->response->withView( 'admin/log.twig', - ['entries' => $entries, 'search' => $search] + ['entries' => $entries, 'search' => $search, 'users' => $users, 'search_user_id' => $searchUserId] ); } } diff --git a/src/Controllers/Admin/NewsController.php b/src/Controllers/Admin/NewsController.php index 848b259d4..a94263dab 100644 --- a/src/Controllers/Admin/NewsController.php +++ b/src/Controllers/Admin/NewsController.php @@ -6,6 +6,7 @@ use Engelsystem\Controllers\BaseController; use Engelsystem\Controllers\HasUserNotifications; +use Engelsystem\Controllers\NotificationType; use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; @@ -38,18 +39,19 @@ public function edit(Request $request): Response $news = $this->news->find($newsId); $isMeeting = (bool) $request->get('meeting', false); - return $this->showEdit($news, $isMeeting); + return $this->showEdit($news, !$news, $isMeeting); } - protected function showEdit(?News $news, bool $isMeetingDefault = false): Response + protected function showEdit(?News $news, bool $sendNotification = true, bool $isMeetingDefault = false): Response { return $this->response->withView( 'pages/news/edit.twig', [ - 'news' => $news, - 'is_meeting' => $news ? $news->is_meeting : $isMeetingDefault, - 'is_pinned' => $news ? $news->is_pinned : false, - 'is_important' => $news ? $news->is_important : false, + 'news' => $news, + 'is_meeting' => $news ? $news->is_meeting : $isMeetingDefault, + 'is_pinned' => $news ? $news->is_pinned : false, + 'is_highlighted' => $news ? $news->is_highlighted : false, + 'send_notification' => $sendNotification, ], ); } @@ -61,17 +63,7 @@ public function save(Request $request): Response /** @var News $news */ $news = $this->news->findOrNew($newsId); - $data = $this->validate($request, [ - 'title' => 'required', - 'text' => 'required', - 'is_meeting' => 'optional|checked', - 'is_pinned' => 'optional|checked', - 'is_important' => 'optional|checked', - 'delete' => 'optional|checked', - 'preview' => 'optional|checked', - ]); - - if (!is_null($data['delete'])) { + if ($request->request->has('delete')) { $news->delete(); $this->log->info( @@ -87,6 +79,17 @@ public function save(Request $request): Response return $this->redirect->to('/news'); } + $data = $this->validate($request, [ + 'title' => 'required', + 'text' => 'required', + 'is_meeting' => 'optional|checked', + 'is_pinned' => 'optional|checked', + 'is_highlighted' => 'optional|checked', + 'delete' => 'optional|checked', + 'preview' => 'optional|checked', + 'send_notification' => 'optional|checked', + ]); + if (!$news->user) { $news->user()->associate($this->auth->user()); } @@ -94,27 +97,34 @@ public function save(Request $request): Response $news->text = $data['text']; $news->is_meeting = !is_null($data['is_meeting']); $news->is_pinned = !is_null($data['is_pinned']); + $notify = !is_null($data['send_notification']); - if ($this->auth->can('news.important')) { - $news->is_important = !is_null($data['is_important']); + if ($this->auth->can('news.highlight')) { + $news->is_highlighted = !is_null($data['is_highlighted']); } if (!is_null($data['preview'])) { - return $this->showEdit($news); + return $this->showEdit($news, $notify); } $isNewNews = !$news->id; + if ($isNewNews && News::where('title', $news->title)->where('text', $news->text)->count()) { + $this->addNotification('news.edit.duplicate', NotificationType::ERROR); + return $this->showEdit($news, $notify); + } $news->save(); if ($isNewNews) { - event('news.created', ['news' => $news]); + event('news.created', ['news' => $news, 'sendNotification' => $notify]); + } else { + event('news.updated', ['news' => $news, 'sendNotification' => $notify]); } $this->log->info( 'Updated {pinned}{type} "{news}": {text}', [ 'pinned' => $news->is_pinned ? 'pinned ' : '', - 'important' => $news->is_important ? 'important ' : '', + 'highlighted' => $news->is_highlighted ? 'highlighted ' : '', 'type' => $news->is_meeting ? 'meeting' : 'news', 'news' => $news->title, 'text' => $news->text, diff --git a/src/Controllers/Admin/QuestionsController.php b/src/Controllers/Admin/QuestionsController.php index c033bdb4c..cf6d7be90 100644 --- a/src/Controllers/Admin/QuestionsController.php +++ b/src/Controllers/Admin/QuestionsController.php @@ -38,7 +38,8 @@ public function index(): Response $questions = $this->question ->orderBy('answered_at') ->orderByDesc('created_at') - ->get(); + ->get() + ->load(['user.state', 'answerer.state']); return $this->response->withView( 'pages/questions/overview.twig', diff --git a/src/Controllers/Admin/ShiftTypesController.php b/src/Controllers/Admin/ShiftTypesController.php new file mode 100644 index 000000000..8fd1fb8ed --- /dev/null +++ b/src/Controllers/Admin/ShiftTypesController.php @@ -0,0 +1,180 @@ + */ + protected array $permissions = [ + 'shifttypes', + 'edit' => 'shifttypes.edit', + 'delete' => 'shifttypes.edit', + 'save' => 'shifttypes.edit', + ]; + + public function __construct( + protected LoggerInterface $log, + protected ShiftType $shiftType, + protected Redirector $redirect, + protected Response $response + ) { + } + + public function index(): Response + { + $shiftTypes = $this->shiftType + ->get() + ->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE); + + return $this->response->withView( + 'admin/shifttypes/index', + ['shifttypes' => $shiftTypes, 'is_index' => true] + ); + } + + public function edit(Request $request): Response + { + $shiftTypeId = (int) $request->getAttribute('shift_type_id'); + + $shiftType = $this->shiftType->find($shiftTypeId); + $angeltypes = AngelType::all() + ->sortBy('name'); + + return $this->response->withView( + 'admin/shifttypes/edit', + [ + 'shifttype' => $shiftType, + 'angel_types' => $angeltypes, + ] + ); + } + + public function view(Request $request): Response + { + $shiftTypeId = (int) $request->getAttribute('shift_type_id'); + $shiftType = $this->shiftType->findOrFail($shiftTypeId); + + return $this->response->withView( + 'admin/shifttypes/view', + ['shifttype' => $shiftType, 'is_view' => true] + ); + } + + public function save(Request $request): Response + { + $shiftTypeId = (int) $request->getAttribute('shift_type_id'); + + /** @var ShiftType $shiftType */ + $shiftType = $this->shiftType->findOrNew($shiftTypeId); + + if ($request->request->has('delete')) { + return $this->delete($request); + } + + /** @var Collection|AngelType[] $angelTypes */ + $angelTypes = AngelType::all(); + $validation = []; + foreach ($angelTypes as $angelType) { + $validation['angel_type_' . $angelType->id] = 'optional|int'; + } + + $data = $this->validate( + $request, + [ + 'name' => 'required', + 'description' => 'required|optional', + ] + $validation + ); + + if (ShiftType::whereName($data['name'])->where('id', '!=', $shiftType->id)->exists()) { + throw new ValidationException((new Validator())->addErrors(['name' => ['validation.name.exists']])); + } + + $shiftType->name = $data['name']; + $shiftType->description = $data['description']; + + $shiftType->save(); + $shiftType->neededAngelTypes()->delete(); + + $angelsInfo = ''; + foreach ($angelTypes as $angelType) { + $count = $data['angel_type_' . $angelType->id]; + if (!$count) { + continue; + } + + $neededAngelType = new NeededAngelType(); + + $neededAngelType->shiftType()->associate($shiftType); + $neededAngelType->angelType()->associate($angelType); + + $neededAngelType->count = $data['angel_type_' . $angelType->id]; + + $neededAngelType->save(); + + $angelsInfo .= sprintf(', %s: %s', $angelType->name, $count); + } + + $this->log->info( + 'Updated shift type "{name}": {description} {angels}', + [ + 'name' => $shiftType->name, + 'description' => $shiftType->description, + 'angels' => $angelsInfo, + ] + ); + + $this->addNotification('shifttype.edit.success'); + + return $this->redirect->to('/admin/shifttypes'); + } + + public function delete(Request $request): Response + { + $data = $this->validate($request, [ + 'id' => 'required|int', + 'delete' => 'checked', + ]); + + $shiftType = $this->shiftType->findOrFail($data['id']); + + $shifts = $shiftType->shifts; + foreach ($shifts as $shift) { + foreach ($shift->shiftEntries as $entry) { + event('shift.entry.deleting', [ + 'user' => $entry->user, + 'start' => $shift->start, + 'end' => $shift->end, + 'name' => $shift->shiftType->name, + 'title' => $shift->title, + 'type' => $entry->angelType->name, + 'location' => $shift->location, + 'freeloaded' => $entry->freeloaded, + ]); + } + } + $shiftType->delete(); + + $this->log->info('Deleted shift type {name}', ['name' => $shiftType->name]); + $this->addNotification('shifttype.delete.success'); + + return $this->redirect->to('/admin/shifttypes'); + } +} diff --git a/src/Controllers/Admin/ShiftsController.php b/src/Controllers/Admin/ShiftsController.php new file mode 100644 index 000000000..3b4bb09c0 --- /dev/null +++ b/src/Controllers/Admin/ShiftsController.php @@ -0,0 +1,94 @@ + */ + protected array $permissions = [ + 'admin_shifts', + ]; + + public function __construct( + protected LoggerInterface $log, + protected Shift $shift, + protected Redirector $redirect, + protected Response $response + ) { + } + + public function history(): Response + { + $shifts = $this->shift + ->select() + ->selectRaw('MIN(start) AS start') + ->selectRaw('MAX(end) AS end') + ->selectRaw('COUNT(*) AS count') + ->selectRaw('MIN(created_at) AS created_at') + ->with(['schedule', 'createdBy']) + ->whereNotNull('transaction_id') + ->groupBy('transaction_id') + ->orderByDesc('created_at') + ->get(); + return $this->response->withView('admin/shifts/history', ['shifts' => $shifts]); + } + + public function deleteTransaction(Request $request): Response + { + $transactionId = $request->postData('transaction_id'); + + /** @var Shift[]|Collection $shifts */ + $shifts = $this->shift->with([ + 'location', + 'shiftEntries', + 'shiftEntries.angelType', + 'shiftEntries.user', + 'shiftType', + ])->where('transaction_id', $transactionId)->get(); + + $this->log->info( + 'Deleting {count} shifts with transaction ID: {id}', + ['count' => $shifts->count(), 'id' => $transactionId] + ); + + foreach ($shifts as $shift) { + foreach ($shift->shiftEntries as $entry) { + event('shift.entry.deleting', [ + 'user' => $entry->user, + 'start' => $shift->start, + 'end' => $shift->end, + 'name' => $shift->shiftType->name, + 'title' => $shift->title, + 'type' => $entry->angelType->name, + 'location' => $shift->location, + 'freeloaded' => $entry->freeloaded, + ]); + } + + $shift->delete(); + + $this->log->info( + 'Deleted shift ' . $shift->title . ' / ' . $shift->shiftType->name + . ' from ' . $shift->start->format('Y-m-d H:i') + . ' to ' . $shift->end->format('Y-m-d H:i') + ); + } + + $this->addNotification('shifts.history.delete.success'); + + return $this->redirect->back(); + } +} diff --git a/src/Controllers/Admin/UserShirtController.php b/src/Controllers/Admin/UserShirtController.php index 807a607f4..d213b0bcf 100644 --- a/src/Controllers/Admin/UserShirtController.php +++ b/src/Controllers/Admin/UserShirtController.php @@ -58,13 +58,13 @@ public function saveShirt(Request $request): Response $user = $this->user->findOrFail($userId); $data = $this->validate($request, [ - 'shirt_size' => $shirtEnabled ? 'required' : 'optional', + 'shirt_size' => ($shirtEnabled ? 'required' : 'optional') . '|shirt_size', 'arrived' => 'optional|checked', 'active' => 'optional|checked', 'got_shirt' => 'optional|checked', ]); - if ($shirtEnabled && isset($this->config->get('tshirt_sizes')[$data['shirt_size']])) { + if ($shirtEnabled) { $user->personalData->shirt_size = $data['shirt_size']; $user->personalData->save(); } diff --git a/src/Controllers/Admin/UserWorkLogController.php b/src/Controllers/Admin/UserWorkLogController.php index 829d012a2..89c315ba8 100644 --- a/src/Controllers/Admin/UserWorkLogController.php +++ b/src/Controllers/Admin/UserWorkLogController.php @@ -52,7 +52,7 @@ public function editWorklog(Request $request): Response } return $this->showEditWorklog($user, $worklog->worked_at, $worklog->hours, $worklog->comment, true); } else { - return $this->showEditWorklog($user, $this->getWorkDateSuggestion()); + return $this->showEditWorklog($user, Carbon::today()); } } @@ -64,9 +64,9 @@ public function saveWorklog(Request $request): Response $user = $this->user->findOrFail($userId); $data = $this->validate($request, [ - 'work_date' => 'required|date:Y-m-d', + 'work_date' => 'required|date:Y-m-d', 'work_hours' => 'float|min:0', - 'comment' => 'required|max:200', + 'comment' => 'required|max:200', ]); if (isset($worklogId)) { @@ -85,6 +85,16 @@ public function saveWorklog(Request $request): Response $worklog->comment = $data['comment']; $worklog->save(); + $this->log->info( + 'Added worklog for {name} ({id}) at {time} spanning {hours}h: {text}', + [ + 'name' => $user->name, + 'id' => $user->id, + 'time' => $worklog->worked_at, + 'hours' => $worklog->hours, + 'text' => $worklog->comment, + ] + ); $this->addNotification(isset($worklogId) ? 'worklog.edit.success' : 'worklog.add.success'); return $this->redirect->to('/users?action=view&user_id=' . $userId); @@ -121,6 +131,16 @@ public function deleteWorklog(Request $request): Response } $worklog->delete(); + $this->log->info( + 'Deleted worklog for {name} ({id}) at {time} spanning {hours}h: {text}', + [ + 'name' => $worklog->user->name, + 'id' => $worklog->user->id, + 'time' => $worklog->worked_at, + 'hours' => $worklog->hours, + 'text' => $worklog->comment, + ] + ); $this->addNotification('worklog.delete.success'); return $this->redirect->to('/users?action=view&user_id=' . $userId); @@ -145,16 +165,4 @@ private function showEditWorklog( ] ); } - - private function getWorkDateSuggestion(): Carbon - { - $buildup_start = config('buildup_start'); - $event_start = config('event_start'); - - $work_date_suggestion = Carbon::today(); - if (!empty($buildup_start) && (empty($event_start) || $event_start->lessThan(Carbon::now()))) { - $work_date_suggestion = $buildup_start->startOfDay(); - } - return $work_date_suggestion; - } } diff --git a/src/Controllers/Api/AngelTypeController.php b/src/Controllers/Api/AngelTypeController.php new file mode 100644 index 000000000..296cf3c87 --- /dev/null +++ b/src/Controllers/Api/AngelTypeController.php @@ -0,0 +1,38 @@ +orderBy('name') + ->get(); + + $data = ['data' => AngelTypeResource::collection($models)]; + return $this->response + ->withContent(json_encode($data)); + } + + public function ofUser(Request $request): Response + { + $id = $request->getAttribute('user_id'); + $user = $this->getUser($id); + + $data = ['data' => UserAngelTypeResource::collection($user->userAngelTypes)]; + + return $this->response + ->withContent(json_encode($data)); + } +} diff --git a/src/Controllers/Api/ApiController.php b/src/Controllers/Api/ApiController.php new file mode 100644 index 000000000..7e4097d51 --- /dev/null +++ b/src/Controllers/Api/ApiController.php @@ -0,0 +1,23 @@ +response = $this->response + ->withHeader('content-type', 'application/json') + // Using * here to "skip" all other headers on browser requests + ->withHeader('access-control-allow-origin', '*'); + } +} diff --git a/src/Controllers/ApiController.php b/src/Controllers/Api/FsrController.php similarity index 66% rename from src/Controllers/ApiController.php rename to src/Controllers/Api/FsrController.php index 53d34d831..226dcc554 100644 --- a/src/Controllers/ApiController.php +++ b/src/Controllers/Api/FsrController.php @@ -2,24 +2,18 @@ declare(strict_types=1); -namespace Engelsystem\Controllers; +namespace Engelsystem\Controllers\Api; use Engelsystem\Http\Response; use Engelsystem\Http\Request; use Engelsystem\Database\Db; -class ApiController extends BaseController +class FsrController extends ApiController { - public function __construct(protected Response $response) - { - } + public array $permissions = []; - public function index(): Response + public function __construct(protected Response $response) { - return $this->response - ->setStatusCode(501) - ->withHeader('content-type', 'application/json') - ->withContent(json_encode(['error' => 'Not implemented'])); } /** @@ -32,7 +26,7 @@ public function usershifts(Request $request): Response $count = (Db::selectOne( ' SELECT COUNT(*) AS count FROM shift_entries, users - WHERE shift_entries.user_id = users.id + WHERE shift_entries.user_id = users.id AND users.email = ?; ', [$email] )); diff --git a/src/Controllers/Api/IndexController.php b/src/Controllers/Api/IndexController.php new file mode 100644 index 000000000..0a2fc1da4 --- /dev/null +++ b/src/Controllers/Api/IndexController.php @@ -0,0 +1,116 @@ + 'api', + 'indexV0' => 'api', + 'openApiV0' => 'api', + ]; + + public function index(): Response + { + return $this->response + ->withContent(json_encode([ + 'versions' => [ + $this->getApiSpecV0()->info->version => '/v0-beta', + ], + ])); + } + + public function indexV0(): Response + { + $schema = $this->getApiSpecV0(); + $info = $schema->info; + $paths = []; + foreach ($schema->paths->getIterator() as $path => $item) { + $paths[] = $path; + } + + return $this->response + ->withContent(json_encode([ + 'version' => $info->version, + 'description' => $info->description, + 'paths' => $paths, + ])); + } + + public function openApiV0(): Response + { + $schema = $this->getApiSpecV0(); + $data = $schema->getSerializableData(); + unset($data->servers[1]); + $data->servers[0]->url = url('/api/v0-beta'); + + return $this->response->withContent(json_encode($data)); + } + + public function info(): Response + { + $config = config(); + $schema = $this->getApiSpecV0(); + + $data = ['data' => [ + 'api' => $schema->info->version, + 'spec' => url('/api/v0-beta/openapi'), + 'name' => (string) $config->get('name'), + 'app_name' => (string) $config->get('app_name'), + 'url' => url('/'), + 'timezone' => (string) $config->get('timezone'), + 'buildup' => [ + 'start' => $config->get('buildup_start'), + ], + 'event' => [ + 'start' => $config->get('event_start'), + 'end' => $config->get('event_end'), + ], + 'teardown' => [ + 'end' => $config->get('teardown_end'), + ], + ]]; + + return $this->response + ->withContent(json_encode($data)); + } + + public function options(): Response + { + // Respond to browser preflight options requests + return $this->response + ->setStatusCode(200) + ->withHeader('allow', 'OPTIONS, HEAD, GET') + ->withHeader('access-control-allow-headers', 'Authorization, x-api-key'); + } + + public function notFound(): Response + { + return $this->response + ->setStatusCode(404) + ->withContent(json_encode(['message' => 'Not implemented'])); + } + + public function notImplemented(): Response + { + return $this->response + ->setStatusCode(405) + ->withHeader('allow', 'GET') + ->withContent(json_encode(['message' => 'Method not implemented'])); + } + + protected function getApiSpecV0(): OpenApi + { + $openApiDefinition = app()->get('path.resources.api') . '/openapi.yml'; + return (new OpenApiValidatorBuilder()) + ->fromYamlFile($openApiDefinition) + ->getResponseValidator() + ->getSchema(); + } +} diff --git a/src/Controllers/Api/LocationsController.php b/src/Controllers/Api/LocationsController.php new file mode 100644 index 000000000..9a9c46675 --- /dev/null +++ b/src/Controllers/Api/LocationsController.php @@ -0,0 +1,23 @@ +orderBy('name') + ->get(); + + $data = ['data' => LocationResource::collection($models)]; + return $this->response + ->withContent(json_encode($data)); + } +} diff --git a/src/Controllers/Api/NewsController.php b/src/Controllers/Api/NewsController.php new file mode 100644 index 000000000..4ef26d081 --- /dev/null +++ b/src/Controllers/Api/NewsController.php @@ -0,0 +1,24 @@ +orderByDesc('updated_at') + ->orderByDesc('created_at') + ->get(); + + $data = ['data' => NewsResource::collection($models)]; + return $this->response + ->withContent(json_encode($data)); + } +} diff --git a/src/Controllers/Api/Resources/AngelTypeResource.php b/src/Controllers/Api/Resources/AngelTypeResource.php new file mode 100644 index 000000000..2b37f9f35 --- /dev/null +++ b/src/Controllers/Api/Resources/AngelTypeResource.php @@ -0,0 +1,18 @@ + $this->model->id, + 'name' => $this->model->name, + 'description' => $this->model->description, + 'url' => url('/angeltypes', ['action' => 'view', 'angeltype_id' => $this->model->id]), + ]; + } +} diff --git a/src/Controllers/Api/Resources/BasicResource.php b/src/Controllers/Api/Resources/BasicResource.php new file mode 100644 index 000000000..b801a86c6 --- /dev/null +++ b/src/Controllers/Api/Resources/BasicResource.php @@ -0,0 +1,49 @@ +add(new static($item)); + } + return $collection; + } + + public function toArray(): array + { + return $this->model->toArray(); + } + + /** + * @param int $options + */ + public function toJson($options = 0): string // phpcs:ignore + { + return json_encode($this->toArray(), $options); + } + + public function __toString(): string + { + return $this->toJson(); + } +} diff --git a/src/Controllers/Api/Resources/LocationResource.php b/src/Controllers/Api/Resources/LocationResource.php new file mode 100644 index 000000000..e05bcc249 --- /dev/null +++ b/src/Controllers/Api/Resources/LocationResource.php @@ -0,0 +1,17 @@ + $this->model->id, + 'name' => $this->model->name, + 'url' => url('/locations', ['action' => 'view', 'location_id' => $this->model->id]), + ]; + } +} diff --git a/src/Controllers/Api/Resources/NewsResource.php b/src/Controllers/Api/Resources/NewsResource.php new file mode 100644 index 000000000..ef2a6d24c --- /dev/null +++ b/src/Controllers/Api/Resources/NewsResource.php @@ -0,0 +1,23 @@ + $this->model->id, + 'title' => $this->model->title, + 'text' => $this->model->text, + 'is_meeting' => $this->model->is_meeting, + 'is_pinned' => $this->model->is_pinned, + 'is_highlighted' => $this->model->is_highlighted, + 'created_at' => $this->model->created_at, + 'updated_at' => $this->model->updated_at, + 'url' => url('/news/' . $this->model->id), + ]; + } +} diff --git a/src/Controllers/Api/Resources/ShiftResource.php b/src/Controllers/Api/Resources/ShiftResource.php new file mode 100644 index 000000000..2087d4153 --- /dev/null +++ b/src/Controllers/Api/Resources/ShiftResource.php @@ -0,0 +1,26 @@ + $this->model->id, + 'title' => $this->model->title, + 'description' => $this->model->description, + 'starts_at' => $this->model->start, + 'ends_at' => $this->model->end, + 'location' => $location instanceof Arrayable ? $location->toArray() : $location, + 'shift_type' => (new ShiftTypeResource($this->model->shiftType))->toArray(), + 'created_at' => $this->model->created_at, + 'updated_at' => $this->model->updated_at, + 'url' => url('/shifts', ['action' => 'view', 'shift_id' => $this->model->id]), + ]; + } +} diff --git a/src/Controllers/Api/Resources/ShiftTypeResource.php b/src/Controllers/Api/Resources/ShiftTypeResource.php new file mode 100644 index 000000000..b336a0567 --- /dev/null +++ b/src/Controllers/Api/Resources/ShiftTypeResource.php @@ -0,0 +1,17 @@ + $this->model->id, + 'name' => $this->model->name, + 'description' => $this->model->description, + ]; + } +} diff --git a/src/Controllers/Api/Resources/ShiftWithEntriesResource.php b/src/Controllers/Api/Resources/ShiftWithEntriesResource.php new file mode 100644 index 000000000..1bd78c712 --- /dev/null +++ b/src/Controllers/Api/Resources/ShiftWithEntriesResource.php @@ -0,0 +1,18 @@ + $entries instanceof Arrayable ? $entries->toArray() : $entries, + ]; + } +} diff --git a/src/Controllers/Api/Resources/UserAngelTypeResource.php b/src/Controllers/Api/Resources/UserAngelTypeResource.php new file mode 100644 index 000000000..70da33322 --- /dev/null +++ b/src/Controllers/Api/Resources/UserAngelTypeResource.php @@ -0,0 +1,19 @@ + !$this->model->restricted + || $this->model->pivot->supporter + || $this->model->pivot->confirm_user_id, + 'supporter' => $this->model->pivot->supporter, + ]; + } +} diff --git a/src/Controllers/Api/Resources/UserDetailResource.php b/src/Controllers/Api/Resources/UserDetailResource.php new file mode 100644 index 000000000..010c2e293 --- /dev/null +++ b/src/Controllers/Api/Resources/UserDetailResource.php @@ -0,0 +1,23 @@ + $this->model->contact->email ?: $this->model->email, + 'tshirt' => $this->model->personalData->shirt_size, + 'language' => $this->model->settings->language, + 'arrived' => $this->model->state->arrived, + 'dates' => [ + 'planned_arrival' => $this->model->personalData->planned_arrival_date, + 'planned_departure' => $this->model->personalData->planned_departure_date, + 'arrival' => $this->model->state->arrival_date, + ], + ]); + } +} diff --git a/src/Controllers/Api/Resources/UserResource.php b/src/Controllers/Api/Resources/UserResource.php new file mode 100644 index 000000000..8a89406ca --- /dev/null +++ b/src/Controllers/Api/Resources/UserResource.php @@ -0,0 +1,21 @@ + $this->model->id, + 'name' => $this->model->name, + 'first_name' => $this->model->personalData->first_name, + 'last_name' => $this->model->personalData->last_name, + 'pronoun' => $this->model->personalData->pronoun, + 'contact' => $this->model->contact->only(['dect', 'mobile']), + 'url' => url('/users', ['action' => 'view', 'user_id' => $this->model->id]), + ]; + } +} diff --git a/src/Controllers/Api/ShiftsController.php b/src/Controllers/Api/ShiftsController.php new file mode 100644 index 000000000..5493c8210 --- /dev/null +++ b/src/Controllers/Api/ShiftsController.php @@ -0,0 +1,175 @@ +getAttribute('angeltype_id'); + /** @var AngelType $angeltype */ + $angeltype = AngelType::findOrFail($id); + /** @var ShiftEntry[]|Collection $shifts */ + $shiftEntries = $angeltype->shiftEntries() + ->with([ + 'shift.neededAngelTypes.angelType', + 'shift.location.neededAngelTypes.angelType', + 'shift.shiftEntries.angelType', + 'shift.shiftEntries.user.contact', + 'shift.shiftEntries.user.personalData', + 'shift.shiftType', + 'shift.schedule.shiftType.neededAngelTypes.angelType', + ]) + ->get(); + + /** @var Shift[]|Collection $shifts */ + $shifts = Collection::make( + $shiftEntries + ->pluck('shift') + ->sortBy('start') + ); + + return $this->shiftEntriesResponse($shifts); + } + + public function entriesByLocation(Request $request): Response + { + $locationId = (int) $request->getAttribute('location_id'); + /** @var Location $location */ + $location = Location::findOrFail($locationId); + /** @var Shift[]|Collection $shifts */ + $shifts = $location->shifts() + ->with([ + 'neededAngelTypes.angelType', + 'location.neededAngelTypes.angelType', + 'shiftEntries.angelType', + 'shiftEntries.user.contact', + 'shiftEntries.user.personalData', + 'shiftType', + 'schedule.shiftType.neededAngelTypes.angelType', + ]) + ->orderBy('start') + ->get(); + + return $this->shiftEntriesResponse($shifts); + } + + public function entriesByUser(Request $request): Response + { + $id = $request->getAttribute('user_id'); + $user = $this->getUser($id); + + /** @var ShiftEntry[]|Collection $shifts */ + $shiftEntries = $user->shiftEntries() + ->with([ + 'shift.neededAngelTypes.angelType', + 'shift.location.neededAngelTypes.angelType', + 'shift.shiftEntries.angelType', + 'shift.shiftEntries.user.contact', + 'shift.shiftEntries.user.personalData', + 'shift.shiftType', + 'shift.schedule.shiftType.neededAngelTypes.angelType', + ]) + ->get(); + + /** @var Shift[]|Collection $shifts */ + $shifts = Collection::make( + $shiftEntries + ->pluck('shift') + ->sortBy('start') + ); + + return $this->shiftEntriesResponse($shifts); + } + + protected function shiftEntriesResponse(Collection $shifts): Response + { + /** @var Collection|Shift[] $shifts */ + $shiftEntries = []; + // Blob of not-optimized mediocre pseudo-serialization + foreach ($shifts as $shift) { + // Get all needed/used angel types + $neededAngelTypes = $this->getNeededAngelTypes($shift); + + $entries = new Collection(); + foreach ($neededAngelTypes as $neededAngelType) { + $users = UserResource::collection($neededAngelType->users ?? []); + + // Skip empty entries + if ($neededAngelType->count <= 0 && $users->isEmpty()) { + continue; + } + + $angelTypeData = new AngelTypeResource($neededAngelType->angelType); + $entries[] = new Collection([ + 'users' => $users, + 'type' => $angelTypeData, + 'needs' => $neededAngelType->count, + ]); + } + + $locationData = new LocationResource($shift->location); + $shiftEntries[] = (new ShiftWithEntriesResource($shift))->toArray($locationData, $entries); + } + + $data = ['data' => $shiftEntries]; + return $this->response + ->withContent(json_encode($data)); + } + + /** + * Collect all needed angeltypes + */ + protected function getNeededAngelTypes(Shift $shift): Collection + { + $neededAngelTypes = new Collection(); + if (!$shift->schedule) { + // Get from shift + $neededAngelTypes = $shift->neededAngelTypes; + } elseif ($shift->schedule->needed_from_shift_type) { + // Load instead from shift type + $neededAngelTypes = $shift->schedule->shiftType->neededAngelTypes; + } elseif (!$shift->schedule->needed_from_shift_type) { + // Load instead from location + $neededAngelTypes = $shift->location->neededAngelTypes; + } + + // Add needed angeltypes from additionally added users + foreach ($shift->shiftEntries as $entry) { + $neededAngelType = $neededAngelTypes->where('angel_type_id', $entry->angelType->id)->first(); + if (!$neededAngelType) { + $neededAngelType = new NeededAngelType([ + 'shift_id' => $shift->id, + 'angel_type_id' => $entry->angelType->id, + 'count' => 0, + ]); + $neededAngelTypes[] = $neededAngelType; + } + + // Add users to entries + $neededAngelType->users = isset($neededAngelType->users) + ? $neededAngelType->users + : new Collection(); + $neededAngelType->users[] = $entry->user; + } + + return $neededAngelTypes; + } +} diff --git a/src/Controllers/Api/UsersController.php b/src/Controllers/Api/UsersController.php new file mode 100644 index 000000000..5173a1280 --- /dev/null +++ b/src/Controllers/Api/UsersController.php @@ -0,0 +1,26 @@ +getAttribute('user_id'); + $user = $this->getUser($id); + + $userData = $user->id == $this->auth->user()->id ? new UserDetailResource($user) : new UserResource($user); + $data = ['data' => $userData->toArray()]; + return $this->response + ->withContent(json_encode($data)); + } +} diff --git a/src/Controllers/Api/UsesAuth.php b/src/Controllers/Api/UsesAuth.php new file mode 100644 index 000000000..cefaaeb94 --- /dev/null +++ b/src/Controllers/Api/UsesAuth.php @@ -0,0 +1,27 @@ +auth = $auth; + } + + protected function getUser(int|string $userId): ?User + { + if ($userId == 'self' && $this->auth) { + return $this->auth->user(); + } + + return User::findOrFail($userId); + } +} diff --git a/src/Controllers/Api/UsesAuthServiceProvider.php b/src/Controllers/Api/UsesAuthServiceProvider.php new file mode 100644 index 000000000..e07873503 --- /dev/null +++ b/src/Controllers/Api/UsesAuthServiceProvider.php @@ -0,0 +1,24 @@ +app->afterResolving(function ($object, Application $app): void { + if (!$object instanceof ApiController || !method_exists($object, 'setAuth')) { + return; + } + + /** @var UsesAuth $object */ + $object->setAuth($app->get(Authenticator::class)); + }); + } +} diff --git a/src/Controllers/DesignController.php b/src/Controllers/DesignController.php index f31177cb5..a8ff1f0b1 100644 --- a/src/Controllers/DesignController.php +++ b/src/Controllers/DesignController.php @@ -54,7 +54,7 @@ public function index(): Response for ($i = 1; $i <= 600; $i++) { $dateKey = $date->format('Y-m-d'); - $formattedDisplayDate = $date->format(__('Y-m-d')); + $formattedDisplayDate = $date->format(__('general.date')); $dateSelectOptions[$dateKey] = $formattedDisplayDate; $date = $date->addDay(); } diff --git a/src/Controllers/FeedController.php b/src/Controllers/FeedController.php index 6be7ca504..4be2eb3da 100644 --- a/src/Controllers/FeedController.php +++ b/src/Controllers/FeedController.php @@ -8,6 +8,7 @@ use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Request; use Engelsystem\Http\Response; +use Engelsystem\Http\UrlGenerator; use Engelsystem\Models\News; use Engelsystem\Models\Shifts\ShiftEntry; use Illuminate\Support\Collection; @@ -26,6 +27,7 @@ public function __construct( protected Authenticator $auth, protected Request $request, protected Response $response, + protected UrlGenerator $url, ) { } @@ -33,7 +35,7 @@ public function atom(): Response { $news = $this->getNews(); - return $this->response + return $this->withEtag($news) ->withHeader('content-type', 'application/atom+xml; charset=utf-8') ->withView('api/atom', ['news' => $news]); } @@ -42,7 +44,7 @@ public function rss(): Response { $news = $this->getNews(); - return $this->response + return $this->withEtag($news) ->withHeader('content-type', 'application/rss+xml; charset=utf-8') ->withView('api/rss', ['news' => $news]); } @@ -51,7 +53,7 @@ public function ical(): Response { $shifts = $this->getShifts(); - return $this->response + return $this->withEtag($shifts) ->withHeader('content-type', 'text/calendar; charset=utf-8') ->withHeader('content-disposition', 'attachment; filename=shifts.ics') ->withView('api/ical', ['shiftEntries' => $shifts]); @@ -71,45 +73,54 @@ public function shifts(): Response // ! All attributes not defined in $data might change at any time ! $data = [ // Name of the shift (type) - 'name' => $shift->shiftType->name, + /** @deprecated, use shifttype_name instead */ + 'name' => (string) $shift->shiftType->name, // Shift / Talk title - 'title' => $shift->title, - // Shift description - 'description' => $shift->description, + 'title' => (string) $shift->title, + // Shift description, should be shown after shifttype_description, markdown formatted + 'description' => (string) $shift->description, - // Users comment - 'Comment' => $entry->user_comment, + 'link' => (string) $this->url->to('/shifts', ['action' => 'view', 'shift_id' => $shift->id]), + + // Users comment, might be empty + 'Comment' => (string) $entry->user_comment, // Shift id - 'SID' => $shift->id, - // Shift type id - 'shifttype_id' => $shift->shiftType->id, - // Talk URL - 'URL' => $shift->url, - - // Room id - 'RID' => $shift->room->id, - // Room name - 'Name' => $shift->room->name, - // Location map url - 'map_url' => $shift->room->map_url, + 'SID' => (int) $shift->id, + + // Shift type + 'shifttype_id' => (int) $shift->shiftType->id, + // General type of the task + 'shifttype_name' => (string) $shift->shiftType->name, + // General description, markdown formatted, might be empty + 'shifttype_description' => (string) $shift->shiftType->description, + + // Talk URL, mostly empty + 'URL' => (string) $shift->url, + + // Location (room) id + 'RID' => (int) $shift->location->id, + // Location (room) name + 'Name' => (string) $shift->location->name, + // Location map url, can be empty + 'map_url' => (string) $shift->location->map_url, // Start timestamp /** @deprecated start_date should be used */ - 'start' => $shift->start->timestamp, + 'start' => (int) $shift->start->timestamp, // Start date - 'start_date' => $shift->start->toRfc3339String(), + 'start_date' => (string) $shift->start->toRfc3339String(), // End timestamp /** @deprecated end_date should be used */ - 'end' => $shift->end->timestamp, + 'end' => (int) $shift->end->timestamp, // End date - 'end_date' => $shift->end->toRfc3339String(), + 'end_date' => (string) $shift->end->toRfc3339String(), // Timezone offset like "+01:00" /** @deprecated should be retrieved from start_date or end_date */ - 'timezone' => $timeZone->toOffsetName(), + 'timezone' => (string) $timeZone->toOffsetName(), // The events timezone like "Europe/Berlin" - 'event_timezone' => $timeZone->getName(), + 'event_timezone' => (string) $timeZone->getName(), ]; $response[] = [ @@ -121,11 +132,18 @@ public function shifts(): Response ]; } - return $this->response + return $this->withEtag($response) ->withAddedHeader('content-type', 'application/json; charset=utf-8') ->withContent(json_encode($response)); } + protected function withEtag(mixed $value): Response + { + $hash = md5(json_encode($value)); + + return $this->response->setEtag($hash); + } + protected function getNews(): Collection { $news = $this->request->has('meetings') @@ -144,7 +162,7 @@ protected function getShifts(): Collection ->shiftEntries() ->leftJoin('shifts', 'shifts.id', 'shift_entries.shift_id') ->orderBy('shifts.start') - ->with(['shift', 'shift.room', 'shift.shiftType']) - ->get(); + ->with(['shift', 'shift.location', 'shift.shiftType']) + ->get(['*', 'shift_entries.id']); } } diff --git a/src/Controllers/MessagesController.php b/src/Controllers/MessagesController.php index 2aef9b519..7a339e6f2 100644 --- a/src/Controllers/MessagesController.php +++ b/src/Controllers/MessagesController.php @@ -218,7 +218,8 @@ protected function latestMessagePerConversation(User $currentUser): Collection $join->on('messages.id', '=', 'conversations.last_id'); }) ->orderBy('created_at', 'DESC') - ->get(); + ->get() + ->load(['receiver.personalData', 'receiver.state']); } protected function raw(mixed $value): QueryExpression diff --git a/src/Controllers/Metrics/Controller.php b/src/Controllers/Metrics/Controller.php index 78a86fb58..c5cbf7360 100644 --- a/src/Controllers/Metrics/Controller.php +++ b/src/Controllers/Metrics/Controller.php @@ -68,10 +68,16 @@ public function metrics(): Response ], 'users' => [ 'type' => 'gauge', - ['labels' => ['state' => 'incoming'], 'value' => $this->stats->newUsers()], - ['labels' => ['state' => 'arrived', 'working' => 'no'], 'value' => $this->stats->arrivedUsers(false)], - ['labels' => ['state' => 'arrived', 'working' => 'yes'], 'value' => $this->stats->arrivedUsers(true)], + ['labels' => ['state' => 'incoming', 'working' => 'no'], 'value' + => $this->stats->usersState(false, false)], + ['labels' => ['state' => 'incoming', 'working' => 'yes'], 'value' + => $this->stats->usersState(true, false)], + ['labels' => ['state' => 'arrived', 'working' => 'no'], 'value' + => $this->stats->usersState(false)], + ['labels' => ['state' => 'arrived', 'working' => 'yes'], 'value' + => $this->stats->usersState(true)], ], + 'users_info' => ['type' => 'gauge', $this->stats->usersInfo()], 'users_force_active' => ['type' => 'gauge', $this->stats->forceActiveUsers()], 'users_pronouns' => ['type' => 'gauge', $this->stats->usersPronouns()], 'licenses' => [ @@ -83,6 +89,8 @@ public function metrics(): Response ['labels' => ['type' => '3.5t'], 'value' => $this->stats->licenses('3.5t')], ['labels' => ['type' => '7.5t'], 'value' => $this->stats->licenses('7.5t')], ['labels' => ['type' => '12t'], 'value' => $this->stats->licenses('12t')], + ['labels' => ['type' => 'ifsg_light'], 'value' => $this->stats->licenses('ifsg_light')], + ['labels' => ['type' => 'ifsg'], 'value' => $this->stats->licenses('ifsg')], ], 'users_email' => [ 'type' => 'gauge', @@ -130,7 +138,9 @@ public function metrics(): Response ] + $userTshirtSizes, 'locales' => ['type' => 'gauge', 'help' => 'The locales users have configured'] + $userLocales, 'themes' => ['type' => 'gauge', 'help' => 'The themes users have configured'] + $userThemes, - 'rooms' => ['type' => 'gauge', $this->stats->rooms()], + 'locations' => ['type' => 'gauge', $this->stats->locations()], + 'angeltypes' => ['type' => 'gauge', $this->stats->angeltypes()], + 'shifttypes' => ['type' => 'gauge', $this->stats->shifttypes()], 'shifts' => ['type' => 'gauge', $this->stats->shifts()], 'announcements' => [ 'type' => 'gauge', @@ -192,8 +202,8 @@ public function stats(): Response $this->checkAuth(true); $data = [ - 'user_count' => $this->stats->newUsers() + $this->stats->arrivedUsers(), - 'arrived_user_count' => $this->stats->arrivedUsers(), + 'user_count' => $this->stats->usersState() + $this->stats->usersState(null, false), + 'arrived_user_count' => $this->stats->usersState(), 'done_work_hours' => round($this->stats->workSeconds(true) / 60 / 60, 0), 'users_in_action' => $this->stats->currentlyWorkingUsers(), ]; diff --git a/src/Controllers/Metrics/MetricsEngine.php b/src/Controllers/Metrics/MetricsEngine.php index ef2cd319c..4ce0ad462 100644 --- a/src/Controllers/Metrics/MetricsEngine.php +++ b/src/Controllers/Metrics/MetricsEngine.php @@ -60,7 +60,7 @@ public function get(string $path, array $data = []): string } /** - * @return array[] + * @return string[] */ protected function formatHistogram(array $row, string $name): array { diff --git a/src/Controllers/Metrics/Stats.php b/src/Controllers/Metrics/Stats.php index 9e4fbbb69..1f4f99368 100644 --- a/src/Controllers/Metrics/Stats.php +++ b/src/Controllers/Metrics/Stats.php @@ -6,6 +6,7 @@ use Carbon\Carbon; use Engelsystem\Database\Database; +use Engelsystem\Models\AngelType; use Engelsystem\Models\EventConfig; use Engelsystem\Models\Faq; use Engelsystem\Models\LogEntry; @@ -14,8 +15,9 @@ use Engelsystem\Models\NewsComment; use Engelsystem\Models\OAuth; use Engelsystem\Models\Question; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\Shift; +use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\User\License; use Engelsystem\Models\User\PasswordReset; use Engelsystem\Models\User\PersonalData; @@ -36,13 +38,13 @@ public function __construct(protected Database $db) } /** - * The number of not arrived users + * The number of users that arrived/not arrived and/or did some work * * @param bool|null $working */ - public function arrivedUsers(bool $working = null): int + public function usersState(bool $working = null, bool $arrived = true): int { - $query = State::whereArrived(true); + $query = State::whereArrived($arrived); if (!is_null($working)) { $query @@ -69,12 +71,12 @@ public function arrivedUsers(bool $working = null): int return $query->count('users_state.user_id'); } - /** - * The number of not arrived users - */ - public function newUsers(): int + public function usersInfo(): int { - return State::whereArrived(false)->count(); + return State::query() + ->whereNotNull('user_info') + ->whereNot('user_info', '') + ->count(); } public function forceActiveUsers(): int @@ -174,23 +176,22 @@ public function themes(): Collection ->get(); } - /** - * @param string|null $vehicle - */ - public function licenses(string $vehicle): int + public function licenses(string $license): int { $mapping = [ - 'has_car' => 'has_car', - 'forklift' => 'drive_forklift', - 'car' => 'drive_car', - '3.5t' => 'drive_3_5t', - '7.5t' => 'drive_7_5t', - '12t' => 'drive_12t', + 'has_car' => 'has_car', + 'forklift' => 'drive_forklift', + 'car' => 'drive_car', + '3.5t' => 'drive_3_5t', + '7.5t' => 'drive_7_5t', + '12t' => 'drive_12t', + 'ifsg_light' => 'ifsg_certificate_light', + 'ifsg' => 'ifsg_certificate', ]; $query = (new License()) ->getQuery() - ->where($mapping[$vehicle], true); + ->where($mapping[$license], true); return $query->count(); } @@ -292,9 +293,21 @@ public function worklogBuckets(array $buckets): array ); } - public function rooms(): int + public function locations(): int + { + return Location::query() + ->count(); + } + + public function shifttypes(): int + { + return ShiftType::query() + ->count(); + } + + public function angeltypes(): int { - return Room::query() + return AngelType::query() ->count(); } diff --git a/src/Controllers/NewsController.php b/src/Controllers/NewsController.php index e8e9d3111..8c001e4a4 100644 --- a/src/Controllers/NewsController.php +++ b/src/Controllers/NewsController.php @@ -53,8 +53,7 @@ public function show(Request $request): Response $newsId = (int) $request->getAttribute('news_id'); $news = $this->news - ->with('user') - ->with('comments') + ->with(['user', 'comments.user.state', 'comments.user.personalData']) ->findOrFail($newsId); return $this->renderView('pages/news/news.twig', ['news' => $news]); @@ -130,24 +129,27 @@ protected function showOverview(bool $onlyMeetings = false): Response $query = $query->where('is_meeting', true); } + $count = $query->count(); + $pagesCount = max(1, ceil($count / $perPage)); + $page = max(1, min($page, $pagesCount)); + $news = $query ->with('user') ->withCount('comments') ->orderByDesc('is_pinned') - ->orderByDesc('is_important') + ->orderByDesc('is_highlighted') ->orderByDesc('updated_at') ->orderByDesc('id') ->limit($perPage) ->offset(($page - 1) * $perPage) ->get(); - $pagesCount = ceil($query->count() / $perPage); return $this->renderView( 'pages/news/overview.twig', [ 'news' => $news, - 'pages' => max(1, $pagesCount), - 'page' => max(1, min($page, $pagesCount)), + 'pages' => $pagesCount, + 'page' => $page, 'only_meetings' => $onlyMeetings, 'is_overview' => true, ] diff --git a/src/Controllers/OAuthController.php b/src/Controllers/OAuthController.php index 7bf4200d9..d606e4b77 100644 --- a/src/Controllers/OAuthController.php +++ b/src/Controllers/OAuthController.php @@ -52,10 +52,13 @@ public function index(Request $request): Response } if (!$request->has('code')) { - $authorizationUrl = $provider->getAuthorizationUrl([ - 'scope' => 'openid email profile' - ]); - + $authorizationUrl = $provider->getAuthorizationUrl( + [ + // League oauth separates scopes by comma, which is wrong, so we do it + // here properly by spaces. See https://www.rfc-editor.org/rfc/rfc6749#section-3.3 + 'scope' => join(' ', $config['scope'] ?? []), + ] + ); $this->session->set('oauth2_state', $provider->getState()); return $this->redirect->to($authorizationUrl); @@ -98,7 +101,7 @@ public function index(Request $request): Response ->where('provider', $providerName) ->where('identifier', $resourceId) ->get() - // Explicit case sensitive comparison using PHP as some DBMS collations are case sensitive and some arent + // Explicit case-sensitive comparison using PHP as some DBMS collations are case-sensitive and some aren't ->where('identifier', '===', (string) $resourceId) ->first(); @@ -147,7 +150,7 @@ public function index(Request $request): Response if (!$oauth) { return $this->redirectRegister( $providerName, - $resourceId, + (string) $resourceId, $accessToken, $config, $userdata @@ -304,15 +307,11 @@ protected function redirectRegister( throw new HttpNotFound('oauth.not-found'); } - $this->session->set( - 'form_data', - [ - 'name' => $userdata->get($config['username']), - 'email' => $userdata->get($config['email']), - 'first_name' => $userdata->get($config['first_name']), - 'last_name' => $userdata->get($config['last_name']), - ], - ); + $this->session->set('form-data-username', $userdata->get($config['username'])); + $this->session->set('form-data-email', $userdata->get($config['email'])); + $this->session->set('form-data-first_name', $userdata->get($config['first_name'])); + $this->session->set('form-data-last_name', $userdata->get($config['last_name'])); + $this->session->set('oauth2_groups', $userdata->get($config['groups'], [])); $this->session->set('oauth2_connect_provider', $providerName); $this->session->set('oauth2_user_id', $providerUserIdentifier); diff --git a/src/Controllers/PasswordResetController.php b/src/Controllers/PasswordResetController.php index 694d4e0b3..70a53d00a 100644 --- a/src/Controllers/PasswordResetController.php +++ b/src/Controllers/PasswordResetController.php @@ -96,6 +96,8 @@ public function postResetPassword(Request $request): Response auth()->setPassword($reset->user, $data['password']); $reset->delete(); + $reset->user->sessions()->getQuery()->delete(); + return $this->showView('pages/password/reset-success', ['type' => 'reset']); } diff --git a/src/Controllers/QuestionsController.php b/src/Controllers/QuestionsController.php index 85b284897..f336686d6 100644 --- a/src/Controllers/QuestionsController.php +++ b/src/Controllers/QuestionsController.php @@ -36,7 +36,8 @@ public function index(): Response ->whereUserId($this->auth->user()->id) ->orderByDesc('answered_at') ->orderBy('created_at') - ->get(); + ->get() + ->load(['user.state', 'answerer.state']); return $this->response->withView( 'pages/questions/overview.twig', diff --git a/src/Controllers/RegistrationController.php b/src/Controllers/RegistrationController.php new file mode 100644 index 000000000..7c0df8b76 --- /dev/null +++ b/src/Controllers/RegistrationController.php @@ -0,0 +1,198 @@ +determineRegistrationDisabled()) { + return $this->notifySignUpDisabledAndRedirectToHome(); + } + + return $this->renderSignUpPage(); + } + + public function save(Request $request): Response + { + if ($this->determineRegistrationDisabled()) { + return $this->notifySignUpDisabledAndRedirectToHome(); + } + + $rawData = $request->getParsedBody(); + $user = $this->userFactory->createFromData($rawData); + + $this->addNotification('registration.successful'); + + if ($this->config->get('welcome_msg')) { + // Set a session marker to display the welcome message on the next page + $this->session->set('show_welcome', true); + } + + if ($user->oauth?->count() > 0) { + // User has OAuth configured. Log in directly. + $provider = $user->oauth->first(); + return $this->redirect->to('/oauth/' . $provider->provider); + } + + if ($this->auth->user()) { + // User is already logged in - that means a supporter has registered an angel. Return to register page. + return $this->redirect->to('/register'); + } + + return $this->redirect->to('/'); + } + + private function notifySignUpDisabledAndRedirectToHome(): Response + { + $this->addNotification('registration.disabled', NotificationType::INFORMATION); + return $this->redirect->to('/'); + } + + private function renderSignUpPage(): Response + { + $goodieType = GoodieType::from($this->config->get('goodie_type')); + $preselectedAngelTypes = $this->determinePreselectedAngelTypes(); + $requiredFields = $this->config->get('required_user_fields'); + + // form-data-register-submit is a marker, that the form was submitted. + // It will be used for instance to use the default angel types or the user selected ones. + // Clear it before render to reset the marker state. + $this->session->remove('form-data-register-submit'); + + return $this->response->withView( + 'pages/registration', + [ + 'minPasswordLength' => $this->config->get('min_password_length'), + 'tShirtSizes' => $this->config->get('tshirt_sizes'), + 'angelTypes' => AngelType::whereHideRegister(false)->get(), + 'preselectedAngelTypes' => $preselectedAngelTypes, + 'buildUpStartDate' => $this->userFactory->determineBuildUpStartDate()->format('Y-m-d'), + 'tearDownEndDate' => $this->config->get('teardown_end')?->format('Y-m-d'), + 'isPasswordEnabled' => $this->userFactory->determineIsPasswordEnabled(), + 'isDECTEnabled' => $this->config->get('enable_dect'), + 'isShowMobileEnabled' => $this->config->get('enable_mobile_show'), + 'isGoodieEnabled' => $goodieType !== GoodieType::None, + 'isGoodieTShirt' => $goodieType === GoodieType::Tshirt, + 'isPronounEnabled' => $this->config->get('enable_pronoun'), + 'isFullNameEnabled' => $this->config->get('enable_user_name'), + 'isPlannedArrivalDateEnabled' => $this->config->get('enable_planned_arrival'), + 'isPronounRequired' => $requiredFields['pronoun'], + 'isFirstnameRequired' => $requiredFields['firstname'], + 'isLastnameRequired' => $requiredFields['lastname'], + 'isTShirtSizeRequired' => $requiredFields['tshirt_size'], + 'isMobileRequired' => $requiredFields['mobile'], + 'isDectRequired' => $requiredFields['dect'], + ], + ); + } + + /** + * @return Array Checkbox field name/Id β†’ 1 + */ + private function determinePreselectedAngelTypes(): array + { + if ($this->session->has('form-data-register-submit')) { + // form-data-register-submit means a user just submitted the page. + // Preselect the angel types from the persisted session form data. + return $this->loadAngelTypesFromSessionFormData(); + } + + $preselectedAngelTypes = []; + + if ($this->session->has('oauth2_connect_provider')) { + $preselectedAngelTypes = $this->loadAngelTypesFromSessionOAuthGroups(); + } + + foreach (AngelType::whereRestricted(false)->whereHideRegister(false)->get() as $angelType) { + // preselect every angel type without restriction + $preselectedAngelTypes['angel_types_' . $angelType->id] = 1; + } + + return $preselectedAngelTypes; + } + + /** + * @return Array + */ + private function loadAngelTypesFromSessionOAuthGroups(): array + { + $oAuthAngelTypes = []; + $ssoTeams = $this->oAuth->getSsoTeams($this->session->get('oauth2_connect_provider')); + $oAuth2Groups = $this->session->get('oauth2_groups'); + + foreach ($ssoTeams as $name => $team) { + if (in_array($name, $oAuth2Groups)) { + // preselect angel type from oauth + $oAuthAngelTypes['angel_types_' . $team['id']] = 1; + } + } + + return $oAuthAngelTypes; + } + + /** + * @return Array + */ + private function loadAngelTypesFromSessionFormData(): array + { + $angelTypes = AngelType::whereHideRegister(false)->get(); + $selectedAngelTypes = []; + + foreach ($angelTypes as $angelType) { + $sessionKey = 'form-data-angel_types_' . $angelType->id; + + if ($this->session->has($sessionKey)) { + $selectedAngelTypes['angel_types_' . $angelType->id] = 1; + // remove from session so that it doesn't stay there forever + $this->session->remove($sessionKey); + } + } + + return $selectedAngelTypes; + } + + private function determineRegistrationDisabled(): bool + { + $authUser = $this->auth->user(); + $isOAuth = $this->session->get('oauth2_connect_provider'); + $isPasswordEnabled = $this->userFactory->determineIsPasswordEnabled(); + + return !auth()->can('register') // No registration permission + // Not authenticated and + // Registration disabled + || ( + !$authUser + && !$this->config->get('registration_enabled') + && !$this->session->get('oauth2_allow_registration') + ) + // Password disabled and not oauth + || (!$authUser && !$isPasswordEnabled && !$isOAuth); + } +} diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 6e1ec4e7b..a6eb93c4c 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -11,6 +11,8 @@ use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Models\AngelType; +use Engelsystem\Models\User\User; use Psr\Log\LoggerInterface; class SettingsController extends BaseController @@ -21,6 +23,8 @@ class SettingsController extends BaseController /** @var string[] */ protected array $permissions = [ 'user_settings', + 'api' => 'api', + 'apiKeyReset' => 'api', ]; public function __construct( @@ -35,6 +39,7 @@ public function __construct( public function profile(): Response { $user = $this->auth->user(); + $requiredFields = $this->config->get('required_user_fields'); return $this->response->withView( 'pages/settings/profile', @@ -43,6 +48,12 @@ public function profile(): Response 'user' => $user, 'goodie_tshirt' => $this->config->get('goodie_type') === GoodieType::Tshirt->value, 'goodie_enabled' => $this->config->get('goodie_type') !== GoodieType::None->value, + 'isPronounRequired' => $requiredFields['pronoun'], + 'isFirstnameRequired' => $requiredFields['firstname'], + 'isLastnameRequired' => $requiredFields['lastname'], + 'isTShirtSizeRequired' => $requiredFields['tshirt_size'], + 'isMobileRequired' => $requiredFields['mobile'], + 'isDectRequired' => $requiredFields['dect'], ] ); } @@ -50,7 +61,7 @@ public function profile(): Response public function saveProfile(Request $request): Response { $user = $this->auth->user(); - $data = $this->validate($request, $this->getSaveProfileRules()); + $data = $this->validate($request, $this->getSaveProfileRules($user)); $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; $goodie_tshirt = $goodie === GoodieType::Tshirt; @@ -99,7 +110,8 @@ public function saveProfile(Request $request): Response if ( $goodie_tshirt - && isset(config('tshirt_sizes')[$data['shirt_size']]) + && isset(config('tshirt_sizes')[$data['shirt_size'] ?? '']) + && !$user->state->got_shirt ) { $user->personalData->shirt_size = $data['shirt_size']; } @@ -109,7 +121,7 @@ public function saveProfile(Request $request): Response $user->settings->save(); $user->save(); - $this->addNotification('settings.profile.success'); + $this->addNotification('settings.success'); return $this->redirect->to('/settings/profile'); } @@ -145,6 +157,11 @@ public function savePassword(Request $request): Response $this->addNotification('settings.password.success'); $this->log->info('User set new password.'); + + $user->sessions() + ->getQuery() + ->where('id', '!=', session()->getId()) + ->delete(); } return $this->redirect->to('/settings/password'); @@ -222,6 +239,94 @@ public function saveLanguage(Request $request): Response return $this->redirect->to('/settings/language'); } + public function certificate(): Response + { + $user = $this->auth->user(); + + if (!config('ifsg_enabled') && !$this->checkDrivingLicense()) { + throw new HttpNotFound(); + } + + return $this->response->withView( + 'pages/settings/certificates', + [ + 'settings_menu' => $this->settingsMenu(), + 'driving_license' => $this->checkDrivingLicense(), + 'certificates' => $user->license, + ] + ); + } + + public function saveIfsgCertificate(Request $request): Response + { + if (!config('ifsg_enabled')) { + throw new HttpNotFound(); + } + + $user = $this->auth->user(); + $data = $this->validate($request, [ + 'ifsg_certificate_light' => 'optional|checked', + 'ifsg_certificate' => 'optional|checked', + ]); + + if (config('ifsg_light_enabled')) { + $user->license->ifsg_certificate_light = !$data['ifsg_certificate'] && $data['ifsg_certificate_light']; + } + $user->license->ifsg_certificate = (bool) $data['ifsg_certificate']; + $user->license->save(); + + $this->addNotification('settings.certificates.success'); + + return $this->redirect->to('/settings/certificates'); + } + + public function saveDrivingLicense(Request $request): Response + { + if (!$this->checkDrivingLicense()) { + throw new HttpNotFound(); + } + + $user = $this->auth->user(); + $data = $this->validate($request, [ + 'has_car' => 'optional|checked', + 'drive_car' => 'optional|checked', + 'drive_3_5t' => 'optional|checked', + 'drive_7_5t' => 'optional|checked', + 'drive_12t' => 'optional|checked', + 'drive_forklift' => 'optional|checked', + ]); + + $user->license->has_car = (bool) $data['has_car']; + $user->license->drive_car = (bool) $data['drive_car']; + $user->license->drive_3_5t = (bool) $data['drive_3_5t']; + $user->license->drive_7_5t = (bool) $data['drive_7_5t']; + $user->license->drive_12t = (bool) $data['drive_12t']; + $user->license->drive_forklift = (bool) $data['drive_forklift']; + $user->license->save(); + + $this->addNotification('settings.certificates.success'); + + return $this->redirect->to('/settings/certificates'); + } + + public function api(): Response + { + return $this->response->withView( + 'pages/settings/api', + [ + 'settings_menu' => $this->settingsMenu(), + ], + ); + } + + public function apiKeyReset(): Response + { + $this->auth->resetApiKey($this->auth->user()); + + $this->addNotification('settings.api.key_reset_success'); + return $this->redirect->back(); + } + public function oauth(): Response { $providers = $this->config->get('oauth'); @@ -238,25 +343,71 @@ public function oauth(): Response ); } + public function sessions(): Response + { + $sessions = $this->auth->user()->sessions->sortByDesc('last_activity'); + + return $this->response->withView( + 'pages/settings/sessions', + [ + 'settings_menu' => $this->settingsMenu(), + 'sessions' => $sessions, + 'current_session' => session()->getId(), + ], + ); + } + + public function sessionsDelete(Request $request): Response + { + $id = $request->postData('id'); + $query = $this->auth->user() + ->sessions() + ->getQuery() + ->where('id', '!=', session()->getId()); + + if ($id != 'all') { + $this->validate($request, [ + 'id' => 'required|alnum|length:15:15', + ]); + $query = $query->where('id', 'LIKE', $id . '%'); + } + + $query->delete(); + $this->addNotification('settings.sessions.delete_success'); + + return $this->redirect->to('/settings/sessions'); + } + public function settingsMenu(): array { $menu = [ + url('/users', ['action' => 'view']) => ['title' => 'profile.my-shifts', 'icon' => 'chevron-left'], url('/settings/profile') => 'settings.profile', - url('/settings/password') => 'settings.password', + url('/settings/password') => ['title' => 'settings.password', 'icon' => 'key-fill'], ]; if (count(config('locales')) > 1) { - $menu[url('/settings/language')] = 'settings.language'; + $menu[url('/settings/language')] = ['title' => 'settings.language', 'icon' => 'translate']; } if (count(config('themes')) > 1) { $menu[url('/settings/theme')] = 'settings.theme'; } + if (config('ifsg_enabled') || $this->checkDrivingLicense()) { + $menu[url('/settings/certificates')] = ['title' => 'settings.certificates', 'icon' => 'card-checklist']; + } + + $menu[url('/settings/sessions')] = 'settings.sessions'; + if (!empty(config('oauth'))) { $menu[url('/settings/oauth')] = ['title' => 'settings.oauth', 'hidden' => $this->checkOauthHidden()]; } + if ($this->auth->can('api')) { + $menu[url('/settings/api')] = ['title' => 'settings.api', 'icon' => 'braces']; + } + return $menu; } @@ -271,18 +422,32 @@ protected function checkOauthHidden(): bool return true; } + protected function checkDrivingLicense(): bool + { + return $this->auth->user()->userAngelTypes->filter(function (AngelType $angelType) { + return $angelType->requires_driver_license; + })->isNotEmpty(); + } + + private function isRequired(string $key): string + { + $requiredFields = $this->config->get('required_user_fields'); + return $requiredFields[$key] ? 'required' : 'optional'; + } + /** * @return string[] */ - private function getSaveProfileRules(): array + private function getSaveProfileRules(User $user): array { $goodie_tshirt = $this->config->get('goodie_type') === GoodieType::Tshirt->value; $rules = [ - 'pronoun' => 'optional|max:15', - 'first_name' => 'optional|max:64', - 'last_name' => 'optional|max:64', - 'dect' => 'optional|length:0:40', // dect/mobile can be purely numbers. "max" would have - 'mobile' => 'optional|length:0:40', // checked their values, not their character length. + 'pronoun' => $this->isRequired('pronoun') . '|max:15', + 'first_name' => $this->isRequired('firstname') . '|max:64', + 'last_name' => $this->isRequired('lastname') . '|max:64', + 'dect' => $this->isRequired('dect') . '|length:0:40', + // dect/mobile can be purely numbers. "max" would have checked their values, not their character length. + 'mobile' => $this->isRequired('mobile') . '|length:0:40', 'mobile_show' => 'optional|checked', 'email' => 'required|email|max:254', 'email_shiftinfo' => 'optional|checked', @@ -295,8 +460,8 @@ private function getSaveProfileRules(): array $rules['planned_arrival_date'] = 'required|date:Y-m-d'; $rules['planned_departure_date'] = 'optional|date:Y-m-d'; } - if ($goodie_tshirt) { - $rules['shirt_size'] = 'required'; + if ($goodie_tshirt && !$user->state->got_shirt) { + $rules['shirt_size'] = $this->isRequired('tshirt_size') . '|shirt_size'; } return $rules; } diff --git a/src/Controllers/ShiftsController.php b/src/Controllers/ShiftsController.php new file mode 100644 index 000000000..240c039ac --- /dev/null +++ b/src/Controllers/ShiftsController.php @@ -0,0 +1,141 @@ +auth->user(); + $nextFreeShifts = $this->getNextFreeShifts($user); + + if ($nextFreeShifts->isEmpty()) { + $this->addNotification('notification.shift.no_next_found', NotificationType::WARNING); + return $this->redirect->to($this->url->to('/shifts')); + } + + /** @var Shift $randomShift */ + $randomShift = $nextFreeShifts + ->collect() + // Prefer soon starting shifts + ->groupBy('start') + ->first() + // Select one at random + ->random(); + + return $this->redirect->to($this->url->to('/shifts', ['action' => 'view', 'shift_id' => $randomShift->id])); + } + + protected function getNextFreeShifts(User $user): Collection | DbCollection + { + $angelTypes = $user + ->userAngelTypes() + ->select('angel_types.id') + ->whereNested(function (Builder $query): void { + $query + ->where('angel_types.restricted', false) + ->orWhereNot('confirm_user_id', false); + }) + ->pluck('id'); + /** @var ShiftEntry[]|DbCollection $shiftEntries */ + $shiftEntries = $user->shiftEntries()->with('shift')->get(); + + $freeShifts = Shift::query() + ->select('shifts.*') + // Load needed from shift if no schedule configured, else from room + ->leftJoin('schedule_shift', 'schedule_shift.shift_id', 'shifts.id') + ->leftJoin('schedules', 'schedules.id', 'schedule_shift.schedule_id') + + // From shift + ->leftJoin('needed_angel_types', function (JoinClause $query): void { + $query->on('needed_angel_types.shift_id', 'shifts.id') + ->whereNull('schedule_shift.shift_id'); + }) + // Via schedule shift type + ->leftJoin('needed_angel_types AS nast', function (JoinClause $query): void { + $query->on('nast.shift_type_id', 'shifts.shift_type_id') + ->whereNotNull('schedule_shift.shift_id') + ->where('schedules.needed_from_shift_type', true); + }) + // Via schedule location + ->leftJoin('needed_angel_types AS nas', function (JoinClause $query): void { + $query->on('nas.location_id', 'shifts.location_id') + ->whereNotNull('schedule_shift.shift_id') + ->where('schedules.needed_from_shift_type', false); + }) + + // Not already signed in + ->whereNotIn('shifts.id', $shiftEntries->pluck('shift_id')) + // Same angel types + ->where(function (EloquentBuilder $query) use ($angelTypes): void { + $query + ->whereIn('needed_angel_types.angel_type_id', $angelTypes) + ->orWhereIn('nast.angel_type_id', $angelTypes) + ->orWhereIn('nas.angel_type_id', $angelTypes); + }) + // Starts soon + ->where('shifts.start', '>', Carbon::now()) + // Where help needed + ->where(function (Builder $query): void { + $query + ->from('shift_entries') + ->selectRaw('COUNT(*)') + ->where(fn(Builder $query) => $this->queryShiftEntries($query)); + }, '<', Shift::query()->raw('COALESCE(needed_angel_types.count, nast.count, nas.count)')) + ->limit(10) + ->orderBy('start'); + + foreach ($shiftEntries as $entry) { + $freeShifts->where(function (QueryBuilder $query) use ($entry): void { + $query->where('end', '<=', $entry->shift->start); + $query->orWhere('start', '>=', $entry->shift->end); + }); + } + + return $freeShifts->get(); + } + + protected function queryShiftEntries(Builder $query): void + { + $query->select('id') + ->from('shift_entries') + ->where('shift_entries.shift_id', $query->raw('shifts.id')) + ->where(function (Builder $query): void { + $query->where('shift_entries.angel_type_id', $query->raw('needed_angel_types.angel_type_id')) + ->orWhere('shift_entries.angel_type_id', $query->raw('nas.angel_type_id')) + ->orWhere('shift_entries.angel_type_id', $query->raw('nast.angel_type_id')); + }) + ->groupBy(['shift_entries.shift_id', 'shift_entries.angel_type_id']); + } +} diff --git a/src/Events/Listener/Messages.php b/src/Events/Listener/Messages.php index 528a601fb..4d93e6103 100644 --- a/src/Events/Listener/Messages.php +++ b/src/Events/Listener/Messages.php @@ -8,7 +8,6 @@ use Engelsystem\Models\Message; use Engelsystem\Models\User\User; use Psr\Log\LoggerInterface; -use Symfony\Component\Mailer\Exception\TransportException; class Messages { @@ -29,22 +28,15 @@ public function created(Message $message): void private function sendMail(Message $message, User $user, string $subject, string $template): void { - try { - $this->mailer->sendViewTranslated( - $user, - $subject, - $template, - [ - 'sender' => $message->sender->displayName, - 'send_message' => $message, - 'username' => $user->displayName, - ] - ); - } catch (TransportException $e) { - $this->log->error( - 'Unable to send email "{title}" to user {user} with {exception}', - ['title' => $subject, 'user' => $user->name, 'exception' => $e] - ); - } + $this->mailer->sendViewTranslated( + $user, + $subject, + $template, + [ + 'sender' => $message->sender->displayName, + 'send_message' => $message, + 'username' => $user->displayName, + ] + ); } } diff --git a/src/Events/Listener/News.php b/src/Events/Listener/News.php index efc495ca6..72836f358 100644 --- a/src/Events/Listener/News.php +++ b/src/Events/Listener/News.php @@ -7,10 +7,8 @@ use Engelsystem\Mail\EngelsystemMailer; use Engelsystem\Models\News as NewsModel; use Engelsystem\Models\User\Settings as UserSettings; -use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Collection; use Psr\Log\LoggerInterface; -use Symfony\Component\Mailer\Exception\TransportException; class News { @@ -21,32 +19,34 @@ public function __construct( ) { } - public function created(NewsModel $news): void + public function created(NewsModel $news, bool $sendNotification = true): void { + $this->sendMail($news, 'notification.news.new', 'emails/news-new', $sendNotification); + } + + public function updated(NewsModel $news, bool $sendNotification = true): void + { + $this->sendMail($news, 'notification.news.updated', 'emails/news-updated', $sendNotification); + } + + protected function sendMail(NewsModel $news, string $subject, string $template, bool $sendNotification = true): void + { + if (!$sendNotification) { + return; + } + /** @var UserSettings[]|Collection $recipients */ $recipients = $this->settings - ->whereEmailNews(true) - ->with('user') + ->with('user.personalData') + ->where('email_news', true) ->get(); foreach ($recipients as $recipient) { - $this->sendMail($news, $recipient->user, 'notification.news.new', 'emails/news-new'); - } - } - - protected function sendMail(NewsModel $news, User $user, string $subject, string $template): void - { - try { $this->mailer->sendViewTranslated( - $user, + $recipient->user, $subject, $template, - ['title' => $news->title, 'news' => $news, 'username' => $user->displayName] - ); - } catch (TransportException $e) { - $this->log->error( - 'Unable to send email "{title}" to user {user} with {exception}', - ['title' => $subject, 'user' => $user->name, 'exception' => $e] + ['title' => $news->title, 'news' => $news, 'username' => $recipient->user->displayName] ); } } diff --git a/src/Exceptions/Handlers/Legacy.php b/src/Exceptions/Handlers/Legacy.php index 49ea37aff..dbd830c0e 100644 --- a/src/Exceptions/Handlers/Legacy.php +++ b/src/Exceptions/Handlers/Legacy.php @@ -14,6 +14,10 @@ class Legacy implements HandlerInterface public function render(Request $request, Throwable $e): void { + if ($this->isCli()) { + return; + } + echo 'An unexpected error occurred. A team of untrained monkeys has been dispatched to fix it.'; } @@ -27,7 +31,7 @@ public function report(Throwable $e): void $this->stripBasePath($e->getFile()), $e->getLine(), $previous ? $previous->getMessage() : 'None', - json_encode($e->getTrace(), PHP_SAPI == 'cli' ? JSON_PRETTY_PRINT : 0) + json_encode($e->getTrace()) )); if (is_null($this->log)) { @@ -50,4 +54,13 @@ protected function stripBasePath(string $path): string $basePath = realpath(__DIR__ . '/../../..') . '/'; return str_replace($basePath, '', $path); } + + /** + * Test if is called from cli + * @codeCoverageIgnore + */ + protected function isCli(): bool + { + return PHP_SAPI == 'cli' || PHP_SAPI == 'phpdbg'; + } } diff --git a/src/Exceptions/Handlers/Whoops.php b/src/Exceptions/Handlers/Whoops.php index f5d4d9a2a..1d3365e57 100644 --- a/src/Exceptions/Handlers/Whoops.php +++ b/src/Exceptions/Handlers/Whoops.php @@ -5,7 +5,6 @@ namespace Engelsystem\Exceptions\Handlers; use Engelsystem\Application; -use Engelsystem\Container\Container; use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Request; use Throwable; @@ -15,14 +14,11 @@ class Whoops extends Legacy implements HandlerInterface { - protected Application $app; - /** * Whoops constructor. */ - public function __construct(Container $app) + public function __construct(protected Application $app) { - $this->app = $app; } public function render(Request $request, Throwable $e): void diff --git a/src/Factories/User.php b/src/Factories/User.php new file mode 100644 index 000000000..b0346473c --- /dev/null +++ b/src/Factories/User.php @@ -0,0 +1,346 @@ + $rawData Raw data from which a user should be created + * @return EngelsystemUser The user if successful + * @throws + */ + public function createFromData(array $rawData): EngelsystemUser + { + $data = $this->validateUser($rawData); + return $this->createUser($data, $rawData); + } + + public function determineIsPasswordEnabled(): bool + { + $isPasswordEnabled = $this->config->get('enable_password'); + $oAuthEnablePassword = $this->session->get('oauth2_enable_password'); + + if (!is_null($oAuthEnablePassword)) { + // o-auth overwrites config + $isPasswordEnabled = $oAuthEnablePassword; + } + + return $isPasswordEnabled; + } + + public function determineBuildUpStartDate(): DateTimeInterface + { + return $this->config->get('buildup_start') ?? CarbonImmutable::now(); + } + + private function isRequired(string $key): string + { + $requiredFields = $this->config->get('required_user_fields'); + return $requiredFields[$key] ? 'required' : 'optional'; + } + + /** + * @param Array $rawData + * @throws ValidationException + */ + private function validateUser(array $rawData): array + { + $validationRules = [ + 'username' => 'required|username', + 'email' => 'required|email', + 'email_shiftinfo' => 'optional|checked', + 'email_by_human_allowed' => 'optional|checked', + 'email_messages' => 'optional|checked', + 'email_news' => 'optional|checked', + 'email_goody' => 'optional|checked', + // Using length here, because min/max would validate dect/mobile as numbers. + 'mobile' => $this->isRequired('mobile') . '|length:0:40', + ]; + + $isPasswordEnabled = $this->determineIsPasswordEnabled(); + + if ($isPasswordEnabled) { + $minPasswordLength = $this->config->get('min_password_length'); + $validationRules['password'] = 'required|length:' . $minPasswordLength; + $validationRules['password_confirmation'] = 'required'; + } + + $isFullNameEnabled = $this->config->get('enable_user_name'); + + if ($isFullNameEnabled) { + $validationRules['firstname'] = $this->isRequired('firstname') . '|length:0:64'; + $validationRules['lastname'] = $this->isRequired('lastname') . '|length:0:64'; + } + + $isPronounEnabled = $this->config->get('enable_pronoun'); + + if ($isPronounEnabled) { + $validationRules['pronoun'] = $this->isRequired('pronoun') . '|max:15'; + } + + $isShowMobileEnabled = $this->config->get('enable_mobile_show'); + + if ($isShowMobileEnabled) { + $validationRules['mobile_show'] = 'optional|checked'; + } + + $isPlannedArrivalDateEnabled = $this->config->get('enable_planned_arrival'); + + if ($isPlannedArrivalDateEnabled) { + $isoBuildUpStartDate = $this->determineBuildUpStartDate(); + /** @var DateTimeInterface|null $tearDownEndDate */ + $tearDownEndDate = $this->config->get('teardown_end'); + + if ($tearDownEndDate) { + $validationRules['planned_arrival_date'] = sprintf( + 'required|date|between:%s:%s', + $isoBuildUpStartDate->format('Y-m-d'), + $tearDownEndDate->format('Y-m-d') + ); + } else { + $validationRules['planned_arrival_date'] = sprintf( + 'required|date|min:%s', + $isoBuildUpStartDate->format('Y-m-d'), + ); + } + } + + $isDECTEnabled = $this->config->get('enable_dect'); + + if ($isDECTEnabled) { + // Using length here, because min/max would validate dect/mobile as numbers. + $validationRules['dect'] = $this->isRequired('dect') . '|length:0:40'; + } + + $goodieType = GoodieType::from($this->config->get('goodie_type')); + $isGoodieTShirt = $goodieType === GoodieType::Tshirt; + + if ($isGoodieTShirt) { + $validationRules['tshirt_size'] = $this->isRequired('tshirt_size') . '|shirt-size'; + } + + $data = $this->validate($rawData, $validationRules); + + // additional validations + $this->validateUniqueUsername($data['username']); + $this->validateUniqueEmail($data['email']); + + if ($isPasswordEnabled) { + // Finally, validate that password matches password_confirmation. + // The respect keyValue validation does not seem to work. + $this->validatePasswordMatchesConfirmation($rawData); + } + + return $data; + } + + /** + * @param Array $rawData + */ + private function validatePasswordMatchesConfirmation(array $rawData): void + { + if ($rawData['password'] !== $rawData['password_confirmation']) { + throw new ValidationException( + (new Validator())->addErrors(['password' => [ + 'settings.password.confirmation-does-not-match', + ]]) + ); + } + } + + private function validateUniqueUsername(string $username): void + { + if (EngelsystemUser::whereName($username)->exists()) { + throw new ValidationException( + (new Validator())->addErrors(['username' => [ + 'settings.profile.nick.already-taken', + ]]) + ); + } + } + + private function validateUniqueEmail(string $email): void + { + if (EngelsystemUser::whereEmail($email)->exists()) { + throw new ValidationException( + (new Validator())->addErrors(['email' => [ + 'settings.profile.email.already-taken', + ]]) + ); + } + } + + /** + * @param Array $data + * @param Array $rawData + */ + private function createUser(array $data, array $rawData): EngelsystemUser + { + $this->dbConnection->beginTransaction(); + + $user = new EngelsystemUser([ + 'name' => $data['username'], + 'password' => '', + 'email' => $data['email'], + 'api_key' => '', + 'last_login_at' => null, + ]); + $user->save(); + + $contact = new Contact([ + 'dect' => $data['dect'] ?? null, + 'mobile' => $data['mobile'], + ]); + $contact->user() + ->associate($user) + ->save(); + + $isPlannedArrivalDateEnabled = $this->config->get('enable_planned_arrival'); + $plannedArrivalDate = null; + + if ($isPlannedArrivalDateEnabled) { + $plannedArrivalDate = Carbon::createFromFormat('Y-m-d', $data['planned_arrival_date']); + } + + $personalData = new PersonalData([ + 'first_name' => $data['firstname'] ?? null, + 'last_name' => $data['lastname'] ?? null, + 'pronoun' => $data['pronoun'] ?? null, + 'shirt_size' => $data['tshirt_size'] ?? null, + 'planned_arrival_date' => $plannedArrivalDate, + ]); + $personalData->user() + ->associate($user) + ->save(); + + $isShowMobileEnabled = $this->config->get('enable_mobile_show'); + + $settings = new Settings([ + 'language' => $this->session->get('locale') ?? 'en_US', + 'theme' => $this->config->get('theme'), + 'email_human' => $data['email_by_human_allowed'] ?? false, + 'email_messages' => $data['email_messages'] ?? false, + 'email_goody' => $data['email_goody'] ?? false, + 'email_shiftinfo' => $data['email_shiftinfo'] ?? false, + 'email_news' => $data['email_news'] ?? false, + 'mobile_show' => $isShowMobileEnabled && $data['mobile_show'], + ]); + $settings->user() + ->associate($user) + ->save(); + + $state = new State([]); + + if ($this->config->get('autoarrive')) { + $state->arrived = true; + $state->arrival_date = CarbonImmutable::now(); + } + + $state->user() + ->associate($user) + ->save(); + + if ($this->session->has('oauth2_connect_provider') && $this->session->has('oauth2_user_id')) { + $oauth = new OAuth([ + 'provider' => $this->session->get('oauth2_connect_provider'), + 'identifier' => $this->session->get('oauth2_user_id'), + 'access_token' => $this->session->get('oauth2_access_token'), + 'refresh_token' => $this->session->get('oauth2_refresh_token'), + 'expires_at' => $this->session->get('oauth2_expires_at'), + ]); + $oauth->user() + ->associate($user) + ->save(); + + $this->session->remove('oauth2_connect_provider'); + $this->session->remove('oauth2_user_id'); + $this->session->remove('oauth2_access_token'); + $this->session->remove('oauth2_refresh_token'); + $this->session->remove('oauth2_expires_at'); + } + + $defaultGroup = Group::find($this->authenticator->getDefaultRole()); + $user->groups()->attach($defaultGroup); + + auth()->resetApiKey($user); + if ($this->determineIsPasswordEnabled() && array_key_exists('password', $data)) { + auth()->setPassword($user, $data['password']); + } + + $assignedAngelTypeNames = $this->assignAngelTypes($user, $rawData); + + $this->logger->info( + 'User {user} signed up as: {angeltypes}', + [ + 'user' => sprintf('%s (%u)', $user->displayName, $user->id), + 'angeltypes' => join(', ', $assignedAngelTypeNames), + ] + ); + + $this->dbConnection->commit(); + + return $user; + } + + private function assignAngelTypes(EngelsystemUser $user, array $rawData): array + { + $possibleAngelTypes = AngelType::whereHideRegister(false)->get(); + $assignedAngelTypeNames = []; + + foreach ($possibleAngelTypes as $possibleAngelType) { + $angelTypeCheckboxId = 'angel_types_' . $possibleAngelType->id; + + if (array_key_exists($angelTypeCheckboxId, $rawData)) { + $user->userAngelTypes()->attach($possibleAngelType); + $assignedAngelTypeNames[] = $possibleAngelType->name; + } + } + + return $assignedAngelTypeNames; + } + + private function validate(array $rawData, array $rules): array + { + $isValid = $this->validator->validate($rawData, $rules); + + if (!$isValid) { + throw new ValidationException($this->validator); + } + + return $this->validator->getData(); + } +} diff --git a/src/Helpers/Authenticator.php b/src/Helpers/Authenticator.php index ada9b7d2c..01b733e58 100644 --- a/src/Helpers/Authenticator.php +++ b/src/Helpers/Authenticator.php @@ -42,7 +42,7 @@ public function user(): ?User } $this->user = $this->userFromSession(); - if (!$this->user && request()->getAttribute('route-api', false)) { + if (!$this->user && $this->isApiRequest()) { $this->user = $this->userFromApi(); } @@ -102,8 +102,10 @@ public function can(array|string $abilities): bool if ($user) { $this->permissions = $user->privileges->pluck('name')->toArray(); - $user->last_login_at = new Carbon(); - $user->save(); + if ($user->last_login_at < Carbon::now()->subMinutes(5) && !$this->isApiRequest()) { + $user->last_login_at = Carbon::now(); + $user->save(['touch' => false]); + } } elseif ($this->session->get('user_id')) { $this->session->remove('user_id'); } @@ -163,7 +165,7 @@ protected function userByHeaders(): ?User { $header = $this->request->getHeader('authorization'); if (!empty($header) && Str::startsWith(Str::lower($header[0]), 'bearer ')) { - return $this->userByApiKey(Str::substr($header[0], 7)); + return $this->userByApiKey(trim(Str::substr($header[0], 7))); } $header = $this->request->getHeader('x-api-key'); @@ -187,6 +189,12 @@ protected function userByQueryParam(): ?User return $this->user; } + public function resetApiKey(User $user): void + { + $user->api_key = bin2hex(random_bytes(32)); + $user->save(); + } + /** * Get the user by its api key */ @@ -200,6 +208,11 @@ protected function userByApiKey(string $key): ?User return $this->user; } + protected function isApiRequest(): bool + { + return (bool) request()->getAttribute('route-api-accessible', false); + } + public function setPassword(User $user, string $password): void { $user->password = password_hash($password, $this->passwordAlgorithm); diff --git a/src/Helpers/BarChart.php b/src/Helpers/BarChart.php index 526d529ad..0177c4999 100644 --- a/src/Helpers/BarChart.php +++ b/src/Helpers/BarChart.php @@ -29,7 +29,7 @@ public static function render( $groupLabels[$groupKey] = $groupKey; if ($date) { - $groupLabels[$groupKey] = $date->format(__('Y-m-d')); + $groupLabels[$groupKey] = $date->format(__('general.date')); } foreach ($rowLabels as $rowKey => $rowName) { @@ -89,7 +89,7 @@ private static function calculateChartGroups( /** * @param int $max Max Y value - * @return array + * @return array */ private static function calculateYLabels(int $max): array { @@ -98,7 +98,7 @@ private static function calculateYLabels(int $max): array for ($y = 0; $y <= $max; $y += $step) { $yLabels[] = [ - 'label' => $y, + 'label' => (string) $y, 'bottom' => $max === 0 ? '0%' : ($y / $max * 100) . '%', ]; } @@ -148,7 +148,7 @@ public static function generateChartDemoData(int $days): array 'count' => $step, 'sum' => $step * $count, ]; - $current = $current->addDay(1); + $current = $current->addDay(); $count++; } diff --git a/src/Helpers/Carbon.php b/src/Helpers/Carbon.php index c12747f50..931a74a88 100644 --- a/src/Helpers/Carbon.php +++ b/src/Helpers/Carbon.php @@ -18,7 +18,7 @@ class Carbon extends \Carbon\Carbon /** * Parses HTML datetime-local and ISO date/time strings. * - * @return \Carbon\Carbon|null Carbon if parseable, else null + * @return self|null Carbon if parseable, else null * @see self::DATETIME_FORMATS */ public static function createFromDatetime(string $value): ?\Carbon\Carbon @@ -43,4 +43,16 @@ public static function createTimestampFromDatetime(string $value): ?int $carbon = self::createFromDateTime($value); return $carbon === null ? null : $carbon->timestamp; } + + /** + * Check if the instance is at the start of an hour. + * + * @param bool $checkMicroseconds check time at microseconds precision + */ + public function isStartOfHour(bool $checkMicroseconds = false): bool + { + return $checkMicroseconds + ? $this->rawFormat('i:s.u') === '00:00.000000' + : $this->rawFormat('i:s') === '00:00'; + } } diff --git a/src/Helpers/DayOfEvent.php b/src/Helpers/DayOfEvent.php new file mode 100644 index 000000000..b989df54f --- /dev/null +++ b/src/Helpers/DayOfEvent.php @@ -0,0 +1,53 @@ +copy()->startOfDay(); + $date = $date ?: Carbon::now(); + + $now = $date->startOfDay(); + $diff = $startOfEvent->diffInDays($now, false); + + if ($diff >= 0) { + // The first day of the event (diff 0) should be 1. + // The seconds day of the event (diff 1) should be 2. + // Add one day to the diff. + return $diff + 1; + } + + if (config('event_has_day0') && $diff < 0) { + // One day before the event (-1 diff) should day 0. + // Two days before the event (-2 diff) should be -1. + // Add one day to the diff. + return $diff + 1; + } + + + // This is the remaining case where the diff is negative (before event). + // One day before the event (-1 diff) should be day -1. + // Two days before the event (-2 diff) should be day -2. + // Return as it is. + return $diff; + } +} diff --git a/src/Helpers/Schedule/CalculatesTime.php b/src/Helpers/Schedule/CalculatesTime.php index 7c4275c36..2c0aa29d6 100644 --- a/src/Helpers/Schedule/CalculatesTime.php +++ b/src/Helpers/Schedule/CalculatesTime.php @@ -12,7 +12,7 @@ protected function secondsFromTime(string $time): int $duration = explode(':', $time); foreach (array_slice($duration, 0, 2) as $key => $times) { - $seconds += [60 * 60, 60][$key] * $times; + $seconds += [60 * 60, 60][$key] * (int) $times; } return $seconds; diff --git a/src/Helpers/Schedule/Event.php b/src/Helpers/Schedule/Event.php index cd5a47de5..2fd566fcf 100644 --- a/src/Helpers/Schedule/Event.php +++ b/src/Helpers/Schedule/Event.php @@ -42,7 +42,7 @@ public function __construct( protected array $persons = [], protected ?string $language = null, protected ?string $description = null, - protected string $recording = '', + protected ?string $recording = '', protected array $links = [], protected array $attachments = [], protected ?string $url = null, diff --git a/src/Helpers/Schedule/XmlParser.php b/src/Helpers/Schedule/XmlParser.php index 0a1bebe00..274198209 100644 --- a/src/Helpers/Schedule/XmlParser.php +++ b/src/Helpers/Schedule/XmlParser.php @@ -15,8 +15,7 @@ class XmlParser public function load(string $xml): bool { - $scheduleXML = simplexml_load_string($xml); - + $scheduleXML = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOWARNING | LIBXML_NOERROR); if (!$scheduleXML) { return false; } diff --git a/src/Http/SessionHandlers/DatabaseHandler.php b/src/Http/SessionHandlers/DatabaseHandler.php index 212bb4d19..5851fa8be 100644 --- a/src/Http/SessionHandlers/DatabaseHandler.php +++ b/src/Http/SessionHandlers/DatabaseHandler.php @@ -5,7 +5,8 @@ namespace Engelsystem\Http\SessionHandlers; use Engelsystem\Database\Database; -use Illuminate\Database\Query\Builder as QueryBuilder; +use Engelsystem\Helpers\Carbon; +use Engelsystem\Models\Session; class DatabaseHandler extends AbstractHandler { @@ -18,9 +19,7 @@ public function __construct(protected Database $database) */ public function read(string $id): string { - $session = $this->getQuery() - ->where('id', '=', $id) - ->first(); + $session = Session::whereId($id)->first(); return $session ? $session->payload : ''; } @@ -30,27 +29,14 @@ public function read(string $id): string */ public function write(string $id, string $data): bool { - $values = [ - 'payload' => $data, - 'last_activity' => $this->getCurrentTimestamp(), - ]; - - $session = $this->getQuery() - ->where('id', '=', $id) - ->first(); - - if (!$session) { - return $this->getQuery() - ->insert($values + [ - 'id' => $id, - ]); - } - - $this->getQuery() - ->where('id', '=', $id) - ->update($values); - - // The update return can't be used directly because it won't change if the second call is in the same second + $session = Session::findOrNew($id); + $session->id = $id; + $session->payload = $data; + $session->last_activity = Carbon::now(); + $session->user_id = auth()->user()?->id; + $session->save(); + + // The save return can't be used directly as it won't change if the second call is in the same second return true; } @@ -59,9 +45,7 @@ public function write(string $id, string $data): bool */ public function destroy(string $id): bool { - $this->getQuery() - ->where('id', '=', $id) - ->delete(); + Session::whereId($id)->delete(); return true; } @@ -71,25 +55,10 @@ public function destroy(string $id): bool */ public function gc(int $max_lifetime): int|false { - $timestamp = $this->getCurrentTimestamp(-$max_lifetime); + $sessionDays = config('session')['lifetime']; + $deleteBefore = Carbon::now()->subDays($sessionDays); - return $this->getQuery() - ->where('last_activity', '<', $timestamp) + return Session::where('last_activity', '<', $deleteBefore) ->delete(); } - - protected function getQuery(): QueryBuilder - { - return $this->database - ->getConnection() - ->table('sessions'); - } - - /** - * Format the SQL timestamp - */ - protected function getCurrentTimestamp(int $diff = 0): string - { - return date('Y-m-d H:i:s', strtotime(sprintf('%+d seconds', $diff))); - } } diff --git a/src/Http/SessionServiceProvider.php b/src/Http/SessionServiceProvider.php index fdac9cb4d..d5ab93b5f 100644 --- a/src/Http/SessionServiceProvider.php +++ b/src/Http/SessionServiceProvider.php @@ -18,7 +18,10 @@ class SessionServiceProvider extends ServiceProvider { public function register(): void { - $sessionStorage = $this->getSessionStorage(); + /** @var Request $request */ + $request = $this->app->get('request'); + + $sessionStorage = $this->getSessionStorage($request); $this->app->instance('session.storage', $sessionStorage); $this->app->bind(SessionStorageInterface::class, 'session.storage'); @@ -31,8 +34,6 @@ public function register(): void $session->set('_token', Str::random(42)); } - /** @var Request $request */ - $request = $this->app->get('request'); $request->setSession($session); $session->start(); @@ -41,7 +42,7 @@ public function register(): void /** * Returns the session storage */ - protected function getSessionStorage(): SessionStorageInterface + protected function getSessionStorage(Request $request): SessionStorageInterface { if ($this->isCli()) { return $this->app->make(MockArraySessionStorage::class); @@ -58,8 +59,9 @@ protected function getSessionStorage(): SessionStorageInterface return $this->app->make(NativeSessionStorage::class, [ 'options' => [ - 'cookie_httponly' => true, 'name' => $sessionConfig['name'], + 'cookie_secure' => $request->isSecure(), + 'cookie_httponly' => true, 'cookie_lifetime' => (int) ($sessionConfig['lifetime'] * 24 * 60 * 60), ], 'handler' => $handler, diff --git a/src/Http/Validation/Rules/ShirtSize.php b/src/Http/Validation/Rules/ShirtSize.php new file mode 100644 index 000000000..325045b5f --- /dev/null +++ b/src/Http/Validation/Rules/ShirtSize.php @@ -0,0 +1,18 @@ +diff($input)->format('%s')) > 1; - } catch (Exception $e) { + return abs($inputDateTime->getTimestamp() - $now->getTimestamp()) > 1 + // Different timezone to prevent interpreting the value as a timezone which happens with H + && $inputDateTime->getTimezone()->getName() != $input; + } catch (Exception) { // Ignore it } diff --git a/src/Http/Validation/Rules/Username.php b/src/Http/Validation/Rules/Username.php new file mode 100644 index 000000000..ff9b7cea8 --- /dev/null +++ b/src/Http/Validation/Rules/Username.php @@ -0,0 +1,28 @@ +validate($input) + && Validator::not(Validator::regex($regex))->validate($input); + } +} diff --git a/src/Logger/Logger.php b/src/Logger/Logger.php index 2b5bcdbe2..f697a2a89 100644 --- a/src/Logger/Logger.php +++ b/src/Logger/Logger.php @@ -44,7 +44,7 @@ public function log(mixed $level, string|Stringable $message, array $context = [ $message .= $this->formatException($context['exception']); } - $this->log->create(['level' => $level, 'message' => $message]); + $this->createEntry(['level' => $level, 'message' => $message]); } /** @@ -81,4 +81,9 @@ protected function checkLevel(string $level): bool { return in_array($level, $this->allowedLevels); } + + protected function createEntry(array $data): void + { + $this->log->create($data); + } } diff --git a/src/Logger/UserAwareLogger.php b/src/Logger/UserAwareLogger.php index 7d40c0d73..dc4f187b1 100644 --- a/src/Logger/UserAwareLogger.php +++ b/src/Logger/UserAwareLogger.php @@ -5,24 +5,21 @@ namespace Engelsystem\Logger; use Engelsystem\Helpers\Authenticator; -use Psr\Log\InvalidArgumentException; -use Stringable; class UserAwareLogger extends Logger { - protected Authenticator $auth; + protected ?Authenticator $auth; /** - * Logs with an arbitrary level and prepends the user - * @throws InvalidArgumentException + * Adds the authenticated user to the log message */ - public function log(mixed $level, string|Stringable $message, array $context = []): void + public function createEntry(array $data): void { if ($this->auth && ($user = $this->auth->user())) { - $message = sprintf('%s (%u): %s', $user->name, $user->id, $message); + $data['user_id'] = $user->id; } - parent::log($level, $message, $context); + parent::createEntry($data); } public function setAuth(Authenticator $auth): void diff --git a/src/Mail/EngelsystemMailer.php b/src/Mail/EngelsystemMailer.php index 51d7a7cab..bea73cc9c 100644 --- a/src/Mail/EngelsystemMailer.php +++ b/src/Mail/EngelsystemMailer.php @@ -7,6 +7,7 @@ use Engelsystem\Helpers\Translation\Translator; use Engelsystem\Models\User\User; use Engelsystem\Renderer\Renderer; +use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\MailerInterface; class EngelsystemMailer extends Mailer @@ -21,9 +22,13 @@ class EngelsystemMailer extends Mailer * @param Renderer|null $view * @param Translator|null $translation */ - public function __construct(MailerInterface $mailer, Renderer $view = null, Translator $translation = null) - { - parent::__construct($mailer); + public function __construct( + LoggerInterface $log, + MailerInterface $mailer, + Renderer $view = null, + Translator $translation = null + ) { + parent::__construct($log, $mailer); $this->translation = $translation; $this->view = $view; @@ -38,7 +43,7 @@ public function sendViewTranslated( string $template, array $data = [], ?string $locale = null - ): void { + ): bool { if ($to instanceof User) { $locale = $locale ?: $to->settings->language; $to = $to->contact->email ?: $to->email; @@ -55,11 +60,13 @@ public function sendViewTranslated( } $subject = $this->translation ? $this->translation->translate($subject, $data) : $subject; - $this->sendView($to, $subject, $template, $data); + $status = $this->sendView($to, $subject, $template, $data); if ($activeLocale) { $this->translation->setLocale($activeLocale); } + + return $status; } /** @@ -67,11 +74,11 @@ public function sendViewTranslated( * * @param string|string[] $to */ - public function sendView(string|array $to, string $subject, string $template, array $data = []): void + public function sendView(string|array $to, string $subject, string $template, array $data = []): bool { $body = $this->view->render($template, $data); - $this->send($to, $subject, $body); + return $this->send($to, $subject, $body); } /** @@ -79,13 +86,13 @@ public function sendView(string|array $to, string $subject, string $template, ar * * @param string|string[] $to */ - public function send(string|array $to, string $subject, string $body): void + public function send(string|array $to, string $subject, string $body): bool { if ($this->subjectPrefix) { $subject = sprintf('[%s] %s', $this->subjectPrefix, trim($subject)); } - parent::send($to, $subject, $body); + return parent::send($to, $subject, $body); } public function getSubjectPrefix(): string diff --git a/src/Mail/Mailer.php b/src/Mail/Mailer.php index de6402188..eb905d79b 100644 --- a/src/Mail/Mailer.php +++ b/src/Mail/Mailer.php @@ -4,8 +4,10 @@ namespace Engelsystem\Mail; +use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Email; +use Throwable; class Mailer { @@ -13,7 +15,7 @@ class Mailer protected ?string $fromName = null; - public function __construct(protected MailerInterface $mailer) + public function __construct(protected LoggerInterface $log, protected MailerInterface $mailer) { } @@ -22,7 +24,7 @@ public function __construct(protected MailerInterface $mailer) * * @param string|string[] $to */ - public function send(string|array $to, string $subject, string $body): void + public function send(string|array $to, string $subject, string $body): bool { $message = (new Email()) ->to(...(array) $to) @@ -30,7 +32,25 @@ public function send(string|array $to, string $subject, string $body): void ->subject($subject) ->text($body); - $this->mailer->send($message); + try { + $this->mailer->send($message); + } catch (Throwable $e) { + $this->log->error( + 'Unable to send e-mail "{subject}" to {to} in {file}:{line}: {type}: {message}', + [ + 'subject' => $subject, + 'to' => $to, + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'type' => get_class($e), + 'message' => $e->getMessage(), + ] + ); + + return false; + } + + return true; } public function getFromAddress(): string diff --git a/src/Middleware/ApiRouteHandler.php b/src/Middleware/ApiRouteHandler.php new file mode 100644 index 000000000..f2a42a16b --- /dev/null +++ b/src/Middleware/ApiRouteHandler.php @@ -0,0 +1,97 @@ +getUri()))->getPath(); + if ($request instanceof Request) { + $path = $request->getPathInfo(); + } + + $path = urldecode($path); + $isApi = $this->apiPrefix && (Str::startsWith($path, $this->apiPrefix . '/') || $path == $this->apiPrefix); + $isApiAccessible = $isApi || $this->apiAccessiblePaths && in_array($path, $this->apiAccessiblePaths); + $request = $request + ->withAttribute('route-api', $isApi) + ->withAttribute('route-api-accessible', $isApiAccessible); + + return $isApi ? $this->processApi($request, $handler) : $handler->handle($request); + } + + /** + * Process the API request by ensuring that JSON is returned + */ + protected function processApi(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + $response = $handler->handle($request); + } catch (ModelNotFoundException) { + $response = new Response('', 404); + $response->setContent($response->getReasonPhrase()); + } catch (HttpException $e) { + $response = new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders()); + $response->setContent($response->getContent() ?: $response->getReasonPhrase()); + } catch (Throwable $e) { + /** @var Handler $handler */ + $handler = app('error.handler'); + $handler->exceptionHandler($e, true); + $response = new Response('', 500); + $response->setContent($response->getReasonPhrase()); + } + + if (!Str::isJson((string) $response->getBody())) { + $content = (string) $response->getBody(); + $content = Stream::create(json_encode([ + 'message' => $content, + ])); + $response = $response + ->withHeader('content-type', 'application/json') + ->withBody($content); + } + + if (!$response->hasHeader('access-control-allow-origin')) { + $response = $response->withHeader('access-control-allow-origin', '*'); + } + + $eTag = md5((string) $response->getBody()); + $response->setEtag($eTag); + + return $response; + } +} diff --git a/src/Middleware/ETagHandler.php b/src/Middleware/ETagHandler.php new file mode 100644 index 000000000..e2eaf6bb8 --- /dev/null +++ b/src/Middleware/ETagHandler.php @@ -0,0 +1,39 @@ +handle($request); + + $etagMatch = $request->getHeader('If-None-Match'); + $etag = $response->getHeader('ETag'); + + if ( + !$etagMatch + || !$etag + || !Str::contains(implode(', ', $etagMatch), trim($etag[0], '"')) + ) { + return $response; + } + + return $response + ->withStatus(Response::HTTP_NOT_MODIFIED) + ->withBody(Stream::create()); + } +} diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 34b861fe1..c10652283 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -20,12 +20,10 @@ class LegacyMiddleware implements MiddlewareInterface 'admin_event_config', 'angeltypes', 'public_dashboard', - 'rooms', + 'locations', 'shift_entries', 'shifts', 'users', - 'user_driver_licenses', - 'admin_shifts_history', ]; public function __construct(protected ContainerInterface $container, protected Authenticator $auth) @@ -62,7 +60,7 @@ public function process( $translator = $this->container->get('translator'); $page = 404; - $title = $translator->translate('Page not found'); + $title = $translator->translate('page.404.title'); $content = $translator->translate('page.404.text'); } @@ -90,16 +88,11 @@ protected function loadPage(string $page): array return users_controller(); case 'user_angeltypes': return user_angeltypes_controller(); - case 'user_driver_licenses': - return user_driver_licenses_controller(); - case 'shifttypes': - list($title, $content) = shifttypes_controller(); - return [$title, $content]; case 'admin_event_config': list($title, $content) = event_config_edit_controller(); return [$title, $content]; - case 'rooms': - return rooms_controller(); + case 'locations': + return locations_controller(); case 'user_myshifts': $title = myshifts_title(); $content = user_myshifts(); @@ -108,10 +101,6 @@ protected function loadPage(string $page): array $title = shifts_title(); $content = user_shifts(); return [$title, $content]; - case 'register': - $title = register_title(); - $content = guest_register(); - return [$title, $content]; case 'admin_user': $title = admin_user_title(); $content = admin_user(); @@ -136,11 +125,9 @@ protected function loadPage(string $page): array $title = admin_shifts_title(); $content = admin_shifts(); return [$title, $content]; - case 'admin_shifts_history': - return [admin_shifts_history_title(), admin_shifts_history()]; } - throw_redirect(page_link_to('login')); + throw_redirect(url('/login')); return []; } diff --git a/src/Middleware/ResolvesMiddlewareTrait.php b/src/Middleware/ResolvesMiddlewareTrait.php index 2f21a81bf..55605f390 100644 --- a/src/Middleware/ResolvesMiddlewareTrait.php +++ b/src/Middleware/ResolvesMiddlewareTrait.php @@ -15,7 +15,7 @@ trait ResolvesMiddlewareTrait * Resolve the middleware with the container */ protected function resolveMiddleware( - string|callable|MiddlewareInterface|RequestHandlerInterface $middleware + string|callable|array|MiddlewareInterface|RequestHandlerInterface $middleware ): MiddlewareInterface|RequestHandlerInterface { if ($this->isMiddleware($middleware)) { return $middleware; diff --git a/src/Middleware/RouteDispatcher.php b/src/Middleware/RouteDispatcher.php index 1f18c4289..dcc5d05c2 100644 --- a/src/Middleware/RouteDispatcher.php +++ b/src/Middleware/RouteDispatcher.php @@ -14,18 +14,15 @@ class RouteDispatcher implements MiddlewareInterface { - protected ?MiddlewareInterface $notFound = null; - /** - * @param ResponseInterface $response Default response + * @param ResponseInterface $response Default response * @param MiddlewareInterface|null $notFound Handles any requests if the route can't be found */ public function __construct( protected FastRouteDispatcher $dispatcher, protected ResponseInterface $response, - MiddlewareInterface $notFound = null + protected ?MiddlewareInterface $notFound = null ) { - $this->notFound = $notFound; } /** @@ -34,7 +31,7 @@ public function __construct( */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $path = (new Uri($request->getUri()))->getPath(); + $path = (new Uri((string) $request->getUri()))->getPath(); if ($request instanceof Request) { $path = $request->getPathInfo(); } diff --git a/src/Middleware/SessionHandler.php b/src/Middleware/SessionHandler.php index e66559a1b..56274b252 100644 --- a/src/Middleware/SessionHandler.php +++ b/src/Middleware/SessionHandler.php @@ -13,22 +13,21 @@ class SessionHandler implements MiddlewareInterface { - public function __construct(protected SessionStorageInterface $session, protected array $paths = []) + public function __construct(protected SessionStorageInterface $session) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $requestPath = $request->getAttribute('route-request-path'); - $isApi = in_array($requestPath, $this->paths); - $request = $request->withAttribute('route-api', $isApi); - $return = $handler->handle($request); $cookies = $request->getCookieParams(); if ( - $isApi + // Is api (accessible) path + $request->getAttribute('route-api-accessible') + // Uses native PHP session && $this->session instanceof NativeSessionStorage + // No session cookie was sent on request && !isset($cookies[$this->session->getName()]) ) { $this->destroyNative(); diff --git a/src/Middleware/SessionHandlerServiceProvider.php b/src/Middleware/SessionHandlerServiceProvider.php deleted file mode 100644 index 97e9145d7..000000000 --- a/src/Middleware/SessionHandlerServiceProvider.php +++ /dev/null @@ -1,29 +0,0 @@ -app - ->when(SessionHandler::class) - ->needs('$paths') - ->give(function () { - return [ - '/api', - '/atom', - '/rss', - '/health', - '/ical', - '/metrics', - '/shifts-json-export', - '/stats', - ]; - }); - } -} diff --git a/src/Middleware/TrimInput.php b/src/Middleware/TrimInput.php new file mode 100644 index 000000000..f4b7a7e53 --- /dev/null +++ b/src/Middleware/TrimInput.php @@ -0,0 +1,70 @@ + List of field names to exclude from trim + */ + private const TRIM_EXCLUDE_LIST = [ + 'password', + 'password2', + 'new_password', + 'new_password2', + 'new_pw', + 'new_pw2', + 'password_confirmation', + ]; + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if (in_array($request->getMethod(), ['PUT', 'POST']) && is_array($request->getParsedBody())) { + $trimmedParsedBody = $this->trimArrayValues($request->getParsedBody()); + $request = $request->withParsedBody($trimmedParsedBody); + } + + + return $handler->handle($request); + } + + /** + * @template AK array key type + * @template AV array value type + * @param array $array + * @return array + */ + private function trimArrayValues(array $array): array + { + $result = []; + + foreach ($array as $key => $value) { + if (is_array($value)) { + // recurse trim + $result[$key] = $this->trimArrayValues($value); + continue; + } + + if (is_string($value) && !in_array($key, self::TRIM_EXCLUDE_LIST)) { + // trim only non-excluded string values + $result[$key] = trim($value); + continue; + } + + $result[$key] = $value; + } + + return $result; + } +} diff --git a/src/Models/AngelType.php b/src/Models/AngelType.php index 4438106f5..3fdca538b 100644 --- a/src/Models/AngelType.php +++ b/src/Models/AngelType.php @@ -23,9 +23,11 @@ * @property string $contact_email * @property boolean $restricted # If users need an introduction * @property boolean $requires_driver_license # If users must have a driver license - * @property boolean $no_self_signup # Users can sign up for shifts + * @property boolean $requires_ifsg_certificate # If users must have a ifsg certificate + * @property boolean $shift_self_signup # Users can sign up for shifts * @property boolean $show_on_dashboard # Show on public dashboard * @property boolean $hide_register # Hide from registration page + * @property boolean $hide_on_shift_view # Hide from shift page * * @property-read Collection|NeededAngelType[] $neededBy * @property-read UserAngelType $pivot @@ -48,7 +50,21 @@ class AngelType extends BaseModel { use HasFactory; - /** @var array */ + /** @var array Default attributes */ + protected $attributes = [ // phpcs:ignore + 'restricted' => false, + 'requires_driver_license' => false, + 'requires_ifsg_certificate' => false, + 'shift_self_signup' => true, + 'show_on_dashboard' => false, + 'hide_register' => false, + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ // phpcs:ignore 'name', 'description', @@ -59,18 +75,22 @@ class AngelType extends BaseModel 'restricted', 'requires_driver_license', - 'no_self_signup', + 'requires_ifsg_certificate', + 'shift_self_signup', 'show_on_dashboard', 'hide_register', + 'hide_on_shift_view', ]; /** @var array */ protected $casts = [ // phpcs:ignore - 'restricted' => 'boolean', - 'requires_driver_license' => 'boolean', - 'no_self_signup' => 'boolean', - 'show_on_dashboard' => 'boolean', - 'hide_register' => 'boolean', + 'restricted' => 'boolean', + 'requires_driver_license' => 'boolean', + 'requires_ifsg_certificate' => 'boolean', + 'shift_self_signup' => 'boolean', + 'show_on_dashboard' => 'boolean', + 'hide_register' => 'boolean', + 'hide_on_shift_view' => 'boolean', ]; public function neededBy(): HasMany diff --git a/src/Models/Room.php b/src/Models/Location.php similarity index 58% rename from src/Models/Room.php rename to src/Models/Location.php index 42c6d9a9f..e155f87d5 100644 --- a/src/Models/Room.php +++ b/src/Models/Location.php @@ -6,9 +6,11 @@ use Carbon\Carbon; use Engelsystem\Models\Shifts\NeededAngelType; +use Engelsystem\Models\Shifts\Schedule; use Engelsystem\Models\Shifts\Shift; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -21,24 +23,32 @@ * @property Carbon|null $created_at * @property Carbon|null $updated_at * + * @property-read Collection|Schedule[] $activeForSchedules * @property-read Collection|NeededAngelType[] $neededAngelTypes * @property-read Collection|Shift[] $shifts * - * @method static QueryBuilder|Room[] whereId($value) - * @method static QueryBuilder|Room[] whereName($value) - * @method static QueryBuilder|Room[] whereMapUrl($value) - * @method static QueryBuilder|Room[] whereDect($value) - * @method static QueryBuilder|Room[] whereDescription($value) - * @method static QueryBuilder|Room[] whereCreatedAt($value) - * @method static QueryBuilder|Room[] whereUpdatedAt($value) + * @method static QueryBuilder|Location[] whereId($value) + * @method static QueryBuilder|Location[] whereName($value) + * @method static QueryBuilder|Location[] whereMapUrl($value) + * @method static QueryBuilder|Location[] whereDect($value) + * @method static QueryBuilder|Location[] whereDescription($value) + * @method static QueryBuilder|Location[] whereCreatedAt($value) + * @method static QueryBuilder|Location[] whereUpdatedAt($value) */ -class Room extends BaseModel +class Location extends BaseModel { use HasFactory; /** @var bool Enable timestamps */ public $timestamps = true; // phpcs:ignore + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'map_url' => null, + 'description' => null, + 'dect' => null, + ]; + /** @var array */ protected $fillable = [ // phpcs:ignore 'name', @@ -47,6 +57,11 @@ class Room extends BaseModel 'description', ]; + public function activeForSchedules(): BelongsToMany + { + return $this->belongsToMany(Schedule::class, 'schedule_locations'); + } + public function neededAngelTypes(): HasMany { return $this->hasMany(NeededAngelType::class); diff --git a/src/Models/LogEntry.php b/src/Models/LogEntry.php index f8577f857..ae2df0364 100644 --- a/src/Models/LogEntry.php +++ b/src/Models/LogEntry.php @@ -5,6 +5,8 @@ namespace Engelsystem\Models; use Carbon\Carbon; +use Engelsystem\Models\User\User; +use Engelsystem\Models\User\UsesUserModel; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -23,35 +25,52 @@ */ class LogEntry extends BaseModel { + use UsesUserModel; + /** @var bool enable timestamps for created_at */ public $timestamps = true; // phpcs:ignore /** @var null Disable updated_at */ public const UPDATED_AT = null; + /** @var array */ + protected $casts = [ // phpcs:ignore + 'user_id' => 'integer', + ]; + /** * The attributes that are mass assignable. */ protected $fillable = [ // phpcs:ignore 'level', 'message', + 'user_id', ]; /** * @return Builder[]|Collection|SupportCollection|LogEntry[] */ - public static function filter(string $keyword = null): array|Collection|SupportCollection + public static function filter(?string $keyword = null, ?int $userId = null): array|Collection|SupportCollection { - $query = self::query() - ->select() + $query = self::with(['user', 'user.personalData', 'user.state']) ->orderByDesc('created_at') ->orderByDesc('id') ->limit(10000); + if (!empty($userId)) { + $query->where(function (Builder $query) use ($userId): void { + $user = User::findOrFail($userId); + $query->where('user_id', $userId) + ->orWhere('message', 'like', '%' . $user->name . ' (' . $userId . ')%'); + }); + } + if (!empty($keyword)) { $query - ->where('level', '=', $keyword) - ->orWhere('message', 'LIKE', '%' . $keyword . '%'); + ->where(function (Builder $query) use ($keyword): void { + $query->where('level', '=', $keyword) + ->orWhere('message', 'LIKE', '%' . $keyword . '%'); + }); } return $query->get(); diff --git a/src/Models/News.php b/src/Models/News.php index 0bcd42dc8..58b54ad21 100644 --- a/src/Models/News.php +++ b/src/Models/News.php @@ -16,9 +16,9 @@ * @property int $id * @property string $title * @property string $text + * @property bool $is_highlighted * @property bool $is_meeting * @property bool $is_pinned - * @property bool $is_important * @property Carbon|null $created_at * @property Carbon|null $updated_at * @@ -30,7 +30,7 @@ * @method static QueryBuilder|News[] whereText($value) * @method static QueryBuilder|News[] whereIsMeeting($value) * @method static QueryBuilder|News[] whereIsPinned($value) - * @method static QueryBuilder|News[] whereIsImportant($value) + * @method static QueryBuilder|News[] whereIsHighlighted($value) * @method static QueryBuilder|News[] whereCreatedAt($value) * @method static QueryBuilder|News[] whereUpdatedAt($value) */ @@ -44,17 +44,17 @@ class News extends BaseModel /** @var array */ protected $casts = [ // phpcs:ignore - 'user_id' => 'integer', - 'is_meeting' => 'boolean', - 'is_pinned' => 'boolean', - 'is_important' => 'boolean', + 'user_id' => 'integer', + 'is_meeting' => 'boolean', + 'is_pinned' => 'boolean', + 'is_highlighted' => 'boolean', ]; /** @var array Default attributes */ protected $attributes = [ // phpcs:ignore - 'is_meeting' => false, - 'is_pinned' => false, - 'is_important' => false, + 'is_meeting' => false, + 'is_pinned' => false, + 'is_highlighted' => false, ]; /** @var array */ @@ -63,7 +63,7 @@ class News extends BaseModel 'text', 'is_meeting', 'is_pinned', - 'is_important', + 'is_highlighted', 'user_id', ]; diff --git a/src/Models/OAuth.php b/src/Models/OAuth.php index 62de480ea..0b94f98c1 100644 --- a/src/Models/OAuth.php +++ b/src/Models/OAuth.php @@ -6,6 +6,7 @@ use Carbon\Carbon; use Engelsystem\Models\User\UsesUserModel; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Query\Builder as QueryBuilder; /** @@ -26,21 +27,25 @@ */ class OAuth extends BaseModel { + use HasFactory; use UsesUserModel; public $table = 'oauth'; // phpcs:ignore + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'access_token' => null, + 'refresh_token' => null, + 'expires_at' => null, + ]; + /** @var bool Enable timestamps */ public $timestamps = true; // phpcs:ignore /** @var array */ protected $casts = [ // phpcs:ignore 'user_id' => 'integer', - ]; - - /** @var array */ - protected $dates = [ // phpcs:ignore - 'expires_at', + 'expires_at' => 'datetime', ]; /** @var array */ diff --git a/src/Models/Question.php b/src/Models/Question.php index cab51ae8e..bb956c021 100644 --- a/src/Models/Question.php +++ b/src/Models/Question.php @@ -36,9 +36,11 @@ class Question extends BaseModel /** @var bool Enable timestamps */ public $timestamps = true; // phpcs:ignore - /** @var array */ - protected $dates = [ // phpcs:ignore - 'answered_at', + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'answer' => null, + 'answerer_id' => null, + 'answered_at' => null, ]; /** @var array */ @@ -54,6 +56,7 @@ class Question extends BaseModel protected $casts = [ // phpcs:ignore 'user_id' => 'integer', 'answerer_id' => 'integer', + 'answered_at' => 'datetime', ]; public function answerer(): BelongsTo diff --git a/src/Models/Session.php b/src/Models/Session.php new file mode 100644 index 000000000..925eb3f00 --- /dev/null +++ b/src/Models/Session.php @@ -0,0 +1,52 @@ + default attributes */ + protected $attributes = [ // phpcs:ignore + 'payload' => '', + 'user_id' => null, + ]; + + /** @var array */ + protected $fillable = [ // phpcs:ignore + 'id', + 'payload', + 'user_id', + ]; + + /** @var array */ + protected $casts = [ // phpcs:ignore + 'user_id' => 'integer', + 'last_activity' => 'datetime', + ]; +} diff --git a/src/Models/Shifts/NeededAngelType.php b/src/Models/Shifts/NeededAngelType.php index c3223b2ea..4241f7673 100644 --- a/src/Models/Shifts/NeededAngelType.php +++ b/src/Models/Shifts/NeededAngelType.php @@ -6,25 +6,28 @@ use Engelsystem\Models\AngelType; use Engelsystem\Models\BaseModel; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Query\Builder as QueryBuilder; /** * @property int $id - * @property int|null $room_id + * @property int|null $location_id * @property int|null $shift_id + * @property int|null $shift_type_id * @property int $angel_type_id * @property int $count * - * @property-read Room|null $room + * @property-read Location|null $location * @property-read Shift|null $shift + * @property-read ShiftType|null $shiftType * @property-read AngelType $angelType * * @method static QueryBuilder|NeededAngelType[] whereId($value) - * @method static QueryBuilder|NeededAngelType[] whereRoomId($value) + * @method static QueryBuilder|NeededAngelType[] whereLocationId($value) * @method static QueryBuilder|NeededAngelType[] whereShiftId($value) + * @method static QueryBuilder|NeededAngelType[] whereShiftTypeId($value) * @method static QueryBuilder|NeededAngelType[] whereAngelTypeId($value) * @method static QueryBuilder|NeededAngelType[] whereCount($value) */ @@ -32,23 +35,25 @@ class NeededAngelType extends BaseModel { use HasFactory; + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'location_id' => null, + 'shift_id' => null, + 'shift_type_id' => null, + ]; + /** @var array */ protected $fillable = [ // phpcs:ignore - 'room_id', + 'location_id', 'shift_id', + 'shift_type_id', 'angel_type_id', 'count', ]; - /** @var array default attributes */ - protected $attributes = [ // phpcs:ignore - 'room_id' => null, - 'shift_id' => null, - ]; - - public function room(): BelongsTo + public function location(): BelongsTo { - return $this->belongsTo(Room::class); + return $this->belongsTo(Location::class); } public function shift(): BelongsTo @@ -56,6 +61,11 @@ public function shift(): BelongsTo return $this->belongsTo(Shift::class); } + public function shiftType(): BelongsTo + { + return $this->belongsTo(ShiftType::class); + } + public function angelType(): BelongsTo { return $this->belongsTo(AngelType::class); diff --git a/src/Models/Shifts/Schedule.php b/src/Models/Shifts/Schedule.php index b8cbd89cb..3688f522e 100644 --- a/src/Models/Shifts/Schedule.php +++ b/src/Models/Shifts/Schedule.php @@ -6,9 +6,11 @@ use Carbon\Carbon; use Engelsystem\Models\BaseModel; +use Engelsystem\Models\Location; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -18,11 +20,13 @@ * @property string $name * @property string $url * @property int $shift_type + * @property bool $needed_from_shift_type * @property int $minutes_before * @property int $minutes_after * @property Carbon $created_at * @property Carbon $updated_at * + * @property-read QueryBuilder|Location[] $activeLocations * @property-read QueryBuilder|Collection|Shift[] $shifts * @property-read QueryBuilder|Collection|ScheduleShift[] $scheduleShifts * @property-read QueryBuilder|ShiftType $shiftType @@ -31,6 +35,7 @@ * @method static QueryBuilder|Schedule[] whereName($value) * @method static QueryBuilder|Schedule[] whereUrl($value) * @method static QueryBuilder|Schedule[] whereShiftType($value) + * @method static QueryBuilder|Schedule[] whereNeededFromShiftType($value) * @method static QueryBuilder|Schedule[] whereMinutesBefore($value) * @method static QueryBuilder|Schedule[] whereMinutesAfter($value) * @method static QueryBuilder|Schedule[] whereCreatedAt($value) @@ -46,6 +51,7 @@ class Schedule extends BaseModel /** @var array */ protected $casts = [ // phpcs:ignore 'shift_type' => 'integer', + 'needed_from_shift_type' => 'boolean', 'minutes_before' => 'integer', 'minutes_after' => 'integer', ]; @@ -55,10 +61,16 @@ class Schedule extends BaseModel 'name', 'url', 'shift_type', + 'needed_from_shift_type', 'minutes_before', 'minutes_after', ]; + public function activeLocations(): BelongsToMany + { + return $this->belongsToMany(Location::class, 'schedule_locations'); + } + public function scheduleShifts(): HasMany { return $this->hasMany(ScheduleShift::class); diff --git a/src/Models/Shifts/Shift.php b/src/Models/Shifts/Shift.php index ceabee404..f062d72e9 100644 --- a/src/Models/Shifts/Shift.php +++ b/src/Models/Shifts/Shift.php @@ -6,7 +6,7 @@ use Carbon\Carbon; use Engelsystem\Models\BaseModel; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -23,7 +23,7 @@ * @property Carbon $start * @property Carbon $end * @property int $shift_type_id - * @property int $room_id + * @property int $location_id * @property string $transaction_id * @property int $created_by * @property int|null $updated_by @@ -34,7 +34,7 @@ * @property-read Schedule $schedule * @property-read Collection|ShiftEntry[] $shiftEntries * @property-read ShiftType $shiftType - * @property-read Room $room + * @property-read Location $location * @property-read User $createdBy * @property-read User|null $updatedBy * @@ -45,7 +45,7 @@ * @method static QueryBuilder|Shift[] whereStart($value) * @method static QueryBuilder|Shift[] whereEnd($value) * @method static QueryBuilder|Shift[] whereShiftTypeId($value) - * @method static QueryBuilder|Shift[] whereRoomId($value) + * @method static QueryBuilder|Shift[] whereLocationId($value) * @method static QueryBuilder|Shift[] whereTransactionId($value) * @method static QueryBuilder|Shift[] whereCreatedBy($value) * @method static QueryBuilder|Shift[] whereUpdatedBy($value) @@ -56,15 +56,25 @@ class Shift extends BaseModel { use HasFactory; + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'description' => '', + 'url' => '', + 'transaction_id' => null, + 'updated_by' => null, + ]; + /** @var bool enable timestamps */ public $timestamps = true; // phpcs:ignore /** @var array */ protected $casts = [ // phpcs:ignore 'shift_type_id' => 'integer', - 'room_id' => 'integer', + 'location_id' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', + 'start' => 'datetime', + 'end' => 'datetime', ]; /** @var array Values that are mass assignable */ @@ -75,18 +85,12 @@ class Shift extends BaseModel 'start', 'end', 'shift_type_id', - 'room_id', + 'location_id', 'transaction_id', 'created_by', 'updated_by', ]; - /** @var array Values that are DateTimes */ - protected $dates = [ // phpcs:ignore - 'start', - 'end', - ]; - public function neededAngelTypes(): HasMany { return $this->hasMany(NeededAngelType::class); @@ -107,9 +111,9 @@ public function shiftType(): BelongsTo return $this->belongsTo(ShiftType::class); } - public function room(): BelongsTo + public function location(): BelongsTo { - return $this->belongsTo(Room::class); + return $this->belongsTo(Location::class); } public function createdBy(): BelongsTo diff --git a/src/Models/Shifts/ShiftEntry.php b/src/Models/Shifts/ShiftEntry.php index 632f5649a..1ba40148d 100644 --- a/src/Models/Shifts/ShiftEntry.php +++ b/src/Models/Shifts/ShiftEntry.php @@ -34,6 +34,13 @@ class ShiftEntry extends BaseModel use HasFactory; use UsesUserModel; + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'user_comment' => '', + 'freeloaded' => false, + 'freeloaded_comment' => '', + ]; + /** @var array */ protected $fillable = [ // phpcs:ignore 'shift_id', @@ -44,13 +51,6 @@ class ShiftEntry extends BaseModel 'freeloaded_comment', ]; - /** @var array default attributes */ - protected $attributes = [ // phpcs:ignore - 'user_comment' => '', - 'freeloaded' => false, - 'freeloaded_comment' => '', - ]; - /** @var array */ protected $casts = [ // phpcs:ignore 'freeloaded' => 'bool', diff --git a/src/Models/Shifts/ShiftType.php b/src/Models/Shifts/ShiftType.php index 5ee37dc08..bc3556ede 100644 --- a/src/Models/Shifts/ShiftType.php +++ b/src/Models/Shifts/ShiftType.php @@ -15,6 +15,7 @@ * @property string $name * @property string $description * + * @property-read Collection|NeededAngelType[] $neededAngelTypes * @property-read Collection|Schedule[] $schedules * @property-read Collection|Shift[] $shifts * @@ -32,6 +33,11 @@ class ShiftType extends BaseModel 'description', ]; + public function neededAngelTypes(): HasMany + { + return $this->hasMany(NeededAngelType::class); + } + public function schedules(): HasMany { return $this->hasMany(Schedule::class, 'shift_type'); diff --git a/src/Models/User/Contact.php b/src/Models/User/Contact.php index b4e74f893..043e29c2b 100644 --- a/src/Models/User/Contact.php +++ b/src/Models/User/Contact.php @@ -20,6 +20,13 @@ class Contact extends HasUserModel { use HasFactory; + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'dect' => null, + 'mobile' => null, + 'email' => null, + ]; + /** @var string The table associated with the model */ protected $table = 'users_contact'; // phpcs:ignore diff --git a/src/Models/User/License.php b/src/Models/User/License.php index c628dca28..a25102bbc 100644 --- a/src/Models/User/License.php +++ b/src/Models/User/License.php @@ -14,6 +14,8 @@ * @property bool $drive_3_5t * @property bool $drive_7_5t * @property bool $drive_12t + * @property bool $ifsg_certificate_light + * @property bool $ifsg_certificate * * @method static QueryBuilder|License[] whereHasCar($value) * @method static QueryBuilder|License[] whereDriveForklift($value) @@ -21,6 +23,8 @@ * @method static QueryBuilder|License[] whereDrive35T($value) * @method static QueryBuilder|License[] whereDrive75T($value) * @method static QueryBuilder|License[] whereDrive12T($value) + * @method static QueryBuilder|License[] whereIfsgCertificateLight($value) + * @method static QueryBuilder|License[] whereIfsgCertificate($value) */ class License extends HasUserModel { @@ -29,14 +33,16 @@ class License extends HasUserModel /** @var string The table associated with the model */ protected $table = 'users_licenses'; // phpcs:ignore - /** @var array Default attributes */ + /** @var array Default attributes */ protected $attributes = [ // phpcs:ignore - 'has_car' => false, - 'drive_forklift' => false, - 'drive_car' => false, - 'drive_3_5t' => false, - 'drive_7_5t' => false, - 'drive_12t' => false, + 'has_car' => false, + 'drive_forklift' => false, + 'drive_car' => false, + 'drive_3_5t' => false, + 'drive_7_5t' => false, + 'drive_12t' => false, + 'ifsg_certificate_light' => false, + 'ifsg_certificate' => false, ]; /** @@ -52,16 +58,20 @@ class License extends HasUserModel 'drive_3_5t', 'drive_7_5t', 'drive_12t', + 'ifsg_certificate_light', + 'ifsg_certificate', ]; /** @var array */ protected $casts = [ // phpcs:ignore - 'has_car' => 'boolean', - 'drive_forklift' => 'boolean', - 'drive_car' => 'boolean', - 'drive_3_5t' => 'boolean', - 'drive_7_5t' => 'boolean', - 'drive_12t' => 'boolean', + 'has_car' => 'boolean', + 'drive_forklift' => 'boolean', + 'drive_car' => 'boolean', + 'drive_3_5t' => 'boolean', + 'drive_7_5t' => 'boolean', + 'drive_12t' => 'boolean', + 'ifsg_certificate_light' => 'boolean', + 'ifsg_certificate' => 'boolean', ]; /** diff --git a/src/Models/User/PersonalData.php b/src/Models/User/PersonalData.php index 5bc017931..0c735bdaf 100644 --- a/src/Models/User/PersonalData.php +++ b/src/Models/User/PersonalData.php @@ -30,10 +30,20 @@ class PersonalData extends HasUserModel /** @var string The table associated with the model */ protected $table = 'users_personal_data'; // phpcs:ignore - /** @var array The attributes that should be mutated to dates */ - protected $dates = [ // phpcs:ignore - 'planned_arrival_date', - 'planned_departure_date', + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'first_name' => null, + 'last_name' => null, + 'pronoun' => null, + 'shirt_size' => null, + 'planned_arrival_date' => null, + 'planned_departure_date' => null, + ]; + + /** @var array */ + protected $casts = [ // phpcs:ignore + 'planned_arrival_date' => 'datetime', + 'planned_departure_date' => 'datetime', ]; /** diff --git a/src/Models/User/State.php b/src/Models/User/State.php index 6661dd6fb..64c0ea40c 100644 --- a/src/Models/User/State.php +++ b/src/Models/User/State.php @@ -11,6 +11,7 @@ /** * @property bool $arrived * @property Carbon|null $arrival_date + * @property string|null $user_info * @property bool $active * @property bool $force_active * @property bool $got_shirt @@ -18,6 +19,7 @@ * * @method static QueryBuilder|State[] whereArrived($value) * @method static QueryBuilder|State[] whereArrivalDate($value) + * @method static QueryBuilder|State[] whereUserInfo($value) * @method static QueryBuilder|State[] whereActive($value) * @method static QueryBuilder|State[] whereForceActive($value) * @method static QueryBuilder|State[] whereGotShirt($value) @@ -30,9 +32,11 @@ class State extends HasUserModel /** @var string The table associated with the model */ protected $table = 'users_state'; // phpcs:ignore - /** @var array Default attributes */ + /** @var array Default attributes */ protected $attributes = [ // phpcs:ignore 'arrived' => false, + 'arrival_date' => null, + 'user_info' => null, 'active' => false, 'force_active' => false, 'got_shirt' => false, @@ -47,11 +51,7 @@ class State extends HasUserModel 'force_active' => 'boolean', 'got_shirt' => 'boolean', 'got_voucher' => 'integer', - ]; - - /** @var array The attributes that should be mutated to dates */ - protected $dates = [ // phpcs:ignore - 'arrival_date', + 'arrival_date' => 'datetime', ]; /** @@ -63,6 +63,7 @@ class State extends HasUserModel 'user_id', 'arrived', 'arrival_date', + 'user_info', 'active', 'force_active', 'got_shirt', diff --git a/src/Models/User/User.php b/src/Models/User/User.php index 5ee705e06..71e4a8e1a 100644 --- a/src/Models/User/User.php +++ b/src/Models/User/User.php @@ -14,6 +14,7 @@ use Engelsystem\Models\OAuth; use Engelsystem\Models\Privilege; use Engelsystem\Models\Question; +use Engelsystem\Models\Session; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; use Engelsystem\Models\UserAngelType; @@ -52,6 +53,7 @@ * @property-read Collection|AngelType[] $userAngelTypes * @property-read UserAngelType $pivot * @property-read Collection|ShiftEntry[] $shiftEntries + * @property-read Collection|Session[] $sessions * @property-read Collection|Worklog[] $worklogs * @property-read Collection|Worklog[] $worklogsCreated * @property-read Collection|Question[] $questionsAsked @@ -78,6 +80,11 @@ class User extends BaseModel /** @var bool enable timestamps */ public $timestamps = true; // phpcs:ignore + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'last_login_at' => null, + ]; + /** * The attributes that are mass assignable. * @@ -97,9 +104,9 @@ class User extends BaseModel 'password', ]; - /** @var array The attributes that should be mutated to dates */ - protected $dates = [ // phpcs:ignore - 'last_login_at', + /** @var array */ + protected $casts = [ // phpcs:ignore + 'last_login_at' => 'datetime', ]; public function contact(): HasOne @@ -217,6 +224,11 @@ public function worklogsCreated(): HasMany return $this->hasMany(Worklog::class, 'creator_id'); } + public function sessions(): HasMany + { + return $this->hasMany(Session::class); + } + public function questionsAsked(): HasMany { return $this->hasMany(Question::class, 'user_id') diff --git a/src/Models/UserAngelType.php b/src/Models/UserAngelType.php index 83fcbecdc..0a37b9188 100644 --- a/src/Models/UserAngelType.php +++ b/src/Models/UserAngelType.php @@ -39,6 +39,12 @@ class UserAngelType extends Pivot /** @var bool Disable timestamps */ public $timestamps = false; // phpcs:ignore + /** @var array default attributes */ + protected $attributes = [ // phpcs:ignore + 'confirm_user_id' => null, + 'supporter' => false, + ]; + /** @var array */ protected $fillable = [ // phpcs:ignore 'user_id', diff --git a/src/Models/Worklog.php b/src/Models/Worklog.php index bd1634afc..f17343d74 100644 --- a/src/Models/Worklog.php +++ b/src/Models/Worklog.php @@ -36,16 +36,12 @@ class Worklog extends BaseModel /** @var bool Enable timestamps */ public $timestamps = true; // phpcs:ignore - /** @var array The attributes that should be mutated to dates */ - protected $dates = [ // phpcs:ignore - 'worked_at', - ]; - /** @var array */ protected $casts = [ // phpcs:ignore 'user_id' => 'integer', 'creator_id' => 'integer', 'hours' => 'float', + 'worked_at' => 'datetime', ]; /** diff --git a/src/Renderer/Twig/Extensions/Globals.php b/src/Renderer/Twig/Extensions/Globals.php index a6c581832..fbefa0904 100644 --- a/src/Renderer/Twig/Extensions/Globals.php +++ b/src/Renderer/Twig/Extensions/Globals.php @@ -5,6 +5,7 @@ namespace Engelsystem\Renderer\Twig\Extensions; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\DayOfEvent; use Engelsystem\Http\Request; use Twig\Extension\AbstractExtension as TwigExtension; use Twig\Extension\GlobalsInterface as GlobalsInterface; @@ -13,6 +14,8 @@ class Globals extends TwigExtension implements GlobalsInterface { + protected array $globals = []; + public function __construct(protected Authenticator $auth, protected Request $request) { } @@ -21,6 +24,18 @@ public function __construct(protected Authenticator $auth, protected Request $re * Returns a list of global variables to add to the existing list. */ public function getGlobals(): array + { + if (empty($this->globals)) { + $this->globals = $this->getGlobalValues(); + } + + return $this->globals; + } + + /** + * Generates the list of global variables + */ + protected function getGlobalValues(): array { $user = $this->auth->user(); $themes = config('themes'); @@ -53,6 +68,7 @@ public function getGlobals(): array 'request' => $this->request, 'themeId' => $themeId, 'theme' => $theme, + 'day_of_event' => DayOfEvent::get(), ]; } } diff --git a/tests/Feature/Controllers/RegistrationControllerTest.php b/tests/Feature/Controllers/RegistrationControllerTest.php new file mode 100644 index 000000000..5a47067ed --- /dev/null +++ b/tests/Feature/Controllers/RegistrationControllerTest.php @@ -0,0 +1,248 @@ + + */ + private array $modelsToBeDeleted; + private RegistrationController $subject; + + public function setUp(): void + { + parent::setUp(); + $this->modelsToBeDeleted = []; + $this->application = app(); + $this->oauth = $this->getMockBuilder(OAuth2::class) + ->disableOriginalConstructor() + ->getMock(); + $this->application->instance(OAuth2::class, $this->oauth); + $this->config = $this->application->get(Config::class); + $this->session = $this->application->get(SessionInterface::class); + $this->subject = $this->application->make(RegistrationController::class); + } + + public function tearDown(): void + { + parent::tearDown(); + $this->deleteModels(); + } + + /** + * Renders the registration page with a minimum fields config. + * Asserts that the basic fields are there while the other fields are not there. + * + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testViewMinimumConfig(): void + { + SignUpConfig::setMinimumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert the expected fields are there + FormFieldAssert::assertContainsInputField('username', $responseHTML); + FormFieldAssert::assertContainsInputField('password', $responseHTML); + FormFieldAssert::assertContainsInputField('password_confirmation', $responseHTML); + FormFieldAssert::assertContainsInputField('email', $responseHTML); + FormFieldAssert::assertContainsInputField('mobile', $responseHTML); + + // assert the disabled fields are not there + FormFieldAssert::assertNotContainsInputField('pronoun', $responseHTML); + FormFieldAssert::assertNotContainsInputField('firstname', $responseHTML); + FormFieldAssert::assertNotContainsInputField('lastname', $responseHTML); + FormFieldAssert::assertNotContainsInputField('email_goody', $responseHTML); + FormFieldAssert::assertNotContainsSelectField('tshirt_size', $responseHTML); + FormFieldAssert::assertNotContainsInputField('planned_arrival_date', $responseHTML); + FormFieldAssert::assertNotContainsInputField('mobile_show', $responseHTML); + FormFieldAssert::assertNotContainsInputField('dect', $responseHTML); + } + + /** + * Renders the registration page with a maximum fields config. + * Asserts that all fields are there. + * + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testViewMaximumConfig(): void + { + SignUpConfig::setMaximumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert the expected fields are there + FormFieldAssert::assertContainsInputField('pronoun', $responseHTML); + FormFieldAssert::assertContainsInputField('username', $responseHTML); + FormFieldAssert::assertContainsInputField('email', $responseHTML); + FormFieldAssert::assertContainsInputField('mobile', $responseHTML); + FormFieldAssert::assertContainsInputField('password', $responseHTML); + FormFieldAssert::assertContainsInputField('password_confirmation', $responseHTML); + FormFieldAssert::assertContainsInputField('firstname', $responseHTML); + FormFieldAssert::assertContainsInputField('lastname', $responseHTML); + FormFieldAssert::assertContainsInputField('email_goody', $responseHTML); + FormFieldAssert::assertContainsSelectField('tshirt_size', $responseHTML); + FormFieldAssert::assertContainsInputField('planned_arrival_date', $responseHTML); + FormFieldAssert::assertContainsInputField('mobile_show', $responseHTML); + FormFieldAssert::assertContainsInputField('dect', $responseHTML); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testViewAngelTypesOAuthPreselection(): void + { + SignUpConfig::setMinimumConfig($this->config); + $angelTypes = $this->createAngelTypes(); + $this->session->set('oauth2_connect_provider', 'test_oauth_provider'); + $this->session->set('oauth2_groups', [$angelTypes[1]->name]); + $this->oauth + ->method('getSsoTeams') + ->with('test_oauth_provider') + ->willReturn( + [ + $angelTypes[1]->name => ['id' => $angelTypes[1]->id], + $angelTypes[2]->name => ['id' => $angelTypes[2]->id], + ], + ); + + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert that the unrestricted angel type is there and checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[0]->id, $responseHTML); + + // assert that the first restricted angel type from oauth is there and checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[1]->id, $responseHTML); + + // assert that the second restricted angel type not in oauth is there and not checked + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[2]->id, $responseHTML); + + // assert that the angel type with "hide_register" = true is not there + FormFieldAssert::assertNotContainsInputField('angel_types_' . $angelTypes[3]->id, $responseHTML); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testViewAngelTypesPreselection(): void + { + $angelTypes = $this->createAngelTypes(); + + SignUpConfig::setMinimumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert that the unrestricted angel type is there and checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[0]->id, $responseHTML); + + // assert that restricted angel type are there and not checked + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[1]->id, $responseHTML); + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[2]->id, $responseHTML); + + // assert that the angel type with "hide_register" = true is not there + FormFieldAssert::assertNotContainsInputField('angel_types_' . $angelTypes[3]->id, $responseHTML); + } + + /** + * Asserts that values are prefilled after submit + * + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testViewValuesAfterSubmit(): void + { + $angelTypes = $this->createAngelTypes(); + + // fake submit and set form-data in session + $this->session->set('form-data-register-submit', '1'); + $this->session->set('form-data-angel_types_' . $angelTypes[1]->id, '1'); + + SignUpConfig::setMinimumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert that the unrestricted angel type is not checked + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[0]->id, $responseHTML); + + // assert that the restricted angel type is checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[1]->id, $responseHTML); + } + + /** + * Creates three angel types: + * - unrestricted + * - restricted + * - unrestricted, hidden on registration + * + * @return Array + */ + private function createAngelTypes(): array + { + $angelType1 = AngelType::create([ + 'name' => 'Test angel type 1', + ]); + + $angelType2 = AngelType::create([ + 'name' => 'Test angel type 2', + 'restricted' => true, + ]); + + $angelType3 = AngelType::create([ + 'name' => 'Test angel type 3', + 'restricted' => true, + ]); + + $angelType4 = AngelType::create([ + 'name' => 'Test angel type 4', + 'hide_register' => true, + ]); + + $this->modelsToBeDeleted[] = $angelType1; + $this->modelsToBeDeleted[] = $angelType2; + $this->modelsToBeDeleted[] = $angelType3; + $this->modelsToBeDeleted[] = $angelType4; + return [$angelType1, $angelType2, $angelType3, $angelType4]; + } + + private function deleteModels(): void + { + foreach ($this->modelsToBeDeleted as $modelToBeDeleted) { + $modelToBeDeleted->delete(); + } + } +} diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php index 7e10aafef..f929acf6b 100644 --- a/tests/Unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -52,6 +52,7 @@ public function testAppPath(): void $this->assertTrue($app->has('path.config')); $this->assertTrue($app->has('path.lang')); $this->assertTrue($app->has('path.resources')); + $this->assertTrue($app->has('path.resources.api')); $this->assertTrue($app->has('path.views')); $this->assertTrue($app->has('path.storage')); $this->assertTrue($app->has('path.cache')); @@ -114,7 +115,8 @@ public function testRegisterClassName(): void { $app = new Application(); - $mockClassName = $this->getMockClass(ServiceProvider::class); + $mock = $this->createMock(ServiceProvider::class); + $mockClassName = get_class($mock); $serviceProvider = $this->getMockBuilder($mockClassName) ->setConstructorArgs([$app]) ->onlyMethods(['register']) diff --git a/tests/Unit/Config/ConfigServiceProviderTest.php b/tests/Unit/Config/ConfigServiceProviderTest.php index e8d688afc..f528b0fad 100644 --- a/tests/Unit/Config/ConfigServiceProviderTest.php +++ b/tests/Unit/Config/ConfigServiceProviderTest.php @@ -19,6 +19,9 @@ class ConfigServiceProviderTest extends ServiceProviderTest { use ArraySubsetAsserts; + private array $configVarsWhereNullIsPruned = + ['themes', 'tshirt_sizes', 'headers', 'header_items', 'footer_items', 'locales']; + /** * @covers \Engelsystem\Config\ConfigServiceProvider::getConfigPath * @covers \Engelsystem\Config\ConfigServiceProvider::register @@ -29,11 +32,40 @@ public function testRegister(): void /** @var Config|MockObject $config */ list($app, $config) = $this->getConfiguredApp(__DIR__ . '/../../../config'); - $this->setExpects($config, 'set', null, null, $this->exactly(3)); - $config->expects($this->exactly(4)) + $config + ->expects($this->exactly(4 + sizeof($this->configVarsWhereNullIsPruned))) ->method('get') - ->with(null) - ->willReturnOnConsecutiveCalls([], [], [], ['lor' => 'em']); + ->with($this->callback(function (mixed $arg) { + return is_null($arg) || in_array($arg, $this->configVarsWhereNullIsPruned); + })) + ->will($this->returnCallback(function (mixed $arg) { + if (in_array($arg, $this->configVarsWhereNullIsPruned)) { + return [$arg . '_foo' => $arg . '_bar', $arg . '_willBePruned' => null]; + } elseif (is_null($arg)) { + return ['some' => 'value']; + } else { + throw new Exception('Unexpected arg: ' . $arg); + } + })); + + $config + ->expects($this->exactly(3 + sizeof($this->configVarsWhereNullIsPruned))) + ->method('set') + //With does not support a callback funtion with multiple args ... + //Therefore, we misuse will + ->will($this->returnCallback(function (mixed $key, mixed $value = null) { + if (is_array($key)) { + return null; + } + if (in_array($key, $this->configVarsWhereNullIsPruned)) { + if ($value == [$key . '_foo' => $key . '_bar']) { + return null; + } + throw new Exception('Value for key ' . print_r($key, true) . + 'is not as expected: ' . print_r($value, true)); + } + throw new Exception('Unexpected key: ' . print_r($key, true)); + })); $configFile = __DIR__ . '/../../../config/config.php'; $configExists = file_exists($configFile); @@ -100,7 +132,7 @@ public function testBoot(): void } if (is_null($returnValue)) { - throw new QueryException('', [], new Exception()); + throw new QueryException('', '', [], new Exception()); } return null; diff --git a/tests/Unit/Config/RoutesFileTest.php b/tests/Unit/Config/RoutesFileTest.php index f2c957b30..377f5d39e 100644 --- a/tests/Unit/Config/RoutesFileTest.php +++ b/tests/Unit/Config/RoutesFileTest.php @@ -12,6 +12,7 @@ class RoutesFileTest extends TestCase { /** * @doesNotPerformAssertions + * @coversNothing */ public function testLoadRoutes(): void { diff --git a/tests/Unit/Controllers/Admin/RoomsControllerTest.php b/tests/Unit/Controllers/Admin/LocationsControllerTest.php similarity index 55% rename from tests/Unit/Controllers/Admin/RoomsControllerTest.php rename to tests/Unit/Controllers/Admin/LocationsControllerTest.php index 4e8853313..77eddf2f0 100644 --- a/tests/Unit/Controllers/Admin/RoomsControllerTest.php +++ b/tests/Unit/Controllers/Admin/LocationsControllerTest.php @@ -4,14 +4,14 @@ namespace Engelsystem\Test\Unit\Controllers\Admin; -use Engelsystem\Controllers\Admin\RoomsController; +use Engelsystem\Controllers\Admin\LocationsController; use Engelsystem\Events\EventDispatcher; use Engelsystem\Helpers\Carbon; use Engelsystem\Http\Exceptions\ValidationException; use Engelsystem\Http\Redirector; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\AngelType; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; @@ -19,26 +19,26 @@ use Engelsystem\Test\Unit\Controllers\ControllerTest; use PHPUnit\Framework\MockObject\MockObject; -class RoomsControllerTest extends ControllerTest +class LocationsControllerTest extends ControllerTest { protected Redirector|MockObject $redirect; /** - * @covers \Engelsystem\Controllers\Admin\RoomsController::__construct - * @covers \Engelsystem\Controllers\Admin\RoomsController::index + * @covers \Engelsystem\Controllers\Admin\LocationsController::__construct + * @covers \Engelsystem\Controllers\Admin\LocationsController::index */ public function testIndex(): void { - /** @var RoomsController $controller */ - $controller = $this->app->make(RoomsController::class); - Room::factory(5)->create(); + /** @var LocationsController $controller */ + $controller = $this->app->make(LocationsController::class); + Location::factory(5)->create(); $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function (string $view, array $data) { - $this->assertEquals('admin/rooms/index', $view); + $this->assertEquals('admin/locations/index', $view); $this->assertTrue($data['is_index'] ?? false); - $this->assertCount(5, $data['rooms'] ?? []); + $this->assertCount(5, $data['locations'] ?? []); return $this->response; }); @@ -46,48 +46,52 @@ public function testIndex(): void } /** - * @covers \Engelsystem\Controllers\Admin\RoomsController::edit - * @covers \Engelsystem\Controllers\Admin\RoomsController::showEdit + * @covers \Engelsystem\Controllers\Admin\LocationsController::edit + * @covers \Engelsystem\Controllers\Admin\LocationsController::showEdit */ public function testEdit(): void { - /** @var RoomsController $controller */ - $controller = $this->app->make(RoomsController::class); - /** @var Room $room */ - $room = Room::factory()->create(); + /** @var LocationsController $controller */ + $controller = $this->app->make(LocationsController::class); + /** @var Location $location */ + $location = Location::factory()->create(); $angelTypes = AngelType::factory(3)->create(); - (new NeededAngelType(['room_id' => $room->id, 'angel_type_id' => $angelTypes[0]->id, 'count' => 3]))->save(); + (new NeededAngelType([ + 'location_id' => $location->id, + 'angel_type_id' => $angelTypes[0]->id, + 'count' => 3, + ]))->save(); $this->response->expects($this->once()) ->method('withView') - ->willReturnCallback(function (string $view, array $data) use ($room) { - $this->assertEquals('admin/rooms/edit', $view); - $this->assertEquals($room->id, $data['room']?->id); + ->willReturnCallback(function (string $view, array $data) use ($location) { + $this->assertEquals('admin/locations/edit', $view); + $this->assertEquals($location->id, $data['location']?->id); $this->assertCount(3, $data['angel_types'] ?? []); $this->assertCount(1, $data['needed_angel_types'] ?? []); return $this->response; }); - $this->request = $this->request->withAttribute('room_id', 1); + $this->request = $this->request->withAttribute('location_id', 1); $controller->edit($this->request); } /** - * @covers \Engelsystem\Controllers\Admin\RoomsController::edit - * @covers \Engelsystem\Controllers\Admin\RoomsController::showEdit + * @covers \Engelsystem\Controllers\Admin\LocationsController::edit + * @covers \Engelsystem\Controllers\Admin\LocationsController::showEdit */ public function testEditNew(): void { - /** @var RoomsController $controller */ - $controller = $this->app->make(RoomsController::class); + /** @var LocationsController $controller */ + $controller = $this->app->make(LocationsController::class); AngelType::factory(3)->create(); $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function (string $view, array $data) { - $this->assertEquals('admin/rooms/edit', $view); - $this->assertEmpty($data['room'] ?? []); + $this->assertEquals('admin/locations/edit', $view); + $this->assertEmpty($data['location'] ?? []); $this->assertCount(3, $data['angel_types'] ?? []); $this->assertEmpty($data['needed_angel_types'] ?? []); return $this->response; @@ -97,19 +101,19 @@ public function testEditNew(): void } /** - * @covers \Engelsystem\Controllers\Admin\RoomsController::save + * @covers \Engelsystem\Controllers\Admin\LocationsController::save */ public function testSave(): void { - /** @var RoomsController $controller */ - $controller = $this->app->make(RoomsController::class); + /** @var LocationsController $controller */ + $controller = $this->app->make(LocationsController::class); $controller->setValidator(new Validator()); AngelType::factory(3)->create(); - $this->setExpects($this->redirect, 'to', ['/admin/rooms']); + $this->setExpects($this->redirect, 'to', ['/admin/locations']); $this->request = $this->request->withParsedBody([ - 'name' => 'Testroom', + 'name' => 'Testlocation', 'description' => 'Something', 'dect' => 'DECTNR', 'map_url' => 'https://osm.url/#map=h/x/y', @@ -119,11 +123,11 @@ public function testSave(): void $controller->save($this->request); - $this->assertTrue($this->log->hasInfoThatContains('Updated room')); - $this->assertHasNotification('room.edit.success'); - $this->assertCount(1, Room::whereName('Testroom')->get()); + $this->assertTrue($this->log->hasInfoThatContains('Updated location')); + $this->assertHasNotification('location.edit.success'); + $this->assertCount(1, Location::whereName('Testlocation')->get()); - $neededAngelType = NeededAngelType::whereRoomId(1) + $neededAngelType = NeededAngelType::whereLocationId(1) ->where('angel_type_id', 2) ->where('count', 3) ->get(); @@ -131,17 +135,17 @@ public function testSave(): void } /** - * @covers \Engelsystem\Controllers\Admin\RoomsController::save + * @covers \Engelsystem\Controllers\Admin\LocationsController::save */ public function testSaveUniqueName(): void { - /** @var RoomsController $controller */ - $controller = $this->app->make(RoomsController::class); + /** @var LocationsController $controller */ + $controller = $this->app->make(LocationsController::class); $controller->setValidator(new Validator()); - Room::factory()->create(['name' => 'Testroom']); + Location::factory()->create(['name' => 'Testlocation']); $this->request = $this->request->withParsedBody([ - 'name' => 'Testroom', + 'name' => 'Testlocation', ]); $this->expectException(ValidationException::class); @@ -149,16 +153,16 @@ public function testSaveUniqueName(): void } /** - * @covers \Engelsystem\Controllers\Admin\RoomsController::save - * @covers \Engelsystem\Controllers\Admin\RoomsController::delete + * @covers \Engelsystem\Controllers\Admin\LocationsController::save + * @covers \Engelsystem\Controllers\Admin\LocationsController::delete */ public function testSaveDelete(): void { - /** @var RoomsController $controller */ - $controller = $this->app->make(RoomsController::class); + /** @var LocationsController $controller */ + $controller = $this->app->make(LocationsController::class); $controller->setValidator(new Validator()); - /** @var Room $room */ - $room = Room::factory()->create(); + /** @var Location $location */ + $location = Location::factory()->create(); $this->request = $this->request->withParsedBody([ 'id' => '1', @@ -166,36 +170,36 @@ public function testSaveDelete(): void ]); $controller->save($this->request); - $this->assertEmpty(Room::find($room->id)); + $this->assertEmpty(Location::find($location->id)); } /** - * @covers \Engelsystem\Controllers\Admin\RoomsController::delete + * @covers \Engelsystem\Controllers\Admin\LocationsController::delete */ public function testDelete(): void { /** @var EventDispatcher|MockObject $dispatcher */ $dispatcher = $this->createMock(EventDispatcher::class); $this->app->instance('events.dispatcher', $dispatcher); - /** @var RoomsController $controller */ - $controller = $this->app->make(RoomsController::class); + /** @var LocationsController $controller */ + $controller = $this->app->make(LocationsController::class); $controller->setValidator(new Validator()); - /** @var Room $room */ - $room = Room::factory()->create(); + /** @var Location $location */ + $location = Location::factory()->create(); /** @var Shift $shift */ - $shift = Shift::factory()->create(['room_id' => $room->id, 'start' => Carbon::create()->subHour()]); + $shift = Shift::factory()->create(['location_id' => $location->id, 'start' => Carbon::create()->subHour()]); /** @var User $user */ $user = User::factory()->create(['name' => 'foo', 'email' => 'lorem@ipsum']); /** @var ShiftEntry $shiftEntry */ ShiftEntry::factory()->create(['shift_id' => $shift->id, 'user_id' => $user->id]); - $this->setExpects($this->redirect, 'to', ['/admin/rooms'], $this->response); + $this->setExpects($this->redirect, 'to', ['/admin/locations'], $this->response); $dispatcher->expects($this->once()) ->method('dispatch') - ->willReturnCallback(function (string $event, array $data) use ($room, $user) { + ->willReturnCallback(function (string $event, array $data) use ($location, $user) { $this->assertEquals('shift.entry.deleting', $event); - $this->assertEquals($room->id, $data['room']->id); + $this->assertEquals($location->id, $data['location']->id); $this->assertEquals($user->id, $data['user']->id); return []; @@ -205,9 +209,9 @@ public function testDelete(): void $controller->delete($this->request); - $this->assertNull(Room::find($room->id)); - $this->assertTrue($this->log->hasInfoThatContains('Deleted room')); - $this->assertHasNotification('room.delete.success'); + $this->assertNull(Location::find($location->id)); + $this->assertTrue($this->log->hasInfoThatContains('Deleted location')); + $this->assertHasNotification('location.delete.success'); } public function setUp(): void diff --git a/tests/Unit/Controllers/Admin/LogsControllerTest.php b/tests/Unit/Controllers/Admin/LogsControllerTest.php index 9034cd1cb..9cc7f9ade 100644 --- a/tests/Unit/Controllers/Admin/LogsControllerTest.php +++ b/tests/Unit/Controllers/Admin/LogsControllerTest.php @@ -4,10 +4,13 @@ namespace Engelsystem\Test\Unit\Controllers\Admin; +use Engelsystem\Config\Config; use Engelsystem\Controllers\Admin\LogsController; +use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\LogEntry; +use Engelsystem\Models\User\User; use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\TestCase; use Illuminate\Database\Eloquent\Collection; @@ -25,22 +28,36 @@ public function testIndex(): void { $log = new LogEntry(); $alert = $log->create(['level' => LogLevel::ALERT, 'message' => 'Alert test']); - $alert = $log->find($alert)->first(); + $alert = $log->with('user')->find($alert)->first(); $error = $log->create(['level' => LogLevel::ERROR, 'message' => 'Error test']); - $error = $log->find($error)->first(); + $error = $log->with('user')->find($error)->first(); + + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, null, 2); + $this->setExpects($auth, 'can', ['logs.all'], true, 2); $response = $this->createMock(Response::class); $response->expects($this->exactly(2)) ->method('withView') ->withConsecutive( - ['admin/log.twig', ['entries' => new Collection([$error, $alert]), 'search' => null]], - ['admin/log.twig', ['entries' => new Collection([$error]), 'search' => 'error']] + ['admin/log.twig', [ + 'entries' => new Collection([$error, $alert]), + 'search' => null, + 'users' => new Collection(), + 'search_user_id' => null, + ]], + ['admin/log.twig', [ + 'entries' => new Collection([$error]), + 'search' => 'error', + 'users' => new Collection(), + 'search_user_id' => null, + ]] ) ->willReturn($response); $request = Request::create('/'); - $controller = new LogsController($log, $response); + $controller = new LogsController($log, $response, $auth); $controller->index($request); $request->request->set('search', 'error'); @@ -48,12 +65,48 @@ public function testIndex(): void } /** - * Setup the DB + * @covers \Engelsystem\Controllers\Admin\LogsController::index + */ + public function testIndexUser(): void + { + User::factory()->create(); + $user = User::with(['personalData', 'state'])->first(); + + $log = new LogEntry(); + $alert = $log->create(['level' => LogLevel::ALERT, 'message' => 'Users message', 'user_id' => $user->id]); + /** @var LogEntry $alert */ + $alert = $log->with('user')->find($alert)->first(); + $log->create(['level' => LogLevel::ERROR, 'message' => 'Error test']); + + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + $this->setExpects($auth, 'can', ['logs.all'], false); + + $response = $this->createMock(Response::class); + $response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) use ($alert, $response) { + $this->assertEquals('admin/log.twig', $view); + $this->assertArrayHasKey('entries', $data); + $this->assertCount(1, $data['entries']); + $this->assertEquals($alert->message, $data['entries'][0]['message']); + return $response; + }); + + $request = Request::create('/'); + + $controller = new LogsController($log, $response, $auth); + $controller->index($request); + } + + /** + * Set up the DB */ public function setUp(): void { parent::setUp(); $this->initDatabase(); + $this->app->instance('config', new Config([])); } } diff --git a/tests/Unit/Controllers/Admin/NewsControllerTest.php b/tests/Unit/Controllers/Admin/NewsControllerTest.php index 3943aa14b..ca40cd40e 100644 --- a/tests/Unit/Controllers/Admin/NewsControllerTest.php +++ b/tests/Unit/Controllers/Admin/NewsControllerTest.php @@ -11,6 +11,7 @@ use Engelsystem\Http\Exceptions\ValidationException; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\News; +use Engelsystem\Models\User\Settings; use Engelsystem\Models\User\User; use Engelsystem\Test\Unit\Controllers\ControllerTest; use PHPUnit\Framework\MockObject\MockObject; @@ -18,6 +19,7 @@ class NewsControllerTest extends ControllerTest { protected Authenticator|MockObject $auth; + protected EventDispatcher|MockObject $eventDispatcher; /** @var array */ protected array $data = [ @@ -42,6 +44,7 @@ public function testEdit(): void $this->assertEquals('pages/news/edit.twig', $view); $this->assertNotEmpty($data['news']); + $this->assertFalse($data['send_notification']); return $this->response; }); @@ -101,10 +104,13 @@ public function testSaveCreateInvalid(): void public function saveCreateEditProvider(): array { return [ + // Text, isMeeting, id, sendNotification ['Some test', true], ['Some test', false], ['Some test', false, 1], ['Some test', true, 1], + ['Some test', false, null, true], + ['Some test', false, 1, true], ]; } @@ -117,20 +123,33 @@ public function saveCreateEditProvider(): array public function testSaveCreateEdit( string $text, bool $isMeeting, - int $id = null + int $id = null, + bool $sendNotification = false ): void { $this->request->attributes->set('news_id', $id); - $id = $id ?: 2; $body = [ - 'title' => 'Some Title', - 'text' => $text, + 'title' => 'Some Title', + 'text' => $text, ]; if ($isMeeting) { $body['is_meeting'] = '1'; } + if ($sendNotification) { + $body['send_notification'] = '1'; + } + + $this->eventDispatcher->expects($this->once()) + ->method('dispatch') + ->willReturnCallback(function (string $event, array $payload) use ($id, $sendNotification) { + $this->assertEquals($id ? 'news.updated' : 'news.created', $event); + $this->assertEquals($sendNotification, $payload['sendNotification']); + $this->assertInstanceOf(News::class, $payload['news']); + + return $this->eventDispatcher; + }); - $this->request = $this->request->withParsedBody($body); $this->addUser(); + $this->request = $this->request->withParsedBody($body); $this->response->expects($this->once()) ->method('redirectTo') ->with('http://localhost/news') @@ -146,7 +165,7 @@ public function testSaveCreateEdit( $this->assertHasNotification('news.edit.success'); - $news = (new News())->find($id); + $news = (new News())->find($id ?: 2); $this->assertEquals($text, $news->text); $this->assertEquals($isMeeting, (bool) $news->is_meeting); } @@ -158,12 +177,13 @@ public function testSavePreview(): void { $this->request->attributes->set('news_id', 1); $this->request = $this->request->withParsedBody([ - 'title' => 'New title', - 'text' => 'New text', - 'is_meeting' => '1', - 'is_pinned' => '1', - 'is_important' => '1', - 'preview' => '1', + 'title' => 'New title', + 'text' => 'New text', + 'is_meeting' => '1', + 'is_pinned' => '1', + 'is_highlighted' => '1', + 'preview' => '1', + 'send_notification' => '1', ]); $this->response->expects($this->once()) ->method('withView') @@ -175,15 +195,17 @@ public function testSavePreview(): void // Contains new text $this->assertTrue($news->is_meeting); $this->assertTrue($news->is_pinned); - $this->assertTrue($news->is_important); + $this->assertTrue($news->is_highlighted); $this->assertEquals('New title', $news->title); $this->assertEquals('New text', $news->text); + $this->assertTrue($data['send_notification']); + return $this->response; }); $this->auth->expects($this->atLeastOnce()) ->method('can') - ->with('news.important') + ->with('news.highlight') ->willReturn(true); /** @var NewsController $controller */ @@ -198,7 +220,7 @@ public function testSavePreview(): void $this->assertEquals('**foo**', $news->text); $this->assertFalse($news->is_meeting); $this->assertFalse($news->is_pinned); - $this->assertFalse($news->is_important); + $this->assertFalse($news->is_highlighted); } /** @@ -228,12 +250,37 @@ public function testSaveDelete(): void $this->assertHasNotification('news.delete.success'); } + /** + * @covers \Engelsystem\Controllers\Admin\NewsController::save + */ + public function testSaveDuplicated(): void + { + $previousNews = News::first(); + $this->request = $this->request->withParsedBody([ + 'title' => $previousNews->title, + 'text' => $previousNews->text, + ]); + $this->response->expects($this->once()) + ->method('withView') + ->willReturn($this->response); + + /** @var NewsController $controller */ + $controller = $this->app->make(NewsController::class); + $controller->setValidator(new Validator()); + + $controller->save($this->request); + + $this->assertHasNotification('news.edit.duplicate', NotificationType::ERROR); + } + /** * Creates a new user */ protected function addUser(): void { - $user = User::factory(['id' => 42])->create(); + $user = User::factory(['id' => 42]) + ->has(Settings::factory(['email_news' => true])) + ->create(); $this->auth->expects($this->any()) ->method('user') @@ -250,16 +297,17 @@ public function setUp(): void $this->auth = $this->createMock(Authenticator::class); $this->app->instance(Authenticator::class, $this->auth); - $eventDispatcher = $this->createMock(EventDispatcher::class); - $eventDispatcher->expects(self::any()) + $this->eventDispatcher = $this->createMock(EventDispatcher::class); + $this->eventDispatcher->expects(self::any()) ->method('dispatch') ->willReturnSelf(); - $this->app->instance('events.dispatcher', $eventDispatcher); + $this->app->instance('events.dispatcher', $this->eventDispatcher); + $user = User::factory()->create(); (new News([ 'title' => 'Foo', 'text' => '**foo**', - 'user_id' => 1, + 'user_id' => $user->id, ]))->save(); } } diff --git a/tests/Unit/Controllers/Admin/ShiftTypesControllerTest.php b/tests/Unit/Controllers/Admin/ShiftTypesControllerTest.php new file mode 100644 index 000000000..d160ae6df --- /dev/null +++ b/tests/Unit/Controllers/Admin/ShiftTypesControllerTest.php @@ -0,0 +1,231 @@ +app->make(ShiftTypesController::class); + ShiftType::factory(5)->create(); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) { + $this->assertEquals('admin/shifttypes/index', $view); + $this->assertTrue($data['is_index'] ?? false); + $this->assertCount(5, $data['shifttypes'] ?? []); + return $this->response; + }); + + $controller->index(); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::view + */ + public function testView(): void + { + /** @var ShiftTypesController $controller */ + $controller = $this->app->make(ShiftTypesController::class); + $shiftType = ShiftType::factory()->create(); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) use ($shiftType) { + $this->assertEquals('admin/shifttypes/view', $view); + $this->assertArrayHasKey('shifttype', $data); + $this->assertEquals($shiftType->id, $data['shifttype']['id']); + return $this->response; + }); + + $controller->view(new Request([], [], ['shift_type_id' => $shiftType->id])); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::edit + */ + public function testEdit(): void + { + /** @var ShiftTypesController $controller */ + $controller = $this->app->make(ShiftTypesController::class); + /** @var ShiftType $shifttype */ + $shifttype = ShiftType::factory()->create(); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) use ($shifttype) { + $this->assertEquals('admin/shifttypes/edit', $view); + $this->assertEquals($shifttype->id, $data['shifttype']?->id); + $this->assertNotEmpty($data['shifttype']?->name); + $this->assertNotEmpty($data['shifttype']?->description); + $this->assertNotNull($data['angel_types']); + return $this->response; + }); + + $this->request = $this->request->withAttribute('shift_type_id', 1); + + $controller->edit($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::edit + */ + public function testEditNew(): void + { + /** @var ShiftTypesController $controller */ + $controller = $this->app->make(ShiftTypesController::class); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) { + $this->assertEquals('admin/shifttypes/edit', $view); + $this->assertArrayHasKey('shifttype', $data); + $this->assertNull($data['shifttype']); + $this->assertNotNull($data['angel_types']); + return $this->response; + }); + + $controller->edit($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::save + */ + public function testSave(): void + { + $angelType = AngelType::factory(2)->create()->first(); + + /** @var ShiftTypesController $controller */ + $controller = $this->app->make(ShiftTypesController::class); + $controller->setValidator(new Validator()); + + $this->setExpects($this->redirect, 'to', ['/admin/shifttypes']); + + $this->request = $this->request->withParsedBody([ + 'name' => 'Test shift type', + 'description' => 'Something', + 'angel_type_' . $angelType->id => 3, + 'angel_type_' . $angelType->id + 1 => 0, + ]); + + $controller->save($this->request); + + $this->assertTrue($this->log->hasInfoThatContains('Updated shift type')); + $this->assertHasNotification('shifttype.edit.success'); + $this->assertCount(1, ShiftType::whereName('Test shift type')->get()); + $this->assertCount(1, ShiftType::first()->neededAngelTypes); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::save + */ + public function testSaveUniqueName(): void + { + /** @var ShiftTypesController $controller */ + $controller = $this->app->make(ShiftTypesController::class); + $controller->setValidator(new Validator()); + ShiftType::factory()->create(['name' => 'Test shift type']); + + $this->request = $this->request->withParsedBody([ + 'name' => 'Test shift type', + ]); + + $this->expectException(ValidationException::class); + $controller->save($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::save + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::delete + */ + public function testSaveDelete(): void + { + /** @var ShiftTypesController $controller */ + $controller = $this->app->make(ShiftTypesController::class); + $controller->setValidator(new Validator()); + /** @var ShiftType $shifttype */ + $shifttype = ShiftType::factory()->create(); + + $this->request = $this->request->withParsedBody([ + 'id' => '1', + 'delete' => '1', + ]); + + $controller->save($this->request); + $this->assertEmpty(ShiftType::find($shifttype->id)); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftTypesController::delete + */ + public function testDelete(): void + { + /** @var EventDispatcher|MockObject $dispatcher */ + $dispatcher = $this->createMock(EventDispatcher::class); + $this->app->instance('events.dispatcher', $dispatcher); + /** @var ShiftTypesController $controller */ + $controller = $this->app->make(ShiftTypesController::class); + $controller->setValidator(new Validator()); + /** @var ShiftType $shifttype */ + $shifttype = ShiftType::factory()->create(); + /** @var Shift $shift */ + $shift = Shift::factory()->create(['shift_type_id' => $shifttype->id, 'start' => Carbon::create()->subHour()]); + /** @var User $user */ + $user = User::factory()->create(['name' => 'foo', 'email' => 'lorem@ipsum']); + /** @var ShiftEntry $shiftEntry */ + ShiftEntry::factory()->create(['shift_id' => $shift->id, 'user_id' => $user->id]); + + $this->setExpects($this->redirect, 'to', ['/admin/shifttypes'], $this->response); + + $dispatcher->expects($this->once()) + ->method('dispatch') + ->willReturnCallback(function (string $event, array $data) use ($shifttype, $user) { + $this->assertEquals('shift.entry.deleting', $event); + $this->assertEquals($shifttype->name, $data['name']); + $this->assertEquals($user->id, $data['user']->id); + + return []; + }); + + $this->request = $this->request->withParsedBody(['id' => 1, 'delete' => '1']); + + $controller->delete($this->request); + + $this->assertNull(ShiftType::find($shifttype->id)); + $this->assertTrue($this->log->hasInfoThatContains('Deleted shift type')); + $this->assertHasNotification('shifttype.delete.success'); + } + + public function setUp(): void + { + parent::setUp(); + + $this->redirect = $this->createMock(Redirector::class); + $this->app->instance(Redirector::class, $this->redirect); + } +} diff --git a/tests/Unit/Controllers/Admin/ShiftsControllerTest.php b/tests/Unit/Controllers/Admin/ShiftsControllerTest.php new file mode 100644 index 000000000..5a292e009 --- /dev/null +++ b/tests/Unit/Controllers/Admin/ShiftsControllerTest.php @@ -0,0 +1,83 @@ +response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) { + $this->assertEquals('admin/shifts/history', $view); + $this->assertCount(2, $data['shifts'] ?? []); + return $this->response; + }); + + /** @var ShiftsController $controller */ + $controller = $this->app->make(ShiftsController::class); + $controller->history(); + } + + /** + * @covers \Engelsystem\Controllers\Admin\ShiftsController::deleteTransaction + */ + public function testDeleteTransaction(): void + { + $this->database->getConnection()->getRawPdo()->exec('PRAGMA foreign_keys = ON'); + + $this->redirect->expects($this->once()) + ->method('back') + ->willReturn($this->response); + /** @var Shift $shift */ + $shift = Shift::factory(3)->create(['transaction_id' => Uuid::uuid()])->last(); + ShiftEntry::factory(2)->create(['shift_id' => $shift->id]); + + /** @var EventDispatcher|MockObject $event */ + $event = $this->createMock(EventDispatcher::class); + $this->app->instance('events.dispatcher', $event); + $this->setExpects($event, 'dispatch', ['shift.entry.deleting'], [], $this->exactly(2)); + + /** @var ShiftsController $controller */ + $controller = $this->app->make(ShiftsController::class); + $controller->deleteTransaction(new Request([], ['transaction_id' => $shift->transaction_id])); + + $this->assertCount(6, Shift::all()); + $this->assertCount(3, ShiftEntry::all()); + $this->log->hasInfoThatContains('Deleted shift'); + $this->log->hasInfoThatContains('shifts with transaction ID'); + $this->assertHasNotification('shifts.history.delete.success'); + } + + public function setUp(): void + { + parent::setUp(); + + $this->redirect = $this->createMock(Redirector::class); + $this->app->instance(Redirector::class, $this->redirect); + + Shift::factory(1)->create(['transaction_id' => null]); + Shift::factory(4)->create(['transaction_id' => Uuid::uuid()]); + $shift = Shift::factory(1)->create(['transaction_id' => Uuid::uuid()])->first(); + + ShiftEntry::factory(3)->create(['shift_id' => $shift->id]); + } +} diff --git a/tests/Unit/Controllers/Admin/UserShirtControllerTest.php b/tests/Unit/Controllers/Admin/UserShirtControllerTest.php index 38a217aeb..0c6a9fb20 100644 --- a/tests/Unit/Controllers/Admin/UserShirtControllerTest.php +++ b/tests/Unit/Controllers/Admin/UserShirtControllerTest.php @@ -7,6 +7,7 @@ use Engelsystem\Config\GoodieType; use Engelsystem\Controllers\Admin\UserShirtController; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Http\Exceptions\ValidationException; use Engelsystem\Http\Redirector; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\User\PersonalData; @@ -60,6 +61,7 @@ public function testIndexUserNotFound(): void } /** + * @todo Factor out separate tests. Isolated User, Config and permissions per test. * @covers \Engelsystem\Controllers\Admin\UserShirtController::saveShirt */ public function testSaveShirt(): void @@ -81,11 +83,11 @@ public function testSaveShirt(): void ->create(); $auth - ->expects($this->exactly(6)) + ->expects($this->exactly(5)) ->method('can') ->with('admin_arrive') - ->willReturnOnConsecutiveCalls(true, true, true, false, false, true); - $this->setExpects($redirector, 'back', null, $this->response, $this->exactly(6)); + ->willReturnOnConsecutiveCalls(true, true, false, false, true); + $this->setExpects($redirector, 'back', null, $this->response, $this->exactly(5)); $controller = new UserShirtController( $auth, @@ -131,7 +133,12 @@ public function testSaveShirt(): void 'shirt_size' => 'L', ]); - $controller->saveShirt($request); + try { + $controller->saveShirt($request); + self::fail('Expected exception was not raised'); + } catch (ValidationException $e) { + // ignore + } $user = User::find(1); $this->assertEquals('S', $user->personalData->shirt_size); @@ -142,6 +149,8 @@ public function testSaveShirt(): void 'arrived' => '1', ]); + $user->state->arrived = false; + $user->state->save(); $this->assertFalse($user->state->arrived); $controller->saveShirt($request); $user = User::find(1); diff --git a/tests/Unit/Controllers/Admin/UserWorkLogControllerTest.php b/tests/Unit/Controllers/Admin/UserWorkLogControllerTest.php index e4598ee35..4469f4281 100644 --- a/tests/Unit/Controllers/Admin/UserWorkLogControllerTest.php +++ b/tests/Unit/Controllers/Admin/UserWorkLogControllerTest.php @@ -60,29 +60,6 @@ public function testShowAddWorklog(): void $this->controller->editWorklog($request); } - /** - * @covers \Engelsystem\Controllers\Admin\UserWorkLogController::editWorklog - * @covers \Engelsystem\Controllers\Admin\UserWorkLogController::getWorkDateSuggestion - * - * @dataProvider buildupConfigsAndWorkDates - */ - public function testShowAddWorklogWithSuggestedWorkDate( - Carbon|null $buildup_start, - Carbon|null $event_start, - Carbon $suggested_work_date - ): void { - $request = $this->request->withAttribute('user_id', $this->user->id); - config(['buildup_start' => $buildup_start]); - config(['event_start' => $event_start]); - $this->response->expects($this->once()) - ->method('withView') - ->willReturnCallback(function (string $view, array $data) use ($suggested_work_date) { - $this->assertEquals($suggested_work_date, $data['work_date']); - return $this->response; - }); - $this->controller->editWorklog($request); - } - /** * @covers \Engelsystem\Controllers\Admin\UserWorkLogController::editWorklog */ @@ -132,7 +109,7 @@ public function testShowEditWorklog(): void /** * @covers \Engelsystem\Controllers\Admin\UserWorkLogController::saveWorklog */ - public function testSaveWorklogWithUnkownUserIdThrows(): void + public function testSaveWorklogWithUnknownUserIdThrows(): void { $request = $this->request->withAttribute('user_id', 1234)->withParsedBody([]); $this->expectException(ModelNotFoundException::class); @@ -170,6 +147,8 @@ public function testSaveNewWorklog(): void $this->controller->saveWorklog($request); $this->assertHasNotification('worklog.add.success'); + $this->assertTrue($this->log->hasInfoThatContains('Added worklog for')); + $this->assertEquals(1, $this->user->worklogs->count()); $new_worklog = $this->user->worklogs[0]; $this->assertEquals($this->user->id, $new_worklog->user->id); @@ -327,6 +306,7 @@ public function testDeleteWorklog(): void $this->controller->deleteWorklog($request); + $this->log->hasInfoThatContains('Deleted worklog'); $this->assertHasNotification('worklog.delete.success'); $worklog = Worklog::find($worklog->id); $this->assertNull($worklog); diff --git a/tests/Unit/Controllers/Api/AngelTypeControllerTest.php b/tests/Unit/Controllers/Api/AngelTypeControllerTest.php new file mode 100644 index 000000000..a4e7f63dd --- /dev/null +++ b/tests/Unit/Controllers/Api/AngelTypeControllerTest.php @@ -0,0 +1,98 @@ +create(); + + $controller = new AngelTypeController(new Response()); + + $response = $controller->index(); + $this->validateApiResponse('/angeltypes', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']); + $this->assertCount(1, collect($data['data'])->filter(function ($item) use ($items) { + return $item['name'] == $items->first()->getAttribute('name'); + })); + } + /** + * @covers \Engelsystem\Controllers\Api\AngelTypeController::ofUser + * @covers \Engelsystem\Controllers\Api\Resources\UserAngelTypeResource::toArray + */ + public function testOfUser(): void + { + $user = User::factory()->create(); + $items = UserAngelType::factory(3)->create(['user_id' => $user->id]); + + $controller = new AngelTypeController(new Response()); + + $response = $controller->ofUser(new Request([], [], ['user_id' => $user->id])); + $this->validateApiResponse('/users/{id}/angeltypes', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']); + $this->assertCount(1, collect($data['data'])->filter(function ($item) use ($items) { + return $item['name'] == $items->first()->angelType->name; + })); + } + + /** + * @covers \Engelsystem\Controllers\Api\AngelTypeController::ofUser + */ + public function testEntriesOfUserSelf(): void + { + $user = User::factory()->create(); + + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + + $request = new Request(); + $request = $request->withAttribute('user_id', 'self'); + + $controller = new AngelTypeController(new Response()); + $controller->setAuth($auth); + + $response = $controller->ofUser($request); + $this->validateApiResponse('/users/{id}/angeltypes', 'get', $response); + } + + /** + * @covers \Engelsystem\Controllers\Api\AngelTypeController::ofUser + */ + public function testEntriesByUserNotFound(): void + { + $request = new Request(); + $request = $request->withAttribute('user_id', 42); + + $controller = new AngelTypeController(new Response()); + + $this->expectException(ModelNotFoundException::class); + $controller->ofUser($request); + } +} diff --git a/tests/Unit/Controllers/Api/ApiBaseControllerTest.php b/tests/Unit/Controllers/Api/ApiBaseControllerTest.php new file mode 100644 index 000000000..b24479b96 --- /dev/null +++ b/tests/Unit/Controllers/Api/ApiBaseControllerTest.php @@ -0,0 +1,45 @@ +validator->validate($operation, $response); + } + + public function setUp(): void + { + parent::setUp(); + $this->initDatabase(); + + $openApiDefinition = $this->app->get('path.resources.api') . '/openapi.yml'; + $this->validator = (new OpenApiValidatorBuilder()) + ->fromYamlFile($openApiDefinition) + ->getResponseValidator(); + + /** @var UrlGeneratorInterface|MockObject $url */ + $url = $this->getMockForAbstractClass(UrlGeneratorInterface::class); + $url->expects($this->any()) + ->method('to') + ->willReturnCallback(function (string $path, array $params): string { + $query = http_build_query($params); + return $path . ($query ? '?' . $query : ''); + }); + $this->app->instance('http.urlGenerator', $url); + } +} diff --git a/tests/Unit/Controllers/Api/ApiControllerTest.php b/tests/Unit/Controllers/Api/ApiControllerTest.php new file mode 100644 index 000000000..296d794ca --- /dev/null +++ b/tests/Unit/Controllers/Api/ApiControllerTest.php @@ -0,0 +1,32 @@ +response; + } + }; + + $response = $controller->getResponse(); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $this->assertEquals(['*'], $response->getHeader('access-control-allow-origin')); + $this->assertJson($response->getContent()); + } +} diff --git a/tests/Unit/Controllers/Api/IndexControllerTest.php b/tests/Unit/Controllers/Api/IndexControllerTest.php new file mode 100644 index 000000000..b0d80b4a4 --- /dev/null +++ b/tests/Unit/Controllers/Api/IndexControllerTest.php @@ -0,0 +1,152 @@ +index(); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertEquals(['*'], $response->getHeader('access-control-allow-origin')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('versions', $data); + } + + /** + * @covers \Engelsystem\Controllers\Api\IndexController::indexV0 + * @covers \Engelsystem\Controllers\Api\IndexController::getApiSpecV0 + */ + public function testIndexV0(): void + { + $controller = new IndexController(new Response()); + $response = $controller->indexV0(); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('version', $data); + $this->assertArrayHasKey('paths', $data); + } + + /** + * @covers \Engelsystem\Controllers\Api\IndexController::openApiV0 + * @covers \Engelsystem\Controllers\Api\IndexController::getApiSpecV0 + */ + public function testOpenApiV0(): void + { + $controller = new IndexController(new Response()); + + $response = $controller->openApiV0(); + $this->validateApiResponse('/openapi', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('openapi', $data); + $this->assertArrayHasKey('info', $data); + } + + /** + * @covers \Engelsystem\Controllers\Api\IndexController::info + */ + public function testInfo(): void + { + $config = new Config(['name' => 'TestEvent', 'app_name' => 'TestSystem', 'timezone' => 'UTC']); + $this->app->instance('config', $config); + + $controller = new IndexController(new Response()); + + $response = $controller->info(); + $this->validateApiResponse('/info', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $data = $data['data']; + $this->assertArrayHasKey('api', $data); + $this->assertArrayHasKey('timezone', $data); + $this->assertEquals('UTC', $data['timezone']); + } + + /** + * @covers \Engelsystem\Controllers\Api\IndexController::info + */ + public function testInfoNotConfigured(): void + { + $config = new Config([]); + $this->app->instance('config', $config); + + $controller = new IndexController(new Response()); + + $response = $controller->info(); + $this->validateApiResponse('/info', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('name', $data['data']); + $this->assertEquals('', $data['data']['name']); + } + + /** + * @covers \Engelsystem\Controllers\Api\IndexController::options + */ + public function testOptions(): void + { + $controller = new IndexController(new Response()); + $response = $controller->options(); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertNotEmpty($response->getHeader('allow')); + $this->assertNotEmpty($response->getHeader('access-control-allow-headers')); + } + + /** + * @covers \Engelsystem\Controllers\Api\IndexController::notFound + */ + public function testNotFound(): void + { + $controller = new IndexController(new Response()); + $response = $controller->notFound(); + + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + } + + /** + * @covers \Engelsystem\Controllers\Api\IndexController::notImplemented + */ + public function testNotImplemented(): void + { + $controller = new IndexController(new Response()); + $response = $controller->notImplemented(); + + $this->assertEquals(405, $response->getStatusCode()); + $this->assertEquals(['GET'], $response->getHeader('allow')); + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + } +} diff --git a/tests/Unit/Controllers/Api/LocationsControllerTest.php b/tests/Unit/Controllers/Api/LocationsControllerTest.php new file mode 100644 index 000000000..951ec5205 --- /dev/null +++ b/tests/Unit/Controllers/Api/LocationsControllerTest.php @@ -0,0 +1,36 @@ +create(); + + $controller = new LocationsController(new Response()); + + $response = $controller->index(); + $this->validateApiResponse('/locations', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']); + $this->assertCount(1, collect($data['data'])->filter(function ($item) use ($items) { + return $item['name'] == $items->first()->getAttribute('name'); + })); + } +} diff --git a/tests/Unit/Controllers/Api/NewsControllerTest.php b/tests/Unit/Controllers/Api/NewsControllerTest.php new file mode 100644 index 000000000..b7ee50bd7 --- /dev/null +++ b/tests/Unit/Controllers/Api/NewsControllerTest.php @@ -0,0 +1,37 @@ +create(); + + $controller = new NewsController(new Response()); + + $response = $controller->index(); + $this->validateApiResponse('/news', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']); + + $this->assertCount(1, collect($data['data'])->filter(function ($item) use ($items) { + return $item['title'] == $items->first()->getAttribute('title'); + })); + } +} diff --git a/tests/Unit/Controllers/Api/Resources/BasicResourceTest.php b/tests/Unit/Controllers/Api/Resources/BasicResourceTest.php new file mode 100644 index 000000000..a1580f524 --- /dev/null +++ b/tests/Unit/Controllers/Api/Resources/BasicResourceTest.php @@ -0,0 +1,100 @@ +getModel(); + $resource = $this->getResource($model); + + $this->assertInstanceOf(Arrayable::class, $resource); + $this->assertEquals(['test' => 'value'], $resource->toArray()); + } + + /** + * @covers \Engelsystem\Controllers\Api\Resources\BasicResource::toJson + */ + public function testToJson(): void + { + $model = $this->getModel(); + $resource = $this->getResource($model); + + $this->assertInstanceOf(Jsonable::class, $resource); + $this->assertEquals('{"test":"value"}', $resource->toJson()); + } + + /** + * @covers \Engelsystem\Controllers\Api\Resources\BasicResource::toJson + */ + public function testToJsonOptions(): void + { + $resource = $this->getResource(new Collection()); + + $this->assertInstanceOf(Jsonable::class, $resource); + $this->assertEquals('{}', $resource->toJson(JSON_FORCE_OBJECT)); + } + + /** + * @covers \Engelsystem\Controllers\Api\Resources\BasicResource::__toString + */ + public function testToString(): void + { + $model = $this->getModel(); + $resource = $this->getResource($model); + + $this->assertInstanceOf(Stringable::class, $resource); + $this->assertEquals('{"test":"value"}', (string) $resource); + } + + /** + * @covers \Engelsystem\Controllers\Api\Resources\BasicResource::collection + */ + public function testCollection(): void + { + $resource = $this->getResource(new Collection()); + $modelA = $this->getModel(); + $modelB = $this->getModel()->setAttribute('test', 'B'); + $collection = $resource->collection([$modelA, $modelB]); + + $this->assertInstanceOf(Collection::class, $collection); + $this->assertCount(2, $collection); + + $this->assertInstanceOf(BasicResource::class, $collection->first()); + $this->assertEquals(['test' => 'value'], $collection->first()->toArray()); + + $this->assertInstanceOf(BasicResource::class, $collection->last()); + $this->assertEquals(['test' => 'B'], $collection->last()->toArray()); + } + + protected function getResource(BaseModel|Collection $model): BasicResource + { + return new class ($model) extends BasicResource { + }; + } + + + protected function getModel(): BaseModel + { + $model = new class extends BaseModel { + }; + $model->setAttribute('test', 'value'); + + return $model; + } +} diff --git a/tests/Unit/Controllers/Api/ShiftsControllerTest.php b/tests/Unit/Controllers/Api/ShiftsControllerTest.php new file mode 100644 index 000000000..5a07856aa --- /dev/null +++ b/tests/Unit/Controllers/Api/ShiftsControllerTest.php @@ -0,0 +1,282 @@ +withAttribute('location_id', $this->location->id); + + $controller = new ShiftsController(new Response()); + + $response = $controller->entriesByLocation($request); + $this->validateApiResponse('/locations/{id}/shifts', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(2, $data['data']); + + // First shift + $shiftAData = $data['data'][0]; + $this->assertEquals($this->shiftA->title, $shiftAData['title'], 'Title is equal'); + $this->assertEquals($this->location->id, $shiftAData['location']['id'], 'Same location'); + $this->assertEquals($this->shiftA->shiftType->id, $shiftAData['shift_type']['id'], 'Shift type equals'); + $this->assertCount(4, $shiftAData['entries']); + // Has users + $entriesA = collect($shiftAData['entries'])->sortBy('type.id'); + $entry = $entriesA[0]; + $this->assertCount(2, $entry['users']); + $this->assertEquals(2, $entry['needs']); + $user = $entry['users'][0]; + $this->assertEquals('/users?action=view&user_id=' . $user['id'], $user['url']); + $this->assertCount(0, $entriesA[1]['users']); + $this->assertCount(1, $entriesA[2]['users']); + $this->assertCount(1, $entriesA[3]['users']); + + // Second (empty) shift + $shiftBData = $data['data'][1]; + $this->assertEquals($this->shiftB->title, $shiftBData['title'], 'Title is equal'); + $this->assertEquals($this->location->id, $shiftBData['location']['id'], 'Same location'); + $this->assertEquals($this->shiftB->shiftType->id, $shiftBData['shift_type']['id'], 'Shift type equals'); + $this->assertCount(3, $shiftBData['entries']); + // No users + $entriesB = collect($shiftBData['entries'])->sortBy('type.id'); + $this->assertCount(0, $entriesB[0]['users']); + } + + /** + * @covers \Engelsystem\Controllers\Api\ShiftsController::entriesByAngeltype + * @covers \Engelsystem\Controllers\Api\ShiftsController::getNeededAngelTypes + */ + public function testEntriesViaShiftType(): void + { + $this->schedule->needed_from_shift_type = true; + $this->schedule->save(); + + /** @var ShiftEntry $firstEntry */ + $firstEntry = $this->shiftB->shiftEntries->first(); + + $request = new Request(); + $request = $request->withAttribute('angeltype_id', $firstEntry->angelType->id); + + $controller = new ShiftsController(new Response()); + + $response = $controller->entriesByAngeltype($request); + $this->validateApiResponse('/angeltypes/{id}/shifts', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(5, $data['data']); + + $shift = $data['data'][0]; + $this->assertTrue(count($shift['entries']) >= 1); + } + + /** + * @covers \Engelsystem\Controllers\Api\ShiftsController::entriesByAngeltype + */ + public function testEntriesByAngeltype(): void + { + /** @var ShiftEntry $firstEntry */ + $firstEntry = $this->shiftA->shiftEntries->first(); + + $request = new Request(); + $request = $request->withAttribute('angeltype_id', $firstEntry->angelType->id); + + $controller = new ShiftsController(new Response()); + + $response = $controller->entriesByAngeltype($request); + $this->validateApiResponse('/angeltypes/{id}/shifts', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(2, $data['data']); + + $shift = $data['data'][0]; + $this->assertTrue(count($shift['entries']) >= 1); + } + + /** + * @covers \Engelsystem\Controllers\Api\ShiftsController::entriesByUser + */ + public function testEntriesByUser(): void + { + /** @var ShiftEntry $firstEntry */ + $firstEntry = $this->shiftA->shiftEntries->first(); + + $request = new Request(); + $request = $request->withAttribute('user_id', $firstEntry->user->id); + + $controller = new ShiftsController(new Response()); + + $response = $controller->entriesByUser($request); + $this->validateApiResponse('/users/{id}/shifts', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertCount(1, $data['data']); + + $shift = $data['data'][0]; + $this->assertTrue(count($shift['entries']) >= 1); + } + + /** + * @covers \Engelsystem\Controllers\Api\ShiftsController::entriesByUser + */ + public function testEntriesByUserSelf(): void + { + $user = User::query()->first(); + + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + + $request = new Request(); + $request = $request->withAttribute('user_id', 'self'); + + $controller = new ShiftsController(new Response()); + $controller->setAuth($auth); + + $response = $controller->entriesByUser($request); + $this->validateApiResponse('/users/{id}/shifts', 'get', $response); + } + + /** + * @covers \Engelsystem\Controllers\Api\ShiftsController::entriesByUser + */ + public function testEntriesByUserNotFound(): void + { + $request = new Request(); + $request = $request->withAttribute('user_id', 42); + + $controller = new ShiftsController(new Response()); + + $this->expectException(ModelNotFoundException::class); + $controller->entriesByUser($request); + } + + public function setUp(): void + { + parent::setUp(); + + $this->location = Location::factory()->create(); + $this->schedule = Schedule::factory()->create(); + + // Shifts + $this->shiftA = Shift::factory(1) + ->create(['location_id' => $this->location->id, 'start' => Carbon::now()->subHour()]) + ->first(); + $this->shiftB = Shift::factory(1) + ->create(['location_id' => $this->location->id, 'start' => Carbon::now()->addHour()]) + ->first(); + (new ScheduleShift(['shift_id' => $this->shiftB->id, 'schedule_id' => $this->schedule->id, 'guid' => 'a'])) + ->save(); + + // "Empty" entry to be skipped + NeededAngelType::factory(1)->create(['location_id' => null, 'shift_id' => $this->shiftA->id, 'count' => 0]); + + // Needed entry by shift + /** @var NeededAngelType $byShift */ + $byShift = NeededAngelType::factory(2) + ->create(['location_id' => null, 'shift_type_id' => null, 'shift_id' => $this->shiftA->id, 'count' => 2]) + ->first(); + + // Needed entry by location + /** @var NeededAngelType $byLocation */ + $byLocation = NeededAngelType::factory(1) + ->create(['location_id' => $this->location->id, 'shift_type_id' => null, 'shift_id' => null, 'count' => 3]) + ->first(); + + // Needed entry by shift type + $shiftType = $this->shiftB->shiftType; + /** @var NeededAngelType $byShiftType */ + $byShiftType = NeededAngelType::factory(2) + ->create(['location_id' => null, 'shift_type_id' => $shiftType->id, 'count' => 3]) + ->first(); + ShiftEntry::factory(5)->create([ + 'shift_id' => $this->shiftB->id, + 'angel_type_id' => $byShiftType->angel_type_id, + ]); + + // Added by both + NeededAngelType::factory(1) + ->create([ + 'location_id' => $this->location->id, + 'shift_type_id' => null, + 'shift_id' => null, + 'angel_type_id' => $byShift->angel_type_id, + 'count' => 3, + ]) + ->first(); + + // By shift + ShiftEntry::factory(2)->create([ + 'shift_id' => $this->shiftA->id, + 'angel_type_id' => $byShift->angel_type_id, + ]); + + // By location + ShiftEntry::factory(1)->create([ + 'shift_id' => $this->shiftA->id, + 'angel_type_id' => $byLocation->angel_type_id, + ]); + + // Additional (not required by shift nor location) + ShiftEntry::factory(1)->create(['shift_id' => $this->shiftA->id]); + + $this->schedule->shiftType()->associate($shiftType); + + foreach (User::all() as $user) { + // Generate user data + /** @var User $user */ + PersonalData::factory()->create(['user_id' => $user->id]); + Contact::factory()->create(['user_id' => $user->id]); + } + } +} diff --git a/tests/Unit/Controllers/Api/Stub/UsesAuthImplementation.php b/tests/Unit/Controllers/Api/Stub/UsesAuthImplementation.php new file mode 100644 index 000000000..755578474 --- /dev/null +++ b/tests/Unit/Controllers/Api/Stub/UsesAuthImplementation.php @@ -0,0 +1,19 @@ +getUser($id); + } +} diff --git a/tests/Unit/Controllers/Api/UsersControllerTest.php b/tests/Unit/Controllers/Api/UsersControllerTest.php new file mode 100644 index 000000000..65371ed57 --- /dev/null +++ b/tests/Unit/Controllers/Api/UsersControllerTest.php @@ -0,0 +1,87 @@ +has(Contact::factory()) + ->has(PersonalData::factory()) + ->has(Settings::factory()) + ->has(State::factory()) + ->create(); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user, $this->atLeastOnce()); + + $request = new Request(); + $request = $request->withAttribute('user_id', 'self'); + + $controller = new UsersController(new Response()); + $controller->setAuth($auth); + + $response = $controller->user($request); + $this->validateApiResponse('/users/{id}', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('id', $data['data']); + $this->assertEquals($user->id, $data['data']['id']); + $this->assertArrayHasKey('dates', $data['data']); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsersController::user + */ + public function testUserById(): void + { + $user = User::factory()->create(); + $otherUser = User::factory() + ->has(Contact::factory()) + ->has(PersonalData::factory()) + ->has(Settings::factory()) + ->has(State::factory()) + ->create(); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user, $this->atLeastOnce()); + + $request = new Request(); + $request = $request->withAttribute('user_id', $otherUser->id); + + $controller = new UsersController(new Response()); + $controller->setAuth($auth); + + $response = $controller->user($request); + $this->validateApiResponse('/users/{id}', 'get', $response); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('id', $data['data']); + $this->assertEquals($otherUser->id, $data['data']['id']); + $this->assertArrayNotHasKey('dates', $data['data']); + } +} diff --git a/tests/Unit/Controllers/Api/UsesAuthServiceProviderTest.php b/tests/Unit/Controllers/Api/UsesAuthServiceProviderTest.php new file mode 100644 index 000000000..0e24aa371 --- /dev/null +++ b/tests/Unit/Controllers/Api/UsesAuthServiceProviderTest.php @@ -0,0 +1,36 @@ +app); + $serviceProvider->register(); + + $user = new User(); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + $this->app->instance(Authenticator::class, $auth); + + /** @var UsesAuthImplementation $instance */ + $instance = $this->app->make(UsesAuthImplementation::class); + + $this->assertEquals($user, $instance->user('self')); + } +} diff --git a/tests/Unit/Controllers/Api/UsesAuthTest.php b/tests/Unit/Controllers/Api/UsesAuthTest.php new file mode 100644 index 000000000..2ba4830f8 --- /dev/null +++ b/tests/Unit/Controllers/Api/UsesAuthTest.php @@ -0,0 +1,72 @@ +createInstance(); + + $this->expectException(ModelNotFoundException::class); + $usesAuth->user('self'); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsesAuth::getUser + */ + public function testGetUserNotFound(): void + { + $usesAuth = $this->createInstance(); + + $this->expectException(ModelNotFoundException::class); + $usesAuth->user(42); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsesAuth::getUser + */ + public function testGetUserWithoutAuth(): void + { + $user = User::factory()->create(); + + $usesAuth = $this->createInstance(); + + $this->assertInstanceOf(User::class, $usesAuth->user($user->id)); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsesAuth::setAuth + * @covers \Engelsystem\Controllers\Api\UsesAuth::getUser + */ + public function testGetUser(): void + { + $user = User::factory()->create(); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + + $usesAuth = $this->createInstance(); + $usesAuth->setAuth($auth); + + $this->assertEquals($user, $usesAuth->user('self')); + } + + protected function createInstance(): object + { + return new UsesAuthImplementation(new Response()); + } +} diff --git a/tests/Unit/Controllers/ApiControllerTest.php b/tests/Unit/Controllers/ApiControllerTest.php deleted file mode 100644 index fce58a7d1..000000000 --- a/tests/Unit/Controllers/ApiControllerTest.php +++ /dev/null @@ -1,27 +0,0 @@ -index(); - - $this->assertEquals(501, $response->getStatusCode()); - $this->assertEquals(['application/json'], $response->getHeader('content-type')); - $this->assertJson($response->getContent()); - } -} diff --git a/tests/Unit/Controllers/ControllerTest.php b/tests/Unit/Controllers/ControllerTest.php index fa428f86c..f6bf84472 100644 --- a/tests/Unit/Controllers/ControllerTest.php +++ b/tests/Unit/Controllers/ControllerTest.php @@ -17,6 +17,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\Test\TestLogger; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; abstract class ControllerTest extends TestCase @@ -78,6 +79,7 @@ public function setUp(): void $this->session = new Session(new MockArraySessionStorage()); $this->app->instance('session', $this->session); $this->app->instance(Session::class, $this->session); + $this->app->instance(SessionInterface::class, $this->session); $this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class); diff --git a/tests/Unit/Controllers/FeedControllerTest.php b/tests/Unit/Controllers/FeedControllerTest.php index ba3f9601d..3bedc68c6 100644 --- a/tests/Unit/Controllers/FeedControllerTest.php +++ b/tests/Unit/Controllers/FeedControllerTest.php @@ -7,6 +7,7 @@ use Engelsystem\Controllers\FeedController; use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Carbon; +use Engelsystem\Http\UrlGenerator; use Engelsystem\Models\News; use Engelsystem\Models\Shifts\ShiftEntry; use Engelsystem\Models\User\User; @@ -18,14 +19,16 @@ class FeedControllerTest extends ControllerTest { protected Authenticator|MockObject $auth; + protected UrlGenerator|MockObject $url; /** * @covers \Engelsystem\Controllers\FeedController::__construct * @covers \Engelsystem\Controllers\FeedController::atom + * @covers \Engelsystem\Controllers\FeedController::withEtag */ public function testAtom(): void { - $controller = new FeedController($this->auth, $this->request, $this->response); + $controller = new FeedController($this->auth, $this->request, $this->response, $this->url); $this->setExpects( $this->response, @@ -33,6 +36,12 @@ public function testAtom(): void ['content-type', 'application/atom+xml; charset=utf-8'], $this->response ); + $this->response->expects($this->once()) + ->method('setEtag') + ->willReturnCallback(function ($etag) { + $this->assertNotEmpty($etag); + return $this->response; + }); $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function ($view, $data) { @@ -50,7 +59,7 @@ public function testAtom(): void */ public function testRss(): void { - $controller = new FeedController($this->auth, $this->request, $this->response); + $controller = new FeedController($this->auth, $this->request, $this->response, $this->url); $this->setExpects( $this->response, @@ -58,6 +67,7 @@ public function testRss(): void ['content-type', 'application/rss+xml; charset=utf-8'], $this->response ); + $this->setExpects($this->response, 'setEtag', null, $this->response); $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function ($view, $data) { @@ -66,6 +76,7 @@ public function testRss(): void return $this->response; }); + $controller->rss(); } @@ -81,7 +92,7 @@ public function testIcal(): void new Session(new MockArraySessionStorage()), new User(), ); - $controller = new FeedController($this->auth, $this->request, $this->response); + $controller = new FeedController($this->auth, $this->request, $this->response, $this->url); /** @var User $user */ $user = User::factory()->create(['api_key' => 'fo0']); @@ -95,6 +106,8 @@ public function testIcal(): void ) ->willReturn($this->response); + $this->setExpects($this->response, 'setEtag', null, $this->response); + $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function ($view, $data) { @@ -109,6 +122,7 @@ public function testIcal(): void return $this->response; }); + $controller->ical(); } @@ -124,7 +138,8 @@ public function testShifts(): void new Session(new MockArraySessionStorage()), new User(), ); - $controller = new FeedController($this->auth, $this->request, $this->response); + $controller = new FeedController($this->auth, $this->request, $this->response, $this->url); + $this->setExpects($this->url, 'to', null, 'https://host/shift/1337', $this->atLeastOnce()); /** @var User $user */ $user = User::factory()->create(['api_key' => 'fo0']); @@ -137,6 +152,8 @@ public function testShifts(): void $this->response ); + $this->setExpects($this->response, 'setEtag', null, $this->response); + $this->response->expects($this->once()) ->method('withContent') ->willReturnCallback(function ($jsonData) { @@ -151,7 +168,8 @@ public function testShifts(): void [ 'name', 'title', 'description', 'Comment', - 'SID', 'shifttype_id', 'URL', + 'SID', 'shifttype_id', 'URL', 'link', + 'shifttype_name', 'shifttype_description', 'RID', 'Name', 'map_url', 'start', 'start_date', 'end', 'end_date', 'timezone', 'event_timezone', @@ -162,6 +180,7 @@ public function testShifts(): void return $this->response; }); + $controller->shifts(); } @@ -180,10 +199,11 @@ public function getNewsMeetingsDataProvider(): array */ public function testGetNewsMeetings(bool $isMeeting): void { - $controller = new FeedController($this->auth, $this->request, $this->response); + $controller = new FeedController($this->auth, $this->request, $this->response, $this->url); $this->request->attributes->set('meetings', $isMeeting); $this->setExpects($this->response, 'withHeader', null, $this->response); + $this->setExpects($this->response, 'setEtag', null, $this->response); $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function ($view, $data) use ($isMeeting) { @@ -207,9 +227,10 @@ public function testGetNewsMeetings(bool $isMeeting): void public function testGetNewsLimit(): void { News::query()->where('id', '<>', 1)->update(['updated_at' => Carbon::now()->subHour()]); - $controller = new FeedController($this->auth, $this->request, $this->response); + $controller = new FeedController($this->auth, $this->request, $this->response, $this->url); $this->setExpects($this->response, 'withHeader', null, $this->response); + $this->setExpects($this->response, 'setEtag', null, $this->response); $this->response->expects($this->once()) ->method('withView') ->willReturnCallback(function ($view, $data) { @@ -233,6 +254,7 @@ public function setUp(): void $this->config->set('display_news', 10); $this->auth = $this->createMock(Authenticator::class); + $this->url = $this->createMock(UrlGenerator::class); News::factory(7)->create(['is_meeting' => false]); News::factory(5)->create(['is_meeting' => true]); diff --git a/tests/Unit/Controllers/Metrics/ControllerTest.php b/tests/Unit/Controllers/Metrics/ControllerTest.php index 66bc2a9e1..a172e19d4 100644 --- a/tests/Unit/Controllers/Metrics/ControllerTest.php +++ b/tests/Unit/Controllers/Metrics/ControllerTest.php @@ -86,14 +86,14 @@ public function testMetrics(): void ->with('metrics return') ->willReturn($response); - $stats->expects($this->exactly(6)) + $stats->expects($this->exactly(8)) ->method('licenses') - ->withConsecutive(['has_car'], ['forklift'], ['car'], ['3.5t'], ['7.5t'], ['12t']) - ->willReturnOnConsecutiveCalls(6, 3, 15, 9, 7, 1); - $stats->expects($this->exactly(2)) - ->method('arrivedUsers') - ->withConsecutive([false], [true]) - ->willReturnOnConsecutiveCalls(7, 43); + ->withConsecutive(['has_car'], ['forklift'], ['car'], ['3.5t'], ['7.5t'], ['12t'], ['ifsg_light'], ['ifsg']) + ->willReturnOnConsecutiveCalls(6, 3, 15, 9, 7, 1, 5, 4); + $stats->expects($this->exactly(4)) + ->method('usersState') + ->withConsecutive([false, false], [true, false], [false], [true]) + ->willReturnOnConsecutiveCalls(7, 43, 42, 10); $stats->expects($this->exactly(2)) ->method('currentlyWorkingUsers') ->withConsecutive([false], [true]) @@ -123,7 +123,6 @@ public function testMetrics(): void [LogLevel::DEBUG] ) ->willReturnOnConsecutiveCalls(0, 1, 0, 5, 999, 4, 55, 3); - $this->setExpects($stats, 'newUsers', null, 9); $this->setExpects($stats, 'worklogSeconds', null, 39 * 60 * 60); $this->setExpects($stats, 'vouchers', null, 17); $this->setExpects($stats, 'tshirts', null, 3); @@ -192,7 +191,7 @@ public function testStats(): void $response->expects($this->once()) ->method('withContent') ->with(json_encode([ - 'user_count' => 13, + 'user_count' => 20, 'arrived_user_count' => 10, 'done_work_hours' => 99, 'users_in_action' => 5, @@ -210,8 +209,7 @@ public function testStats(): void ->method('workSeconds') ->with(true) ->willReturn((int) (60 * 60 * 99.47)); - $this->setExpects($stats, 'newUsers', null, 3); - $this->setExpects($stats, 'arrivedUsers', null, 10, $this->exactly(2)); + $this->setExpects($stats, 'usersState', null, 10, $this->exactly(3)); $this->setExpects($stats, 'currentlyWorkingUsers', null, 5); $controller = new Controller($response, $engine, $config, $request, $stats, $version); diff --git a/tests/Unit/Controllers/Metrics/StatsTest.php b/tests/Unit/Controllers/Metrics/StatsTest.php index 3dfd02182..fcfbcc084 100644 --- a/tests/Unit/Controllers/Metrics/StatsTest.php +++ b/tests/Unit/Controllers/Metrics/StatsTest.php @@ -6,6 +6,7 @@ use Carbon\Carbon; use Engelsystem\Controllers\Metrics\Stats; +use Engelsystem\Models\AngelType; use Engelsystem\Models\Faq; use Engelsystem\Models\LogEntry; use Engelsystem\Models\Message; @@ -13,9 +14,10 @@ use Engelsystem\Models\NewsComment; use Engelsystem\Models\OAuth; use Engelsystem\Models\Question; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; +use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\User\License; use Engelsystem\Models\User\PasswordReset; use Engelsystem\Models\User\PersonalData; @@ -32,21 +34,10 @@ class StatsTest extends TestCase { use HasDatabase; - /** - * @covers \Engelsystem\Controllers\Metrics\Stats::__construct - * @covers \Engelsystem\Controllers\Metrics\Stats::newUsers - */ - public function testNewUsers(): void - { - $this->addUsers(); - - $stats = new Stats($this->database); - $this->assertEquals(2, $stats->newUsers()); - } - /** * @covers \Engelsystem\Controllers\Metrics\Stats::vouchers * @covers \Engelsystem\Controllers\Metrics\Stats::vouchersQuery + * @covers \Engelsystem\Controllers\Metrics\Stats::__construct */ public function testVouchers(): void { @@ -142,6 +133,8 @@ public function testLicenses(): void $this->assertEquals(0, $stats->licenses('3.5t')); $this->assertEquals(0, $stats->licenses('7.5t')); $this->assertEquals(1, $stats->licenses('12t')); + $this->assertEquals(0, $stats->licenses('ifsg_light')); + $this->assertEquals(0, $stats->licenses('ifsg')); } /** @@ -196,17 +189,45 @@ public function testWorklogBuckets(): void } /** - * @covers \Engelsystem\Controllers\Metrics\Stats::rooms + * @covers \Engelsystem\Controllers\Metrics\Stats::locations + */ + public function testLocations(): void + { + (new Location(['name' => 'Location 1']))->save(); + (new Location(['name' => 'Second location']))->save(); + (new Location(['name' => 'Another location']))->save(); + (new Location(['name' => 'Old location']))->save(); + + $stats = new Stats($this->database); + $this->assertEquals(4, $stats->locations()); + } + + /** + * @covers \Engelsystem\Controllers\Metrics\Stats::angeltypes + */ + public function testAngeltypes(): void + { + (new AngelType(['name' => 'AngelType 1']))->save(); + (new AngelType(['name' => 'Second AngelType']))->save(); + (new AngelType(['name' => 'Another AngelType']))->save(); + (new AngelType(['name' => 'Old AngelType']))->save(); + + $stats = new Stats($this->database); + $this->assertEquals(4, $stats->angeltypes()); + } + + /** + * @covers \Engelsystem\Controllers\Metrics\Stats::shifttypes */ - public function testRooms(): void + public function testShifttypes(): void { - (new Room(['name' => 'Room 1']))->save(); - (new Room(['name' => 'Second room']))->save(); - (new Room(['name' => 'Another room']))->save(); - (new Room(['name' => 'Old room']))->save(); + (new ShiftType(['name' => 'ShiftType 1', 'description' => 'rtfm']))->save(); + (new ShiftType(['name' => 'Second ShiftType', 'description' => 'pebkac']))->save(); + (new ShiftType(['name' => 'Another ShiftType', 'description' => 'id10t error']))->save(); + (new ShiftType(['name' => 'Old ShiftType', 'description' => 'layer 8']))->save(); $stats = new Stats($this->database); - $this->assertEquals(4, $stats->rooms()); + $this->assertEquals(4, $stats->shifttypes()); } /** @@ -278,18 +299,33 @@ public function testQuestions(): void } /** - * @covers \Engelsystem\Controllers\Metrics\Stats::arrivedUsers + * @covers \Engelsystem\Controllers\Metrics\Stats::usersState */ - public function testArrivedUsers(): void + public function testUsersState(): void { $this->addUsers(); ShiftEntry::factory()->create(['user_id' => 3]); ShiftEntry::factory()->create(['user_id' => 4]); + ShiftEntry::factory()->create(['user_id' => 1]); + + $stats = new Stats($this->database); + $this->assertEquals(7, $stats->usersState()); + $this->assertEquals(5, $stats->usersState(false)); + $this->assertEquals(2, $stats->usersState(true)); + $this->assertEquals(2, $stats->usersState(null, false)); + $this->assertEquals(1, $stats->usersState(true, false)); + $this->assertEquals(1, $stats->usersState(false, false)); + } + + /** + * @covers \Engelsystem\Controllers\Metrics\Stats::usersInfo + */ + public function testUsersInfo(): void + { + $this->addUsers(); $stats = new Stats($this->database); - $this->assertEquals(7, $stats->arrivedUsers()); - $this->assertEquals(5, $stats->arrivedUsers(false)); - $this->assertEquals(2, $stats->arrivedUsers(true)); + $this->assertEquals(1, $stats->usersInfo()); } /** @@ -479,7 +515,7 @@ protected function addUsers(): void $this->addUser(['arrived' => 1], ['pronoun' => 'unicorn'], ['language' => 'lo_RM', 'email_shiftinfo' => true]); $this->addUser(['arrived' => 1, 'got_voucher' => 2], ['shirt_size' => 'XXL'], ['language' => 'lo_RM']); $this->addUser( - ['arrived' => 1, 'got_voucher' => 9, 'force_active' => true], + ['arrived' => 1, 'got_voucher' => 9, 'force_active' => true, 'user_info' => 'Info'], [], ['theme' => 1], ['drive_car' => true, 'drive_12t' => true] diff --git a/tests/Unit/Controllers/NewsControllerTest.php b/tests/Unit/Controllers/NewsControllerTest.php index cb6e5e15a..864335d18 100644 --- a/tests/Unit/Controllers/NewsControllerTest.php +++ b/tests/Unit/Controllers/NewsControllerTest.php @@ -39,17 +39,18 @@ class NewsControllerTest extends ControllerTest 'user_id' => 1, ], [ - 'title' => 'baz', - 'text' => 'baz', - 'is_meeting' => true, - 'is_pinned' => true, - 'is_important' => true, - 'user_id' => 1, + 'title' => 'baz', + 'text' => 'baz', + 'is_meeting' => true, + 'is_pinned' => true, + 'is_highlighted' => true, + 'user_id' => 1, ], [ 'title' => 'Lorem', 'text' => 'lorem', 'is_meeting' => false, + 'is_pinned' => false, 'user_id' => 1, ], [ @@ -63,6 +64,7 @@ class NewsControllerTest extends ControllerTest 'title' => 'Dolor', 'text' => 'test', 'is_meeting' => true, + 'is_pinned' => false, 'user_id' => 1, ], ]; @@ -104,8 +106,8 @@ function (string $page, array $data) use (&$n) { // Show meetings $this->assertTrue($data['only_meetings']); $this->assertTrue($news->isNotEmpty()); - $this->assertEquals(1, $data['pages']); - $this->assertEquals(1, $data['page']); + $this->assertEquals(2, $data['pages']); + $this->assertEquals(2, $data['page']); break; default: // No news found @@ -247,9 +249,6 @@ public function testDeleteCommentNotAllowed(): void { $this->request = $this->request->withAttribute('comment_id', 2)->withParsedBody(['delete' => '1']); - $this->addUser(1); - $this->addUser(2); - /** @var NewsController $controller */ $controller = $this->app->get(NewsController::class); $controller->setValidator($this->app->get(Validator::class)); @@ -266,8 +265,6 @@ public function testDeleteComment(): void $this->request = $this->request->withAttribute('comment_id', 1)->withParsedBody(['delete' => '1']); $this->setExpects($this->response, 'redirectTo', ['http://localhost/news/1'], $this->response); - $this->addUser(1); - /** @var NewsController $controller */ $controller = $this->app->get(NewsController::class); $controller->setValidator($this->app->get(Validator::class)); @@ -291,6 +288,9 @@ public function setUp(): void $this->auth = $this->createMock(Authenticator::class); $this->app->instance(Authenticator::class, $this->auth); + $this->addUser(1); + $this->addUser(2); + foreach ($this->data as $news) { (new News($news))->save(); } diff --git a/tests/Unit/Controllers/OAuthControllerTest.php b/tests/Unit/Controllers/OAuthControllerTest.php index 43976e988..fb0e5d2f3 100644 --- a/tests/Unit/Controllers/OAuthControllerTest.php +++ b/tests/Unit/Controllers/OAuthControllerTest.php @@ -116,7 +116,7 @@ public function testIndexArrive(): void ); $this->setExpects($provider, 'getResourceOwner', [$accessToken], $resourceOwner, $this->atLeastOnce()); - /** @var EventDispatcher|MockObject $event */ + /** @var EventDispatcher|MockObject $dispatcher */ $dispatcher = $this->createMock(EventDispatcher::class); $this->app->instance('events.dispatcher', $dispatcher); $this->setExpects($dispatcher, 'dispatch', ['oauth2.login'], $dispatcher, 4); @@ -408,7 +408,7 @@ public function testIndexRedirectRegister(): void $resourceOwner, 'getId', null, - 'ProVIdeR-User-IdenTifIer', // Case sensitive variation of existing entry + 'ProVIdeR-User-IdenTifIer', // case-sensitive variation of existing entry $this->atLeastOnce() ); $this->setExpects( @@ -459,15 +459,10 @@ public function testIndexRedirectRegister(): void $this->assertEquals(4242424242, $this->session->get('oauth2_expires_at')->unix()); $this->assertFalse($this->session->get('oauth2_enable_password')); $this->assertEquals(null, $this->session->get('oauth2_allow_registration')); - $this->assertEquals( - [ - 'name' => 'username', - 'email' => 'foo.bar@localhost', - 'first_name' => 'Foo', - 'last_name' => 'Bar', - ], - $this->session->get('form_data') - ); + $this->assertEquals($this->session->get('form-data-username'), 'username'); + $this->assertEquals($this->session->get('form-data-email'), 'foo.bar@localhost'); + $this->assertEquals($this->session->get('form-data-first_name'), 'Foo'); + $this->assertEquals($this->session->get('form-data-last_name'), 'Bar'); $this->config->set('registration_enabled', false); $this->expectException(HttpNotFound::class); @@ -508,7 +503,7 @@ public function testIndexRedirectRegisterNestedInfo(): void null, [ 'nested' => [ - 'id' => 'new-provider-user-identifier', + 'id' => 42, // new provider user identifier 'name' => 'testuser', 'email' => 'foo.bar@localhost', 'first' => 'Test', @@ -544,12 +539,10 @@ public function testIndexRedirectRegisterNestedInfo(): void $this->setExpects($controller, 'getProvider', ['testprovider'], $provider, $this->atLeastOnce()); $controller->index($request); - $this->assertEquals([ - 'email' => 'foo.bar@localhost', - 'name' => 'testuser', - 'first_name' => 'Test', - 'last_name' => 'Tester', - ], $this->session->get('form_data')); + $this->assertEquals($this->session->get('form-data-username'), 'testuser'); + $this->assertEquals($this->session->get('form-data-email'), 'foo.bar@localhost'); + $this->assertEquals($this->session->get('form-data-first_name'), 'Test'); + $this->assertEquals($this->session->get('form-data-last_name'), 'Tester'); } @@ -621,7 +614,7 @@ protected function getMock(array $mockMethods = []): OAuthController } /** - * Setup the DB + * Set up the DB */ public function setUp(): void { diff --git a/tests/Unit/Controllers/PasswordResetControllerTest.php b/tests/Unit/Controllers/PasswordResetControllerTest.php index c653f3c0e..d714177cc 100644 --- a/tests/Unit/Controllers/PasswordResetControllerTest.php +++ b/tests/Unit/Controllers/PasswordResetControllerTest.php @@ -15,6 +15,7 @@ use Engelsystem\Http\Response; use Engelsystem\Http\Validation\Validator; use Engelsystem\Mail\EngelsystemMailer; +use Engelsystem\Models\Session as SessionModel; use Engelsystem\Models\User\PasswordReset; use Engelsystem\Models\User\User; use Engelsystem\Renderer\Renderer; @@ -147,6 +148,8 @@ public function testPostResetPassword(): void ['password' => $password, 'password_confirmation' => $password], ['token' => $token->token] ); + SessionModel::factory()->create(); // Some other session + SessionModel::factory(3)->create(['user_id' => $user->id]); $controller = $this->getController( 'pages/password/reset-success', @@ -162,6 +165,12 @@ public function testPostResetPassword(): void $this->assertEmpty((new PasswordReset())->find($user->id)); $this->assertNotNull(auth()->authenticate($user->name, $password)); $this->assertHasNoNotifications(); + + $this->assertEmpty( + SessionModel::whereUserId($user->id)->get(), + 'All user sessions should be deleted after successful password reset' + ); + $this->assertCount(1, SessionModel::all()); // Another session should be still there } /** diff --git a/tests/Unit/Controllers/RegistrationControllerTest.php b/tests/Unit/Controllers/RegistrationControllerTest.php new file mode 100644 index 000000000..85e88c254 --- /dev/null +++ b/tests/Unit/Controllers/RegistrationControllerTest.php @@ -0,0 +1,225 @@ +mockTranslator(); + + $this->authenticator = $this->getMockBuilder(Authenticator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->app->instance(Authenticator::class, $this->authenticator); + $this->app->alias(Authenticator::class, 'authenticator'); + + $this->userFactory = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->getMock(); + $this->config->set('oauth', []); + $this->app->instance(User::class, $this->userFactory); + $this->subject = $this->app->make(RegistrationController::class); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testSave(): void + { + $this->setPasswordRegistrationEnabledConfig(); + + $userData = ['user' => 'data']; + $request = $this->request->withParsedBody($userData); + + // Assert the controller passes the submitted data to the user factory + $this->userFactory + ->expects(self::once()) + ->method('createFromData') + ->with($userData) + ->willReturn(new EngelsystemUser()); + + // Assert that the user is redirected to home + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/', 302); + + $this->subject->save($request); + + // Assert that the success notification is there + self::assertEquals( + [ + 'messages.message' => ['registration.successful'], + ], + $this->session->all() + ); + + // Assert that "show_welcome" is not set in session, + // because "welcome_msg" is not configured. + $this->assertFalse($this->session->has('show_welcome')); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testSaveAlreadyLoggedIn(): void + { + $this->setPasswordRegistrationEnabledConfig(); + $request = $this->request->withParsedBody(['user' => 'data']); + + // Fake logged in user + $this->authenticator->method('user')->willReturn(new EngelsystemUser()); + + // Assert that the user is redirected to /register again + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/register', 302); + + $this->subject->save($request); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testSaveOAuth(): void + { + $this->setPasswordRegistrationEnabledConfig(); + + $userData = ['user' => 'data']; + $request = $this->request->withParsedBody($userData); + + $user = (new EngelsystemUser())->factory()->create(); + $oauth = (new OAuth())->factory()->create([ + 'user_id' => $user->id, + ]); + + $this->userFactory + ->expects(self::once()) + ->method('createFromData') + ->with($userData) + ->willReturn($user); + + // Assert that the user is redirected to the OAuth login + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/oauth/' . $oauth->provider, 302); + + $this->subject->save($request); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testSaveWithWelcomeMesssage(): void + { + $this->setPasswordRegistrationEnabledConfig(); + $this->config->set('welcome_msg', true); + + $userData = ['user' => 'data']; + $request = $this->request->withParsedBody($userData); + $this->subject->save($request); + + // Assert that "show_welcome" is set in session, + // because "welcome_msg" is enabled in the config. + $this->assertTrue($this->session->get('show_welcome')); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testSaveRegistrationDisabled(): void + { + $this->config->set('registration_enabled', false); + $request = $this->request->withParsedBody([]); + + // Assert the controller does not call createFromData + $this->userFactory + ->expects(self::never()) + ->method('createFromData'); + + // Assert that the user is redirected to home + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/', 302); + + $this->subject->save($request); + + // Assert that the error notification is there + self::assertEquals( + [ + 'messages.information' => ['registration.disabled'], + ], + $this->session->all() + ); + } + + /** + * @covers \Engelsystem\Controllers\RegistrationController + */ + public function testViewRegistrationDisabled(): void + { + $this->config->set('registration_enabled', false); + $request = $this->request->withParsedBody([]); + + // Assert the controller does not call createFromData + $this->userFactory + ->expects(self::never()) + ->method('createFromData'); + + // Assert that the user is redirected to home + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/', 302); + + $this->subject->view($request); + + // Assert that the error notification is there + self::assertEquals( + [ + 'messages.information' => ['registration.disabled'], + ], + $this->session->all() + ); + } + + private function setPasswordRegistrationEnabledConfig(): void + { + $this->config->set('registration_enabled', true); + $this->authenticator + ->method('can') + ->with('register') + ->willReturn(true); + $this->userFactory->method('determineIsPasswordEnabled') + ->willReturn(true); + } +} diff --git a/tests/Unit/Controllers/SettingsControllerTest.php b/tests/Unit/Controllers/SettingsControllerTest.php index 847e170ab..af961809c 100644 --- a/tests/Unit/Controllers/SettingsControllerTest.php +++ b/tests/Unit/Controllers/SettingsControllerTest.php @@ -10,8 +10,13 @@ use Engelsystem\Controllers\NotificationType; use Engelsystem\Controllers\SettingsController; use Engelsystem\Http\Exceptions\HttpNotFound; +use Engelsystem\Http\Redirector; use Engelsystem\Http\Response; +use Engelsystem\Models\AngelType; +use Engelsystem\Models\Session as SessionModel; +use Engelsystem\Models\User\License; use Engelsystem\Models\User\Settings; +use Illuminate\Support\Str; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\HttpFoundation\Session\Session; use Engelsystem\Helpers\Authenticator; @@ -31,6 +36,10 @@ class SettingsControllerTest extends ControllerTest protected SettingsController $controller; + protected SessionModel $currentSession; + protected SessionModel $secondSession; + protected SessionModel $otherSession; + protected function setUpProfileTest(): array { $body = [ @@ -81,7 +90,7 @@ protected function setUpProfileTest(): array */ public function testProfile(): void { - $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); /** @var Response|MockObject $response */ $this->response->expects($this->once()) ->method('withView') @@ -99,6 +108,7 @@ public function testProfile(): void /** * @covers \Engelsystem\Controllers\SettingsController::saveProfile * @covers \Engelsystem\Controllers\SettingsController::getSaveProfileRules + * @covers \Engelsystem\Controllers\SettingsController::isRequired */ public function testSaveProfile(): void { @@ -277,6 +287,13 @@ public function testSavePassword(): void $session = $this->app->get('session'); $messages = $session->get('messages.' . NotificationType::MESSAGE->value); $this->assertEquals('settings.password.success', $messages[0]); + + $this->assertCount( + 1, + SessionModel::whereUserId($this->user->id)->get(), + 'All other user sessions should be deleted after setting a new password' + ); + $this->assertCount(2, SessionModel::all()); // Current session and another one should be still there } /** @@ -375,8 +392,6 @@ public function savePasswordValidationProvider(): array /** * @covers \Engelsystem\Controllers\SettingsController::savePassword * @dataProvider savePasswordValidationProvider - * @param string $new_password - * @param string $new_password2 */ public function testSavePasswordValidation( ?string $password, @@ -404,7 +419,7 @@ public function testSavePasswordValidation( */ public function testThemeUnderNormalConditionReturnsCorrectViewAndData(): void { - $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); /** @var Response|MockObject $response */ $this->response->expects($this->once()) @@ -475,7 +490,7 @@ public function testSaveThemeWithKnownSelectedThemeGivenSavesThemeAndRedirect(): */ public function testLanguageUnderNormalConditionReturnsCorrectViewAndData(): void { - $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); /** @var Response|MockObject $response */ $this->response->expects($this->once()) @@ -575,6 +590,375 @@ public function testOauthNotConfigured(): void $this->controller->oauth(); } + /** + * @covers \Engelsystem\Controllers\SettingsController::sessions + */ + public function testSessions(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/settings/sessions', $view); + + $this->assertArrayHasKey('sessions', $data); + $this->assertCount(3, $data['sessions']); + + $this->assertArrayHasKey('current_session', $data); + $this->assertEquals($this->currentSession->id, $data['current_session']); + + return $this->response; + }); + + $this->controller->sessions(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDelete(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', ['http://localhost/settings/sessions'], $this->response); + + // Delete old user session + $this->request = $this->request->withParsedBody(['id' => Str::substr($this->secondSession->id, 0, 15)]); + $this->controller->sessionsDelete($this->request); + + $this->assertHasNotification('settings.sessions.delete_success'); + $this->assertCount(3, SessionModel::all()); + $this->assertNull(SessionModel::find($this->secondSession->id)); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDeleteActiveSession(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', null, $this->response); + + // Delete active user session + $this->request = $this->request->withParsedBody(['id' => Str::substr($this->currentSession->id, 0, 15)]); + $this->controller->sessionsDelete($this->request); + + $this->assertCount(4, SessionModel::all()); // None got deleted + $this->assertNotNull(SessionModel::find($this->currentSession->id)); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDeleteOtherUsersSession(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', null, $this->response); + + // Delete another users session + $this->request = $this->request->withParsedBody(['id' => Str::substr($this->otherSession->id, 0, 15)]); + $this->controller->sessionsDelete($this->request); + + $this->assertCount(4, SessionModel::all()); // None got deleted + $this->assertNotNull(SessionModel::find($this->otherSession->id)); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDeleteAllSessions(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', null, $this->response); + + // Delete all other user sessions + $this->request = $this->request->withParsedBody(['id' => 'all']); + $this->controller->sessionsDelete($this->request); + + $this->assertCount(2, SessionModel::all()); // Two got deleted + $this->assertNotNull(SessionModel::find($this->currentSession->id)); + $this->assertNull(SessionModel::find($this->secondSession->id)); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::__construct + * @covers \Engelsystem\Controllers\SettingsController::certificate + */ + public function testCertificateIfsg(): void + { + config(['ifsg_enabled' => true, 'ifsg_light_enabled' => true]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/settings/certificates', $view); + $this->assertArrayHasKey('certificates', $data); + $this->assertEquals($this->user->license, $data['certificates']); + return $this->response; + }); + + $this->controller->certificate(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::certificate + */ + public function testCertificateIfsgNotConfigured(): void + { + config(['ifsg_enabled' => false]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $this->expectException(HttpNotFound::class); + $this->controller->certificate(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::certificate + */ + public function testCertificateDrivingLicense(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_driver_license' => true]); + $this->user->userAngelTypes()->attach($angelType); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/settings/certificates', $view); + $this->assertArrayHasKey('certificates', $data); + $this->assertEquals($this->user->license, $data['certificates']); + return $this->response; + }); + + $this->controller->certificate(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::saveIfsgCertificate + */ + public function testSaveIfsgCertificateNotConfigured(): void + { + config(['ifsg_enabled' => false]); + + $this->expectException(HttpNotFound::class); + $this->controller->saveIfsgCertificate($this->request); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::saveIfsgCertificate + */ + public function testSaveIfsgCertificateLight(): void + { + config(['ifsg_enabled' => true, 'ifsg_light_enabled' => true]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + + $body = [ + 'ifsg_certificate_light' => true, + ]; + $this->request = $this->request->withParsedBody($body); + + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/settings/certificates') + ->willReturn($this->response); + + $this->controller->saveIfsgCertificate($this->request); + + $this->assertEquals(true, $this->user->license->ifsg_certificate_light); + $this->assertEquals(false, $this->user->license->ifsg_certificate); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::saveIfsgCertificate + */ + public function testSaveIfsgCertificateLightWhileDisabled(): void + { + config(['ifsg_enabled' => true, 'ifsg_light_enabled' => false]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->user->license->ifsg_certificate_light = false; + $this->user->license->save(); + + $body = [ + 'ifsg_certificate_light' => true, + ]; + $this->request = $this->request->withParsedBody($body); + + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/settings/certificates') + ->willReturn($this->response); + + $this->controller->saveIfsgCertificate($this->request); + + $this->assertEquals(false, $this->user->license->ifsg_certificate_light); + $this->assertEquals(false, $this->user->license->ifsg_certificate); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::saveIfsgCertificate + */ + public function testSaveIfsgCertificate(): void + { + config(['ifsg_enabled' => true]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + + $body = [ + 'ifsg_certificate' => true, + ]; + $this->request = $this->request->withParsedBody($body); + + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/settings/certificates') + ->willReturn($this->response); + + $this->controller->saveIfsgCertificate($this->request); + + $this->assertEquals(false, $this->user->license->ifsg_certificate_light); + $this->assertEquals(true, $this->user->license->ifsg_certificate); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::saveIfsgCertificate + */ + public function testSaveIfsgCertificateBoth(): void + { + config(['ifsg_enabled' => true]); + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + + $body = [ + 'ifsg_certificate_light' => true, + 'ifsg_certificate' => true, + ]; + $this->request = $this->request->withParsedBody($body); + + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/settings/certificates') + ->willReturn($this->response); + + $this->controller->saveIfsgCertificate($this->request); + + $this->assertEquals(false, $this->user->license->ifsg_certificate_light); + $this->assertEquals(true, $this->user->license->ifsg_certificate); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::saveDrivingLicense + * @covers \Engelsystem\Controllers\SettingsController::checkDrivingLicense + */ + public function testSaveDrivingLicense(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + $angelType = AngelType::factory()->create(['requires_driver_license' => true]); + $this->user->userAngelTypes()->attach($angelType); + + $body = [ + 'has_car' => true, + 'drive_forklift' => true, + 'drive_12t' => true, + ]; + $this->request = $this->request->withParsedBody($body); + + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/settings/certificates') + ->willReturn($this->response); + + $this->controller->saveDrivingLicense($this->request); + + $this->assertTrue($this->user->license->has_car); + $this->assertTrue($this->user->license->drive_forklift); + $this->assertTrue($this->user->license->drive_12t); + $this->assertFalse($this->user->license->drive_car); + $this->assertFalse($this->user->license->drive_3_5t); + $this->assertFalse($this->user->license->drive_7_5t); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::saveDrivingLicense + */ + public function testSaveDrivingLicenseNotAvailable(): void + { + $this->expectException(HttpNotFound::class); + $this->controller->saveDrivingLicense($this->request); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::api + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testApi(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + /** @var Response|MockObject $response */ + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/settings/api', $view); + $this->assertArrayHasKey('settings_menu', $data); + return $this->response; + }); + + $this->controller = $this->app->make(SettingsController::class); + $this->controller->api(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::apiKeyReset + */ + public function testApiKeyReset(): void + { + $redirector = $this->createMock(Redirector::class); + $this->app->instance(Redirector::class, $redirector); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + $this->setExpects($this->auth, 'resetApiKey', [$this->user], null, $this->atLeastOnce()); + $this->setExpects($redirector, 'back', null, $this->response); + + $this->controller = $this->app->make(SettingsController::class); + $this->controller->apiKeyReset(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuProfile(): void + { + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/profile', $menu); + $this->assertEquals('settings.profile', $menu['http://localhost/settings/profile']); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuPassword(): void + { + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/password', $menu); + $this->assertEquals( + ['title' => 'settings.password', 'icon' => 'key-fill'], + $menu['http://localhost/settings/password'] + ); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuLanguage(): void + { + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/language', $menu); + $this->assertEquals( + ['title' => 'settings.language', 'icon' => 'translate'], + $menu['http://localhost/settings/language'] + ); + } + /** * @covers \Engelsystem\Controllers\SettingsController::settingsMenu * @covers \Engelsystem\Controllers\SettingsController::checkOauthHidden @@ -585,22 +969,15 @@ public function testSettingsMenuWithOAuth(): void $providersHidden = ['foo' => ['lorem' => 'ipsum', 'hidden' => true]]; config(['oauth' => $providers]); - $this->assertEquals([ - 'http://localhost/settings/profile' => 'settings.profile', - 'http://localhost/settings/password' => 'settings.password', - 'http://localhost/settings/language' => 'settings.language', - 'http://localhost/settings/theme' => 'settings.theme', - 'http://localhost/settings/oauth' => ['title' => 'settings.oauth', 'hidden' => false], - ], $this->controller->settingsMenu()); + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/oauth', $menu); + $this->assertEquals(['title' => 'settings.oauth', 'hidden' => false], $menu['http://localhost/settings/oauth']); config(['oauth' => $providersHidden]); - $this->assertEquals([ - 'http://localhost/settings/profile' => 'settings.profile', - 'http://localhost/settings/password' => 'settings.password', - 'http://localhost/settings/language' => 'settings.language', - 'http://localhost/settings/theme' => 'settings.theme', - 'http://localhost/settings/oauth' => ['title' => 'settings.oauth', 'hidden' => true], - ], $this->controller->settingsMenu()); + + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/oauth', $menu); + $this->assertEquals(['title' => 'settings.oauth', 'hidden' => true], $menu['http://localhost/settings/oauth']); } /** @@ -610,12 +987,55 @@ public function testSettingsMenuWithoutOAuth(): void { config(['oauth' => []]); - $this->assertEquals([ - 'http://localhost/settings/profile' => 'settings.profile', - 'http://localhost/settings/password' => 'settings.password', - 'http://localhost/settings/language' => 'settings.language', - 'http://localhost/settings/theme' => 'settings.theme', - ], $this->controller->settingsMenu()); + $menu = $this->controller->settingsMenu(); + $this->assertArrayNotHasKey('http://localhost/settings/oauth', $menu); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + * @covers \Engelsystem\Controllers\SettingsController::checkOauthHidden + */ + public function testSettingsMenuWithIfsg(): void + { + config(['ifsg_enabled' => true]); + + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/certificates', $menu); + $this->assertEquals( + ['title' => 'settings.certificates', 'icon' => 'card-checklist'], + $menu['http://localhost/settings/certificates'] + ); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuWithoutIfsg(): void + { + config(['ifsg_enabled' => false]); + + $menu = $this->controller->settingsMenu(); + $this->assertArrayNotHasKey('http://localhost/settings/certificates', $menu); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuApi(): void + { + $this->setExpects($this->auth, 'can', ['api'], true, $this->atLeastOnce()); + + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/profile', $menu); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuApiNotAvailable(): void + { + $menu = $this->controller->settingsMenu(); + $this->assertArrayNotHasKey('http://localhost/settings/api', $menu); } /** @@ -634,12 +1054,21 @@ public function setUp(): void 'de_DE' => 'Deutsch', ]; $tshirt_sizes = ['S' => 'Small']; + $requiredFields = [ + 'pronoun' => false, + 'firstname' => false, + 'lastname' => false, + 'tshirt_size' => true, + 'mobile' => false, + 'dect' => false, + ]; $this->config = new Config([ 'min_password_length' => 6, 'themes' => $themes, 'locales' => $languages, 'tshirt_sizes' => $tshirt_sizes, 'goodie_type' => GoodieType::Goodie->value, + 'required_user_fields' => $requiredFields, ]); $this->app->instance('config', $this->config); $this->app->instance(Config::class, $this->config); @@ -656,8 +1085,18 @@ public function setUp(): void 'email_goody' => false, 'mobile_show' => false, ])) + ->has(License::factory()) ->create(); + $this->setExpects($this->auth, 'user', null, $this->user, $this->any()); + + // Create 4 sessions, 3 for the active user + $this->otherSession = SessionModel::factory()->create()->first(); // Other users sessions + $sessions = SessionModel::factory(3)->create(['user_id' => $this->user->id]); + $this->currentSession = $sessions->first(); + $this->secondSession = $sessions->last(); + $this->session->setId($this->currentSession->id); + $this->controller = $this->app->make(SettingsController::class); $this->controller->setValidator(new Validator()); } diff --git a/tests/Unit/Controllers/ShiftsControllerTest.php b/tests/Unit/Controllers/ShiftsControllerTest.php new file mode 100644 index 000000000..336f98882 --- /dev/null +++ b/tests/Unit/Controllers/ShiftsControllerTest.php @@ -0,0 +1,122 @@ +createModels(); + + $this->setExpects($this->redirect, 'to', ['http://localhost/shifts'], $this->response); + $this->setExpects($this->auth, 'user', null, $this->user); + + $controller = new ShiftsController($this->auth, $this->redirect, $this->url); + + $return = $controller->random(); + $this->assertEquals($this->response, $return); + $this->assertHasNotification('notification.shift.no_next_found', NotificationType::WARNING); + } + + /** + * @covers \Engelsystem\Controllers\ShiftsController::random + * @covers \Engelsystem\Controllers\ShiftsController::getNextFreeShifts + * @covers \Engelsystem\Controllers\ShiftsController::queryShiftEntries + */ + public function testRandom(): void + { + $this->createModels(); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + $start = Carbon::now()->addHour(); + + $otherUser = User::factory()->create(); + [$userAngelType, $otherAngelType] = AngelType::factory(2)->create(); + [$possibleShift1, $possibleShift2, $otherAngelTypeShift, $alreadySubscribedShift] = Shift::factory(4) + ->create(['start' => $start]); + $this->user->userAngelTypes()->attach($userAngelType, ['confirm_user_id' => $this->user->id]); + NeededAngelType::factory()->create([ + 'shift_id' => $possibleShift1->id, + 'angel_type_id' => $userAngelType->id, + 'count' => 2, + ]); + NeededAngelType::factory()->create([ + 'shift_id' => $possibleShift2->id, + 'angel_type_id' => $userAngelType->id, + 'count' => 1, + ]); + NeededAngelType::factory()->create([ + 'shift_id' => $otherAngelTypeShift->id, + 'angel_type_id' => $otherAngelType->id, + 'count' => 3, + ]); + ShiftEntry::factory()->create([ + 'shift_id' => $alreadySubscribedShift->id, + 'angel_type_id' => $userAngelType->id, + 'user_id' => $this->user->id, + ]); + + $otherUser->userAngelTypes()->attach($userAngelType, ['confirm_user_id' => $otherUser->id]); + ShiftEntry::factory()->create([ + 'shift_id' => $possibleShift1->id, + 'angel_type_id' => $userAngelType->id, + 'user_id' => $otherUser, + ]); + + $this->redirect->expects($this->exactly(10)) + ->method('to') + ->willReturnCallback(function (string $url) use ($possibleShift1, $possibleShift2) { + parse_str(parse_url($url)['query'] ?? '', $parameters); + $this->assertTrue(Str::startsWith($url, 'http://localhost/shifts')); + $this->assertArrayHasKey('shift_id', $parameters); + $shiftId = $parameters['shift_id'] ?? 0; + $this->assertTrue(in_array($shiftId, [$possibleShift1->id, $possibleShift2->id])); + return $this->response; + }); + + $controller = new ShiftsController($this->auth, $this->redirect, $this->url); + + $return = $controller->random(); + $this->assertEquals($this->response, $return); + + // Try multiple times + for ($i = 1; $i < 10; $i++) { + $controller->random(); + } + } + + protected function createModels(): void + { + $this->user = User::factory()->create(); + + $this->auth = $this->createMock(Authenticator::class); + + $this->redirect = $this->createMock(Redirector::class); + + $this->url = $this->app->make(UrlGeneratorInterface::class); + } +} diff --git a/tests/Unit/Controllers/Stub/ControllerImplementation.php b/tests/Unit/Controllers/Stub/ControllerImplementation.php index dfdd0457e..c3a6af55c 100644 --- a/tests/Unit/Controllers/Stub/ControllerImplementation.php +++ b/tests/Unit/Controllers/Stub/ControllerImplementation.php @@ -8,7 +8,7 @@ class ControllerImplementation extends BaseController { - /** @var array */ + /** @var string[]|string[][] */ protected array $permissions = [ 'foo', 'lorem' => [ diff --git a/tests/Unit/Events/EventDispatcherTest.php b/tests/Unit/Events/EventDispatcherTest.php index 2b64fd808..f0521c9ea 100644 --- a/tests/Unit/Events/EventDispatcherTest.php +++ b/tests/Unit/Events/EventDispatcherTest.php @@ -148,4 +148,11 @@ public function handle(): array { return ['default' => 'handler']; } + + public function setUp(): void + { + parent::setUp(); + + $this->firedEvents = []; + } } diff --git a/tests/Unit/Events/Listener/MessagesTest.php b/tests/Unit/Events/Listener/MessagesTest.php index c7ae7a669..502269d4b 100644 --- a/tests/Unit/Events/Listener/MessagesTest.php +++ b/tests/Unit/Events/Listener/MessagesTest.php @@ -14,7 +14,6 @@ use Engelsystem\Test\Unit\TestCase; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\Test\TestLogger; -use Symfony\Component\Mailer\Exception\TransportException; class MessagesTest extends TestCase { @@ -46,13 +45,14 @@ public function testCreated(): void string $subject, string $template, array $data - ) use ($user): void { + ) use ($user): bool { $this->assertEquals($user->id, $receiver->id); $this->assertEquals('notification.messages.new', $subject); $this->assertEquals('emails/messages-new', $template); $this->assertArrayHasKey('username', $data); $this->assertArrayHasKey('sender', $data); $this->assertArrayHasKey('send_message', $data); + return true; }); $handler = new Messages($this->log, $mailer); @@ -79,32 +79,6 @@ public function testCreatedNoEmail(): void $handler->created($message); } - /** - * @covers \Engelsystem\Events\Listener\Messages::sendMail - */ - public function testSendMailExceptionHandling(): void - { - /** @var EngelsystemMailer|MockObject $mailer */ - $mailer = $this->createMock(EngelsystemMailer::class); - /** @var User $user */ - $user = User::factory() - ->has(Settings::factory([ - 'email_messages' => true, - ])) - ->create(); - $message = Message::factory()->create(['receiver_id' => $user->id]); - $mailer->expects($this->once()) - ->method('sendViewTranslated') - ->willReturnCallback(function (): void { - throw new TransportException(); - }); - - $handler = new Messages($this->log, $mailer); - - $handler->created($message); - $this->assertTrue($this->log->hasErrorThatContains('Unable to send email')); - } - protected function setUp(): void { $this->log = new TestLogger(); diff --git a/tests/Unit/Events/Listener/NewsTest.php b/tests/Unit/Events/Listener/NewsTest.php index 80664fa9d..ff22be833 100644 --- a/tests/Unit/Events/Listener/NewsTest.php +++ b/tests/Unit/Events/Listener/NewsTest.php @@ -15,7 +15,6 @@ use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Psr\Log\Test\TestLogger; -use Symfony\Component\Mailer\Exception\TransportException; class NewsTest extends TestCase { @@ -23,7 +22,9 @@ class NewsTest extends TestCase protected TestLogger $log; - protected EngelsystemMailer|MockObject $mailer; + protected EngelsystemMailer | MockObject $mailer; + + protected NewsModel $news; protected User $user; @@ -34,33 +35,68 @@ class NewsTest extends TestCase */ public function testCreated(): void { - $this->app->instance('config', new Config()); - /** @var NewsModel $news */ - $news = NewsModel::factory(['title' => 'Foo'])->create(); - - $i = 0; - $this->mailer->expects($this->exactly(2)) + $this->mailer->expects($this->once()) ->method('sendViewTranslated') - ->willReturnCallback(function (User $user, string $subject, string $template, array $data) use (&$i): void { - $this->assertEquals(1, $user->id); + ->willReturnCallback(function (User $user, string $subject, string $template, array $data): bool { + $this->assertEquals($this->user->id, $user->id); $this->assertEquals('notification.news.new', $subject); $this->assertEquals('emails/news-new', $template); $this->assertEquals('Foo', array_values($data)[0]); - if ($i++ > 0) { // On second run - throw new TransportException('Oops'); - } + return true; + }); + + /** @var News $listener */ + $listener = $this->app->make(News::class); + $listener->created($this->news); + } + + /** + * @covers \Engelsystem\Events\Listener\News::created + * @covers \Engelsystem\Events\Listener\News::sendMail + */ + public function testCreatedNoNotification(): void + { + $this->setExpects($this->mailer, 'sendViewTranslated', null, null, $this->never()); + + /** @var News $listener */ + $listener = $this->app->make(News::class); + $listener->created($this->news, false); + } + + /** + * @covers \Engelsystem\Events\Listener\News::updated + * @covers \Engelsystem\Events\Listener\News::sendMail + */ + public function testUpdated(): void + { + $this->mailer->expects($this->once()) + ->method('sendViewTranslated') + ->willReturnCallback(function (User $user, string $subject, string $template, array $data): bool { + $this->assertEquals($this->user->id, $user->id); + $this->assertEquals('notification.news.updated', $subject); + $this->assertEquals('emails/news-updated', $template); + $this->assertEquals('Foo', array_values($data)[0]); + + return true; }); /** @var News $listener */ $listener = $this->app->make(News::class); - $error = 'Unable to send email'; + $listener->updated($this->news); + } - $listener->created($news); - $this->assertFalse($this->log->hasErrorThatContains($error)); + /** + * @covers \Engelsystem\Events\Listener\News::updated + * @covers \Engelsystem\Events\Listener\News::sendMail + */ + public function testUpdatedNoNotification(): void + { + $this->setExpects($this->mailer, 'sendViewTranslated', null, null, $this->never()); - $listener->created($news); - $this->assertTrue($this->log->hasErrorThatContains($error)); + /** @var News $listener */ + $listener = $this->app->make(News::class); + $listener->updated($this->news, false); } protected function setUp(): void @@ -74,6 +110,10 @@ protected function setUp(): void $this->mailer = $this->createMock(EngelsystemMailer::class); $this->app->instance(EngelsystemMailer::class, $this->mailer); + $this->app->instance('config', new Config()); + + $this->news = NewsModel::factory(['title' => 'Foo'])->create(); + $this->user = User::factory() ->has(Settings::factory([ 'language' => '', diff --git a/tests/Unit/Exceptions/Handlers/LegacyTest.php b/tests/Unit/Exceptions/Handlers/LegacyTest.php index da6ad3075..46e05968b 100644 --- a/tests/Unit/Exceptions/Handlers/LegacyTest.php +++ b/tests/Unit/Exceptions/Handlers/LegacyTest.php @@ -19,14 +19,23 @@ class LegacyTest extends TestCase */ public function testRender(): void { - $handler = new Legacy(); + /** @var Legacy|MockObject $handler */ + $handler = $this->getMockBuilder(Legacy::class) + ->onlyMethods(['isCli']) + ->getMock(); /** @var Request|MockObject $request */ $request = $this->createMock(Request::class); /** @var Exception|MockObject $exception */ $exception = $this->createMock(Exception::class); + $handler->expects($this->exactly(2)) + ->method('isCli') + ->willReturnOnConsecutiveCalls(false, true); + $this->expectOutputRegex('/.*error occurred.*/i'); + $handler->render($request, $exception); + // As CLI $handler->render($request, $exception); } diff --git a/tests/Unit/Factories/UserTest.php b/tests/Unit/Factories/UserTest.php new file mode 100644 index 000000000..39fd3ac63 --- /dev/null +++ b/tests/Unit/Factories/UserTest.php @@ -0,0 +1,498 @@ +now = CarbonImmutable::now(); + Carbon::setTestNow($this->now); + + $this->initDatabase(); + $this->config = new Config([]); + $this->app->instance(Config::class, $this->config); + $this->app->alias(Config::class, 'config'); + $this->config->set('oauth', []); + $this->session = new Session(new MockArraySessionStorage()); + $this->app->instance(SessionInterface::class, $this->session); + $this->app->instance(LoggerInterface::class, new NullLogger()); + + $this->app->instance(ServerRequestInterface::class, new Request()); + $this->app->instance(Authenticator::class, $this->app->make(Authenticator::class)); + $this->app->alias(Authenticator::class, 'authenticator'); + + $this->subject = $this->app->make(User::class); + } + + public function tearDown(): void + { + Carbon::setTestNow(); + } + + /** + * Minimal config with empty data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMinimumConfigEmpty(): void + { + SignUpConfig::setMinimumConfig($this->config); + + $this->assertDataRaisesValidationException( + [], + [ + 'username' => [ + 'validation.username.required', + 'validation.username.username', + ], + 'email' => [ + 'validation.email.required', + 'validation.email.email', + ], + 'password' => [ + 'validation.password.required', + 'validation.password.length', + ], + 'password_confirmation' => [ + 'validation.password_confirmation.required', + ], + ] + ); + } + + /** + * Minimal config with valid data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMinimumConfigCreate(): void + { + SignUpConfig::setMinimumConfig($this->config); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertSame(false, $user->state->arrived); + $this->assertNotEmpty($user->api_key); + } + + /** + * Maximum config with empty data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMaximumConfigEmpty(): void + { + SignUpConfig::setMaximumConfig($this->config); + + $this->assertDataRaisesValidationException( + [], + [ + 'username' => [ + 'validation.username.required', + 'validation.username.username', + ], + 'email' => [ + 'validation.email.required', + 'validation.email.email', + ], + 'password' => [ + 'validation.password.required', + 'validation.password.length', + ], + 'password_confirmation' => [ + 'validation.password_confirmation.required', + ], + 'planned_arrival_date' => [ + 'validation.planned_arrival_date.required', + 'validation.planned_arrival_date.date', + 'validation.planned_arrival_date.min', + ], + 'tshirt_size' => [ + 'validation.tshirt_size.required', + 'validation.tshirt_size.shirtSize', + ], + ] + ); + } + + /** + * Maximum config with invalid data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMaximumConfigInvalid(): void + { + SignUpConfig::setMaximumConfig($this->config); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz23', + 'pronoun' => str_repeat('a', 20), + 'firstname' => str_repeat('a', 70), + 'lastname' => str_repeat('a', 70), + 'email' => 'notanemail', + 'password' => 'a', + 'tshirt_size' => 'A', + 'planned_arrival_date' => $this->now->subDays(7), + 'dect' => str_repeat('a', 50), + 'mobile' => str_repeat('a', 50), + ], + [ + 'username' => [ + 'validation.username.username', + ], + 'email' => [ + 'validation.email.email', + ], + 'mobile' => [ + 'validation.mobile.optional', + ], + 'password' => [ + 'validation.password.length', + ], + 'password_confirmation' => [ + 'validation.password_confirmation.required', + ], + 'firstname' => [ + 'validation.firstname.optional', + ], + 'lastname' => [ + 'validation.lastname.optional', + ], + 'pronoun' => [ + 'validation.pronoun.optional', + ], + 'planned_arrival_date' => [ + 'validation.planned_arrival_date.min', + ], + 'dect' => [ + 'validation.dect.optional', + ], + 'tshirt_size' => [ + 'validation.tshirt_size.shirtSize', + ], + ] + ); + } + + /** + * Minimal config with valid data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMaximumConfigCreate(): void + { + SignUpConfig::setMaximumConfig($this->config); + + $user = $this->subject->createFromData([ + 'pronoun' => 'they', + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + 'planned_arrival_date' => $this->now->format('Y-m-d'), + 'tshirt_size' => 'M', + 'mobile_show' => 1, + ]); + + $this->assertSame('they', $user->personalData->pronoun); + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertTrue(password_verify('s3cret', $user->password)); + $this->assertSame( + $this->now->format('Y-m-d'), + $user->personalData->planned_arrival_date->format('Y-m-d') + ); + $this->assertTrue($user->settings->mobile_show); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testPasswordDoesNotMatchConfirmation(): void + { + SignUpConfig::setMinimumConfig($this->config); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 'huhuuu', + ], + [ + 'password' => [ + 'settings.password.confirmation-does-not-match', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testUsernameAlreadyTaken(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->createFritz(); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ], + [ + 'username' => [ + 'settings.profile.nick.already-taken', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testEmailAlreadyTaken(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->createFritz(); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'peter', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ], + [ + 'email' => [ + 'settings.profile.email.already-taken', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testAngelTypeAssignment(): void + { + $angelTypes = $this->createAngelTypes(); + SignUpConfig::setMinimumConfig($this->config); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + 'angel_types_' . $angelTypes[0]->id => 1, + 'angel_types_' . $angelTypes[1]->id => 1, + // some angel type, that does not exist + 'angel_types_asd' => 1, + ]); + + // Expect an assignment of the normal angel type + $this->assertTrue( + $user->userAngelTypes->contains('name', $angelTypes[0]->name) + ); + + // Do not expect an assignment of the angel type hidden on registration + $this->assertFalse( + $user->userAngelTypes->contains('name', $angelTypes[1]->name) + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testDisablePasswortViaOAuth(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->config->set('enable_password', false); + $this->session->set('oauth2_enable_password', true); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertTrue(password_verify('s3cret', $user->password)); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testAutoArrive(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->config->set('autoarrive', true); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertSame(true, $user->state->arrived); + $this->assertEqualsWithDelta( + $this->now->timestamp, + $user->state->arrival_date->timestamp, + 1, + ); + } + + /** + * Covers the case where both, build-up and tear-down dates are configured. + * + * @covers \Engelsystem\Factories\User + */ + public function testBuildUpAndTearDownDates(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->config->set('enable_planned_arrival', true); + $this->config->set('buildup_start', $this->now); + $this->config->set('teardown_end', $this->now->addDays(7)); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + 'planned_arrival_date' => $this->now->subDays(7), + ], + [ + 'planned_arrival_date' => [ + 'validation.planned_arrival_date.between', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testOAuth(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->session->set('oauth2_connect_provider', 'sso'); + $this->session->set('oauth2_user_id', 'fritz_sso'); + $this->session->set('oauth2_access_token', 'abc123'); + $this->session->set('oauth2_refresh_token', 'jkl456'); + $this->session->set('oauth2_expires_at', '2023-08-15 08:00:00'); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $oAuth = $user->oauth->first(); + $this->assertNotNull($oAuth); + $this->assertSame('sso', $oAuth->provider); + $this->assertSame('fritz_sso', $oAuth->identifier); + $this->assertSame('abc123', $oAuth->access_token); + $this->assertSame('jkl456', $oAuth->refresh_token); + $this->assertSame('2023-08-15 08:00:00', $oAuth->expires_at->format('Y-m-d H:i:s')); + } + + /** + * Create a user with nick "fritz" and email "fritz@example.com". + */ + private function createFritz(): void + { + UserModel::create([ + 'name' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => '', + 'api_key' => '', + ]); + } + + /** + * Creates two AngelTypes: + * 1. Normal angel type + * 2. Angel type hidden on registration + * + * @return Array + */ + private function createAngelTypes(): array + { + return [ + AngelType::create([ + 'name' => 'Test angel type 1', + ]), + AngelType::create([ + 'name' => 'Test angel type 2', + 'hide_register' => true, + ]), + ]; + } + + /** + * @param Array $data Data passed to User::createFromData + * @param Array> $expectedValidationErrors Expected validation errors + */ + private function assertDataRaisesValidationException(array $data, array $expectedValidationErrors): void + { + try { + $this->subject->createFromData($data); + self::fail('Expected exception not raised'); + } catch (ValidationException $err) { + $validator = $err->getValidator(); + $validationErrors = $validator->getErrors(); + $this->assertSame($expectedValidationErrors, $validationErrors); + } + } +} diff --git a/tests/Unit/FactoriesTest.php b/tests/Unit/FactoriesTest.php index 776c17817..b4d760cd9 100644 --- a/tests/Unit/FactoriesTest.php +++ b/tests/Unit/FactoriesTest.php @@ -4,15 +4,22 @@ namespace Engelsystem\Test\Unit; +use Engelsystem\Models\AngelType; use Engelsystem\Models\Faq; +use Engelsystem\Models\Group; use Engelsystem\Models\Message; use Engelsystem\Models\News; use Engelsystem\Models\NewsComment; +use Engelsystem\Models\OAuth; +use Engelsystem\Models\Privilege; use Engelsystem\Models\Question; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; +use Engelsystem\Models\Session; +use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Schedule; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; +use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\User\Contact; use Engelsystem\Models\User\License; use Engelsystem\Models\User\PasswordReset; @@ -20,6 +27,7 @@ use Engelsystem\Models\User\Settings; use Engelsystem\Models\User\State; use Engelsystem\Models\User\User; +use Engelsystem\Models\UserAngelType; use Engelsystem\Models\Worklog; use Illuminate\Database\Eloquent\Model; @@ -33,21 +41,29 @@ class FactoriesTest extends TestCase public function factoriesProvider(): array { return [ + [AngelType::class], [Contact::class], [Faq::class], + [Group::class], [License::class], + [Location::class], [Message::class], + [NeededAngelType::class], [News::class], [NewsComment::class], + [OAuth::class], [PasswordReset::class], [PersonalData::class], + [Privilege::class], [Question::class], - [Room::class], [Schedule::class], - [ShiftEntry::class], + [Session::class], [Settings::class], [Shift::class], + [ShiftEntry::class], + [ShiftType::class], [State::class], + [UserAngelType::class], [User::class], [Worklog::class], ]; @@ -56,19 +72,28 @@ public function factoriesProvider(): array /** * Test all model factories * - * @covers \Database\Factories\Engelsystem\Models\User\ContactFactory + * @covers \Database\Factories\Engelsystem\Models\AngelTypeFactory * @covers \Database\Factories\Engelsystem\Models\FaqFactory - * @covers \Database\Factories\Engelsystem\Models\User\LicenseFactory + * @covers \Database\Factories\Engelsystem\Models\GroupFactory + * @covers \Database\Factories\Engelsystem\Models\LocationFactory * @covers \Database\Factories\Engelsystem\Models\MessageFactory - * @covers \Database\Factories\Engelsystem\Models\NewsFactory * @covers \Database\Factories\Engelsystem\Models\NewsCommentFactory - * @covers \Database\Factories\Engelsystem\Models\User\PasswordResetFactory - * @covers \Database\Factories\Engelsystem\Models\User\PersonalDataFactory + * @covers \Database\Factories\Engelsystem\Models\NewsFactory + * @covers \Database\Factories\Engelsystem\Models\OAuthFactory + * @covers \Database\Factories\Engelsystem\Models\PrivilegeFactory * @covers \Database\Factories\Engelsystem\Models\QuestionFactory - * @covers \Database\Factories\Engelsystem\Models\RoomFactory + * @covers \Database\Factories\Engelsystem\Models\SessionFactory + * @covers \Database\Factories\Engelsystem\Models\Shifts\NeededAngelTypeFactory * @covers \Database\Factories\Engelsystem\Models\Shifts\ScheduleFactory - * @covers \Database\Factories\Engelsystem\Models\User\SettingsFactory + * @covers \Database\Factories\Engelsystem\Models\Shifts\ShiftEntryFactory * @covers \Database\Factories\Engelsystem\Models\Shifts\ShiftFactory + * @covers \Database\Factories\Engelsystem\Models\Shifts\ShiftTypeFactory + * @covers \Database\Factories\Engelsystem\Models\UserAngelTypeFactory + * @covers \Database\Factories\Engelsystem\Models\User\ContactFactory + * @covers \Database\Factories\Engelsystem\Models\User\LicenseFactory + * @covers \Database\Factories\Engelsystem\Models\User\PasswordResetFactory + * @covers \Database\Factories\Engelsystem\Models\User\PersonalDataFactory + * @covers \Database\Factories\Engelsystem\Models\User\SettingsFactory * @covers \Database\Factories\Engelsystem\Models\User\StateFactory * @covers \Database\Factories\Engelsystem\Models\User\UserFactory * @covers \Database\Factories\Engelsystem\Models\WorklogFactory diff --git a/tests/Unit/HasDatabase.php b/tests/Unit/HasDatabase.php index fc86c2bfd..787ffdb30 100644 --- a/tests/Unit/HasDatabase.php +++ b/tests/Unit/HasDatabase.php @@ -9,6 +9,7 @@ use Engelsystem\Database\Migration\MigrationServiceProvider; use Engelsystem\Http\Request; use Illuminate\Database\Capsule\Manager as CapsuleManager; +use Illuminate\Database\Connection; use PDO; use Psr\Http\Message\ServerRequestInterface; @@ -22,7 +23,7 @@ trait HasDatabase protected function initDatabase(): void { $dbManager = new CapsuleManager(); - $dbManager->addConnection(['driver' => 'sqlite', 'database' => ':memory:']); + $dbManager->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'foreign_key_constraints' => true]); $dbManager->bootEloquent(); $connection = $dbManager->getConnection(); @@ -30,6 +31,7 @@ protected function initDatabase(): void $this->database = new Database($connection); $this->app->instance(Database::class, $this->database); + $this->app->instance(Connection::class, $connection); $this->app->register(MigrationServiceProvider::class); $this->app->instance(ServerRequestInterface::class, new Request()); @@ -44,6 +46,7 @@ protected function initDatabase(): void ->insert( [ // Migrations that can be skipped as they only use legacy tables + // or only change data not available/relevant in migrations ['migration' => '2018_01_01_000001_import_install_sql'], ['migration' => '2018_01_01_000002_import_update_sql'], ['migration' => '2018_01_01_000003_fix_old_tables'], @@ -54,6 +57,8 @@ protected function initDatabase(): void ['migration' => '2020_04_07_000000_change_mysql_database_encoding_to_utf8mb4'], ['migration' => '2020_09_12_000000_create_welcome_angel_permissions_group'], ['migration' => '2020_12_28_000000_oauth_set_identifier_binary'], + ['migration' => '2021_05_23_000000_create_first_user'], + ['migration' => '2021_05_23_000000_set_admin_password'], ['migration' => '2021_08_26_000000_add_shirt_edit_permissions'], ['migration' => '2021_10_12_000000_add_shifts_description'], ['migration' => '2021_12_30_000000_remove_admin_news_html_privilege'], @@ -62,6 +67,7 @@ protected function initDatabase(): void ['migration' => '2022_07_21_000000_fix_old_groups_table_id_and_name'], ['migration' => '2022_10_21_000000_add_hide_register_to_angeltypes'], ['migration' => '2022_11_06_000000_shifttype_remove_angeltype'], + ['migration' => '2023_05_21_000001_cleanup_short_api_keys'], ] ); diff --git a/tests/Unit/Helpers/AuthenticatorTest.php b/tests/Unit/Helpers/AuthenticatorTest.php index 0f9a8bc73..1c03c0a65 100644 --- a/tests/Unit/Helpers/AuthenticatorTest.php +++ b/tests/Unit/Helpers/AuthenticatorTest.php @@ -12,6 +12,7 @@ use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\Helpers\Stub\UserModelImplementation; use Engelsystem\Test\Unit\ServiceProviderTest; +use Illuminate\Support\Str; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\ServerRequestInterface; use Symfony\Component\HttpFoundation\Session\Session; @@ -21,6 +22,14 @@ class AuthenticatorTest extends ServiceProviderTest { use HasDatabase; + protected static ?string $passwordHashTesting = null; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + self::$passwordHashTesting = password_hash('testing', PASSWORD_ARGON2I, ['memory_cost' => 100]); + } + /** * @covers \Engelsystem\Helpers\Authenticator::user * @covers \Engelsystem\Helpers\Authenticator::__construct @@ -81,7 +90,7 @@ public function testUserViaFromApi(): void $session = new Session(new MockArraySessionStorage()); $request = $request->withHeader('Authorization', 'Bearer F00Bar'); - $request = $request->withAttribute('route-api', true); + $request = $request->withAttribute('route-api-accessible', true); $this->app->instance('request', $request); User::factory()->create(['api_key' => 'F00Bar']); @@ -151,7 +160,7 @@ public function testUserByHeaders(): void $this->initDatabase(); $request = new Request(); - $request = $request->withAttribute('route-api', true); + $request = $request->withAttribute('route-api-accessible', true); $session = new Session(new MockArraySessionStorage()); $this->app->instance('request', $request); @@ -175,19 +184,58 @@ public function testUserByHeaders(): void $this->assertEquals('F00Bar', $user->api_key); } + /** + * @covers \Engelsystem\Helpers\Authenticator::userByHeaders + */ + public function testUserByHeadersBearerTrimApiKey(): void + { + $this->initDatabase(); + + $request = new Request(); + $request = $request->withAttribute('route-api-accessible', true); + $session = new Session(new MockArraySessionStorage()); + $this->app->instance('request', $request); + + $request = $request->withHeader('authorization', 'bearer F00Bar '); + $auth = new Authenticator($request, $session, new User()); + User::factory()->create(['api_key' => 'F00Bar']); + $user = $auth->user(); + $this->assertInstanceOf(User::class, $user); + $this->assertEquals('F00Bar', $user->api_key); + } + + /** + * @covers \Engelsystem\Helpers\Authenticator::resetApiKey + */ + public function testResetApiKey(): void + { + $this->initDatabase(); + + $user = User::factory()->create(); + $oldKey = $user->api_key; + + $auth = new Authenticator(new Request(), new Session(new MockArraySessionStorage()), new User()); + $auth->resetApiKey($user); + + $updatedUser = User::all()->last(); + $newApiKey = $updatedUser->api_key; + + $this->assertNotEquals($oldKey, $newApiKey); + $this->assertTrue(Str::isAscii($newApiKey)); + $this->assertEquals(64, Str::length($newApiKey)); + } + /** * @covers \Engelsystem\Helpers\Authenticator::can + * @covers \Engelsystem\Helpers\Authenticator::isApiRequest */ public function testCan(): void { $this->initDatabase(); - /** @var ServerRequestInterface|MockObject $request */ - $request = $this->getMockForAbstractClass(ServerRequestInterface::class); - /** @var Session|MockObject $session */ - $session = $this->createMock(Session::class); - /** @var UserModelImplementation|MockObject $userRepository */ - $userRepository = new UserModelImplementation(); + $request = new Request(); + $this->app->instance('request', $request); + $session = new Session(new MockArraySessionStorage()); /** @var User $user */ $user = User::factory()->create(); /** @var Group $group */ @@ -198,28 +246,8 @@ public function testCan(): void $user->groups()->attach($group); $group->privileges()->attach($privilege); - $session->expects($this->once()) - ->method('get') - ->with('user_id') - ->willReturn(42); - $session->expects($this->once()) - ->method('remove') - ->with('user_id'); - - /** @var Authenticator|MockObject $auth */ - $auth = $this->getMockBuilder(Authenticator::class) - ->setConstructorArgs([$request, $session, $userRepository]) - ->onlyMethods(['user']) - ->getMock(); - $auth->expects($this->exactly(2)) - ->method('user') - ->willReturnOnConsecutiveCalls(null, $user); - - Group::factory()->create(['id' => $auth->getGuestRole()]); - - // No user, no permissions - $this->assertFalse($auth->can('foo')); - + $auth = new Authenticator($request, $session, new User()); + $session->set('user_id', $user->id); // User exists, has permissions $this->assertTrue($auth->can('bar')); @@ -227,6 +255,26 @@ public function testCan(): void $this->assertTrue($auth->can('bar')); } + /** + * @covers \Engelsystem\Helpers\Authenticator::can + */ + public function testCanUnauthorized(): void + { + $this->initDatabase(); + + $request = new Request(); + $this->app->instance('request', $request); + $session = new Session(new MockArraySessionStorage()); + + $auth = new Authenticator($request, $session, new User()); + $session->set('user_id', 42); + + // No user, no permissions + $this->assertFalse($auth->can('foo')); + // Old/invalid user id got removed + $this->assertNull($session->get('user_id')); + } + /** * @covers \Engelsystem\Helpers\Authenticator::authenticate */ @@ -242,7 +290,7 @@ public function testAuthenticate(): void User::factory([ 'name' => 'lorem', - 'password' => password_hash('testing', PASSWORD_DEFAULT), + 'password' => self::$passwordHashTesting, 'email' => 'lorem@foo.bar', ])->create(); User::factory([ @@ -263,7 +311,8 @@ public function testAuthenticate(): void public function testVerifyPassword(): void { $this->initDatabase(); - $password = password_hash('testing', PASSWORD_ARGON2I); + $password = self::$passwordHashTesting; + /** @var User $user */ $user = User::factory([ 'name' => 'lorem', @@ -299,13 +348,13 @@ public function testSetPassword(): void $user->save(); $auth = $this->getAuthenticator(); - $auth->setPasswordAlgorithm(PASSWORD_ARGON2I); + $auth->setPasswordAlgorithm(PASSWORD_BCRYPT); $auth->setPassword($user, 'FooBar'); $this->assertTrue($user->isClean()); $this->assertTrue(password_verify('FooBar', $user->password)); - $this->assertFalse(password_needs_rehash($user->password, PASSWORD_ARGON2I)); + $this->assertFalse(password_needs_rehash($user->password, PASSWORD_BCRYPT)); } /** diff --git a/tests/Unit/Helpers/BarChartTest.php b/tests/Unit/Helpers/BarChartTest.php index 2999942e6..bf7146d94 100644 --- a/tests/Unit/Helpers/BarChartTest.php +++ b/tests/Unit/Helpers/BarChartTest.php @@ -37,7 +37,7 @@ protected function setUp(): void { parent::setUp(); $this->rendererMock = $this->mockRenderer(false); - $this->mockTranslator(); + $this->mockTranslator(fn(string $key, array $replace = []) => $key == 'general.date' ? 'Y-m-d' : $key); } public function provideRenderTestData(): Generator diff --git a/tests/Unit/Helpers/CarbonTest.php b/tests/Unit/Helpers/CarbonTest.php index 3566dd814..d1d8b3084 100644 --- a/tests/Unit/Helpers/CarbonTest.php +++ b/tests/Unit/Helpers/CarbonTest.php @@ -65,4 +65,28 @@ public function testCreateTimestampFromInvalidDatetime(string $value): void $timestamp = Carbon::createTimestampFromDatetime($value); self::assertNull($timestamp); } + + public function startOfHourDates(): array + { + return [ + ['2022-04-16 10:00:00.000000', true], + ['2022-04-16 10:00:00.000000', true, true], + ['2022-04-16 10:00:00.123456', true, false], + ['2022-04-16 23:00:00.000000', true, false], + ['2022-04-16 10:00:42.000000', false], + ['2022-04-16 10:23:00.000000', false], + ['2022-04-16 10:00:00.123456', false, true], + ]; + } + + /** + * @covers \Engelsystem\Helpers\Carbon::isStartOfHour + * @dataProvider startOfHourDates + */ + public function testIsStartOfHour(string $value, bool $expected, bool $checkMicroseconds = false): void + { + $date = Carbon::createFromFormat('Y-m-d H:i:s.u', $value); + + $this->assertEquals($expected, $date->isStartOfHour($checkMicroseconds)); + } } diff --git a/tests/Unit/Helpers/DayOfEventTest.php b/tests/Unit/Helpers/DayOfEventTest.php new file mode 100644 index 000000000..ad13bcd6f --- /dev/null +++ b/tests/Unit/Helpers/DayOfEventTest.php @@ -0,0 +1,68 @@ +createAndSetUpAppWithConfig([]); + $this->config = $app->get('config'); + } + + public function tearDown(): void + { + Carbon::setTestNow(); + } + + /** + * @return Array + */ + public function provideTestGetData(): array + { + return [ + 'day -2 (10:00, with day 0)' => [-2, true, '2023-07-31 15:23:42', '2023-07-28 10:00:00'], + 'day -2 (23:59, with day 0)' => [-2, true, '2023-07-31 15:23:42', '2023-07-28 23:59:59'], + 'day -1 (23:59, with day 0)' => [-1, true, '2023-07-31 15:23:42', '2023-07-29 23:59:59'], + 'day 0 (with day 0)' => [0, true, '2023-07-31 15:23:42', '2023-07-30 16:00:00'], + 'day 1 (with day 0)' => [1, true, '2023-07-31 15:23:42', '2023-07-31 10:00:00'], + 'day 2 (with day 0)' => [2, true, '2023-07-31 15:23:42', '2023-08-01 10:00:00'], + + 'day -2 (without day 0)' => [-2, false, '2023-07-31 15:23:42', '2023-07-29 10:00:00'], + 'day -1 (without day 0)' => [-1, false, '2023-07-31 15:23:42', '2023-07-30 23:59:59'], + 'day 1 (without day 0)' => [1, false, '2023-07-31 15:23:42', '2023-07-31 00:00:00'], + 'day 2 (without day 0)' => [2, false, '2023-07-31 15:23:42', '2023-08-01 16:00:00'], + + 'no start date' => [null, false, null, '2023-08-01 16:00:00'], + ]; + } + + /** + * @dataProvider provideTestGetData + * @covers \Engelsystem\Helpers\DayOfEvent + */ + public function testGet( + int | null $expected, + bool $eventHasDay0, + string | null $eventStart, + string $now + ): void { + $this->config->set( + 'event_start', + $eventStart ? Carbon::createFromFormat(self::FORMAT, $eventStart) : null + ); + $this->config->set('event_has_day0', $eventHasDay0); + Carbon::setTestNow(Carbon::createFromFormat(self::FORMAT, $now)); + $this->assertSame($expected, DayOfEvent::get()); + } +} diff --git a/tests/Unit/Helpers/Schedule/Assets/schedule-invalid.html b/tests/Unit/Helpers/Schedule/Assets/schedule-invalid.html new file mode 100644 index 000000000..da99e808b --- /dev/null +++ b/tests/Unit/Helpers/Schedule/Assets/schedule-invalid.html @@ -0,0 +1,10 @@ + + + + I'm HTML! + + +This is not a schedule
    +and thus must be ignored. + + diff --git a/tests/Unit/Helpers/Schedule/XmlParserTest.php b/tests/Unit/Helpers/Schedule/XmlParserTest.php index 6a41b6d7d..6af982bdf 100644 --- a/tests/Unit/Helpers/Schedule/XmlParserTest.php +++ b/tests/Unit/Helpers/Schedule/XmlParserTest.php @@ -23,12 +23,13 @@ class XmlParserTest extends TestCase */ public function testLoad(): void { - libxml_use_internal_errors(true); - $parser = new XmlParser(); // Invalid XML $this->assertFalse($parser->load('foo')); + // Invalid schedule + $this->assertFalse($parser->load(file_get_contents(__DIR__ . '/Assets/schedule-invalid.html'))); + // Minimal import $this->assertTrue($parser->load(file_get_contents(__DIR__ . '/Assets/schedule-minimal.xml'))); // Basic import diff --git a/tests/Unit/Helpers/ShiftsTest.php b/tests/Unit/Helpers/ShiftsTest.php index 869c3fd14..f6c00c91a 100644 --- a/tests/Unit/Helpers/ShiftsTest.php +++ b/tests/Unit/Helpers/ShiftsTest.php @@ -32,7 +32,7 @@ public function testIsNightShiftDisabled(): void } /** - * @return array[][] + * @return array{0: Carbon, 1: Carbon, 2: boolean}[] */ public function nightShiftData(): array { diff --git a/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php b/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php index ed45d8f80..f76f99c4d 100644 --- a/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php +++ b/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php @@ -4,7 +4,12 @@ namespace Engelsystem\Test\Unit\Http\SessionHandlers; +use Engelsystem\Config\Config; +use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\Carbon; use Engelsystem\Http\SessionHandlers\DatabaseHandler; +use Engelsystem\Models\Session; +use Engelsystem\Models\User\User; use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\TestCase; @@ -14,7 +19,6 @@ class DatabaseHandlerTest extends TestCase /** * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::__construct - * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::getQuery * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::read */ public function testRead(): void @@ -22,26 +26,47 @@ public function testRead(): void $handler = new DatabaseHandler($this->database); $this->assertEquals('', $handler->read('foo')); - $this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', CURRENT_TIMESTAMP)"); - $this->assertEquals('Lorem Ipsum', $handler->read('foo')); + $this->database->getConnection() + ->table('sessions') + ->insert([ + ['id' => 'id-foo', 'payload' => 'Lorem Ipsum', 'last_activity' => Carbon::now()], + ]); + $this->assertEquals('Lorem Ipsum', $handler->read('id-foo')); } /** - * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::getCurrentTimestamp * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::write */ public function testWrite(): void { + $user = User::factory()->create(); + $auth = $this->createMock(Authenticator::class); + $auth->expects($this->exactly(2)) + ->method('user') + ->willReturnOnConsecutiveCalls(null, $user); + $this->app->instance('authenticator', $auth); + $handler = new DatabaseHandler($this->database); + $userExists = false; foreach (['Lorem Ipsum', 'Dolor Sit!'] as $data) { - $this->assertTrue($handler->write('foo', $data)); + $this->assertTrue($handler->write('id-foo', $data)); - $return = $this->database->select('SELECT * FROM sessions WHERE id = :id', ['id' => 'foo']); + $return = Session::whereId('id-foo')->get(); $this->assertCount(1, $return); - $return = array_shift($return); - $this->assertEquals($data, $return->payload); + /** @var Session $session */ + $session = $return->first(); + $this->assertEquals($data, $session->payload); + + if ($userExists) { + $this->assertNotNull($session->user); + $this->assertEquals($user->id, $session->user->id); + } else { + $this->assertNull($session->user); + } + + $userExists = true; } } @@ -50,22 +75,26 @@ public function testWrite(): void */ public function testDestroy(): void { - $this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', CURRENT_TIMESTAMP)"); - $this->database->insert("INSERT INTO sessions VALUES ('bar', 'Dolor Sit', CURRENT_TIMESTAMP)"); + $table = $this->database->getConnection()->table('sessions'); + $table + ->insert([ + ['id' => 'id-foo', 'payload' => 'Lorem Ipsum', 'last_activity' => Carbon::now()->subHours(25)], + ['id' => 'id-bar', 'payload' => 'Dolor Sit', 'last_activity' => Carbon::now()], + ]); $handler = new DatabaseHandler($this->database); - $this->assertTrue($handler->destroy('batz')); + $this->assertTrue($handler->destroy('id-baz')); - $return = $this->database->select('SELECT * FROM sessions'); + $return = $table->get(); $this->assertCount(2, $return); - $this->assertTrue($handler->destroy('bar')); + $this->assertTrue($handler->destroy('id-bar')); - $return = $this->database->select('SELECT * FROM sessions'); + $return = $table->get(); $this->assertCount(1, $return); - $return = array_shift($return); - $this->assertEquals('foo', $return->id); + $return = $return->first(); + $this->assertEquals('id-foo', $return->id); } /** @@ -73,18 +102,25 @@ public function testDestroy(): void */ public function testGc(): void { - $this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', '2000-01-01 01:00')"); - $this->database->insert("INSERT INTO sessions VALUES ('bar', 'Dolor Sit', '3000-01-01 01:00')"); + $this->app->instance('config', new Config(['session' => ['lifetime' => 2]])); // 2 days + + $table = $this->database->getConnection()->table('sessions'); + $table + ->insert([ + ['id' => 'id-foo', 'payload' => 'Lorem Ipsum', 'last_activity' => Carbon::now()->subHours(48 + 1)], + ['id' => 'id-bar', 'payload' => 'Dolor Sit', 'last_activity' => Carbon::now()], + ]); $handler = new DatabaseHandler($this->database); - $this->assertEquals(1, $handler->gc(60 * 60)); + // Max lifetime gets overwritten by settings anyway + $this->assertEquals(1, $handler->gc(42)); - $return = $this->database->select('SELECT * FROM sessions'); + $return = $table->get(); $this->assertCount(1, $return); - $return = array_shift($return); - $this->assertEquals('bar', $return->id); + $return = $return->first(); + $this->assertEquals('id-bar', $return->id); } /** diff --git a/tests/Unit/Http/SessionServiceProviderTest.php b/tests/Unit/Http/SessionServiceProviderTest.php index 6cd6b8cda..1b0dde2e8 100644 --- a/tests/Unit/Http/SessionServiceProviderTest.php +++ b/tests/Unit/Http/SessionServiceProviderTest.php @@ -34,6 +34,7 @@ public function testRegister(): void $session = $this->getSessionMock(); $request = $this->getRequestMock(); + $request->server->set('HTTPS', 'on'); /** @var SessionServiceProvider|MockObject $serviceProvider */ $serviceProvider = $this->getMockBuilder(SessionServiceProvider::class) @@ -59,7 +60,12 @@ public function testRegister(): void NativeSessionStorage::class, [ // 2 days - 'options' => ['cookie_httponly' => true, 'name' => 'session', 'cookie_lifetime' => 172800], + 'options' => [ + 'cookie_secure' => true, + 'cookie_httponly' => true, + 'name' => 'session', + 'cookie_lifetime' => 172800, + ], 'handler' => null, ], ], @@ -69,7 +75,12 @@ public function testRegister(): void NativeSessionStorage::class, [ // 5 days - 'options' => ['cookie_httponly' => true, 'name' => 'foobar', 'cookie_lifetime' => 432000], + 'options' => [ + 'cookie_secure' => true, + 'cookie_httponly' => true, + 'name' => 'foobar', + 'cookie_lifetime' => 432000, + ], 'handler' => $databaseHandler, ], ], @@ -95,18 +106,18 @@ public function testRegister(): void $app->expects($this->exactly(5)) ->method('get') ->withConsecutive( + ['request'], ['request'], ['config'], ['request'], ['config'], - ['request'] ) ->willReturnOnConsecutiveCalls( + $request, $request, $config, $request, $config, - $request ); $app->expects($this->atLeastOnce()) @@ -181,7 +192,7 @@ private function getSessionMock(): MockObject ->getMock(); } - private function getRequestMock(): MockObject + private function getRequestMock(): MockObject|Request { return $this->getMockBuilder(Request::class) ->onlyMethods(['setSession']) diff --git a/tests/Unit/Http/Validation/Rules/BetweenTest.php b/tests/Unit/Http/Validation/Rules/BetweenTest.php index 95d90b8cb..b8e571d49 100644 --- a/tests/Unit/Http/Validation/Rules/BetweenTest.php +++ b/tests/Unit/Http/Validation/Rules/BetweenTest.php @@ -16,8 +16,11 @@ public function testValidate(): void { $rule = new Between(3, 10); $this->assertFalse($rule->validate(1)); + $this->assertFalse($rule->validate(2)); $this->assertFalse($rule->validate('11')); + $this->assertTrue($rule->validate(3)); $this->assertTrue($rule->validate(5)); + $this->assertTrue($rule->validate(10)); $this->assertFalse($rule->validate('AS')); $this->assertFalse($rule->validate('TestContentThatCounts')); $this->assertTrue($rule->validate('TESTING')); @@ -25,6 +28,12 @@ public function testValidate(): void $rule = new Between('2042-01-01', '2042-10-10'); $this->assertFalse($rule->validate('2000-01-01')); $this->assertFalse($rule->validate('3000-01-01')); + $this->assertFalse($rule->validate('2041-12-31')); + $this->assertFalse($rule->validate('2042-10-11')); $this->assertTrue($rule->validate('2042-05-11')); + $this->assertTrue($rule->validate('2042-01-01')); + $this->assertTrue($rule->validate('2042-01-02')); + $this->assertTrue($rule->validate('2042-10-09')); + $this->assertTrue($rule->validate('2042-10-10')); } } diff --git a/tests/Unit/Http/Validation/Rules/MaxTest.php b/tests/Unit/Http/Validation/Rules/MaxTest.php index c326dd0e7..2bd878742 100644 --- a/tests/Unit/Http/Validation/Rules/MaxTest.php +++ b/tests/Unit/Http/Validation/Rules/MaxTest.php @@ -18,11 +18,16 @@ public function testValidate(): void $this->assertFalse($rule->validate(10)); $this->assertFalse($rule->validate('22')); $this->assertTrue($rule->validate(3)); + $this->assertTrue($rule->validate(2)); + $this->assertTrue($rule->validate(-10)); $this->assertFalse($rule->validate('TEST')); $this->assertTrue($rule->validate('AS')); $rule = new Max('2042-01-01'); $this->assertFalse($rule->validate('2100-01-01')); + $this->assertFalse($rule->validate('2042-01-02')); $this->assertTrue($rule->validate('2000-01-01')); + $this->assertTrue($rule->validate('2041-12-31')); + $this->assertTrue($rule->validate('2042-01-01')); } } diff --git a/tests/Unit/Http/Validation/Rules/MinTest.php b/tests/Unit/Http/Validation/Rules/MinTest.php index 5faa341ae..60e95d546 100644 --- a/tests/Unit/Http/Validation/Rules/MinTest.php +++ b/tests/Unit/Http/Validation/Rules/MinTest.php @@ -15,14 +15,19 @@ class MinTest extends TestCase public function testValidate(): void { $rule = new Min(3); + $this->assertFalse($rule->validate(-10)); $this->assertFalse($rule->validate(1)); $this->assertFalse($rule->validate('2')); $this->assertTrue($rule->validate(3)); + $this->assertTrue($rule->validate(4)); $this->assertFalse($rule->validate('AS')); $this->assertTrue($rule->validate('TEST')); $rule = new Min('2042-01-01'); $this->assertFalse($rule->validate('2000-01-01')); + $this->assertFalse($rule->validate('2041-12-31')); + $this->assertTrue($rule->validate('2042-01-01')); + $this->assertTrue($rule->validate('2042-01-02')); $this->assertTrue($rule->validate('2345-01-01')); } } diff --git a/tests/Unit/Http/Validation/Rules/ShirtSizeTest.php b/tests/Unit/Http/Validation/Rules/ShirtSizeTest.php new file mode 100644 index 000000000..9186d31de --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/ShirtSizeTest.php @@ -0,0 +1,49 @@ +createAndSetUpAppWithConfig([]); + $app->get('config')->set('tshirt_sizes', [ + 'S' => 'Small Straight-Cut', + 'M' => 'Medium Straight-Cut', + ]); + $this->subject = new ShirtSize(); + } + + /** + * @return array + */ + public function provideTestValidateData(): array + { + $data = [ + 'empty string' => ['', false], + 'null' => [null, false], + '0' => [0, false], + '"S" (known value)' => ['S', true], + '"M" (known value)' => ['M', true], + '"L" (unknown value)' => ['L', false], + ]; + + return $data; + } + + /** + * @covers \Engelsystem\Http\Validation\Rules\ShirtSize::__construct + * @dataProvider provideTestValidateData + */ + public function testValidate(mixed $value, bool $expectedValid): void + { + self::assertSame($expectedValid, $this->subject->validate($value)); + } +} diff --git a/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php b/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php index 94373d1d6..fbdebd0a5 100644 --- a/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php +++ b/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php @@ -40,6 +40,7 @@ public function validateProvider(): array ['com', 3], ['Test', 4], ['H', 1], + ['Europe/Berlin', 13], ['3', 3], [42, 42], [99.3, 99.3], diff --git a/tests/Unit/Http/Validation/Rules/UsernameTest.php b/tests/Unit/Http/Validation/Rules/UsernameTest.php new file mode 100644 index 000000000..705d97457 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/UsernameTest.php @@ -0,0 +1,67 @@ +subject = new Username(); + + $app = $this->createAndSetUpAppWithConfig([]); + $this->config = $app->get('config'); + + // load "username_regex" from the default config + $defaultConfig = include __DIR__ . '/../../../../../config/config.default.php'; + $this->config->set('username_regex', $defaultConfig['username_regex']); + } + + /** + * @return array + */ + public function provideValidateWithDefaultConfigTestData(): array + { + return [ + 'empty string' => ['', false], + 'max length exceeded' => [str_repeat('1', 25), false], + 'invalid char !' => ['abc!!!', false], + 'space' => ['ab c', false], + 'valid Greek letters' => ['λουκάνικο', true], + 'min length valid' => ['a', true], + 'valid with all chars' => ['abc123_-.jkl', true], + 'valid with accents' => ['cafΓ©', true], + 'max length valid' => [str_repeat('a', 24), true], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Rules\Username::validate + * @dataProvider provideValidateWithDefaultConfigTestData + */ + public function testValidateWithDefaultConfig(mixed $value, bool $expectedValid): void + { + self::assertSame($expectedValid, $this->subject->validate($value)); + } + + /** + * @covers \Engelsystem\Http\Validation\Rules\Username::validate + */ + public function testMissingConfigRaisesException(): void + { + $this->config->set('username_regex', null); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('username_regex not set in config'); + $this->subject->validate('test'); + } +} diff --git a/tests/Unit/Logger/LoggerTest.php b/tests/Unit/Logger/LoggerTest.php index 329a8e1b4..277b4ba37 100644 --- a/tests/Unit/Logger/LoggerTest.php +++ b/tests/Unit/Logger/LoggerTest.php @@ -45,6 +45,7 @@ public function provideLogLevels(): array /** * @covers \Engelsystem\Logger\Logger::log + * @covers \Engelsystem\Logger\Logger::createEntry * @dataProvider provideLogLevels */ public function testAllLevels(string $level): void diff --git a/tests/Unit/Logger/UserAwareLoggerTest.php b/tests/Unit/Logger/UserAwareLoggerTest.php index eeadeed0a..30444547f 100644 --- a/tests/Unit/Logger/UserAwareLoggerTest.php +++ b/tests/Unit/Logger/UserAwareLoggerTest.php @@ -8,18 +8,23 @@ use Engelsystem\Logger\UserAwareLogger; use Engelsystem\Models\LogEntry; use Engelsystem\Models\User\User; -use Engelsystem\Test\Unit\ServiceProviderTest; +use Engelsystem\Test\Unit\HasDatabase; +use Engelsystem\Test\Unit\TestCase; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LogLevel; -class UserAwareLoggerTest extends ServiceProviderTest +class UserAwareLoggerTest extends TestCase { + use HasDatabase; + /** - * @covers \Engelsystem\Logger\UserAwareLogger::log + * @covers \Engelsystem\Logger\UserAwareLogger::createEntry * @covers \Engelsystem\Logger\UserAwareLogger::setAuth */ public function testLog(): void { + $this->initDatabase(); // To be able to run the test by itself + $user = User::factory(['id' => 1, 'name' => 'admin'])->make(); /** @var LogEntry|MockObject $logEntry */ @@ -30,7 +35,7 @@ public function testLog(): void ->method('create') ->withConsecutive( [['level' => LogLevel::INFO, 'message' => 'Some more informational foo']], - [['level' => LogLevel::INFO, 'message' => 'admin (1): Some even more informational bar']] + [['level' => LogLevel::INFO, 'message' => 'Some even more informational bar', 'user_id' => 1]] ); /** @var Authenticator|MockObject $auth */ diff --git a/tests/Unit/Mail/EngelsystemMailerTest.php b/tests/Unit/Mail/EngelsystemMailerTest.php index 04c842d91..6f558ce9e 100644 --- a/tests/Unit/Mail/EngelsystemMailerTest.php +++ b/tests/Unit/Mail/EngelsystemMailerTest.php @@ -13,6 +13,7 @@ use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\TestCase; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\NullLogger; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\RawMessage; @@ -33,13 +34,14 @@ public function testSendView(): void $symfonyMailer = $this->getMockForAbstractClass(MailerInterface::class); /** @var EngelsystemMailer|MockObject $mailer */ $mailer = $this->getMockBuilder(EngelsystemMailer::class) - ->setConstructorArgs(['mailer' => $symfonyMailer, 'view' => $view]) + ->setConstructorArgs(['log' => new NullLogger(), 'mailer' => $symfonyMailer, 'view' => $view]) ->onlyMethods(['send']) ->getMock(); - $this->setExpects($mailer, 'send', ['foo@bar.baz', 'Lorem dolor', 'Rendered Stuff!']); + $this->setExpects($mailer, 'send', ['foo@bar.baz', 'Lorem dolor', 'Rendered Stuff!'], true); $this->setExpects($view, 'render', ['test/template.tpl', ['dev' => true]], 'Rendered Stuff!'); - $mailer->sendView('foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]); + $status = $mailer->sendView('foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]); + $this->assertTrue($status); } /** @@ -63,11 +65,21 @@ public function testSendViewTranslated(): void /** @var EngelsystemMailer|MockObject $mailer */ $mailer = $this->getMockBuilder(EngelsystemMailer::class) - ->setConstructorArgs(['mailer' => $symfonyMailer, 'view' => $view, 'translation' => $translator]) + ->setConstructorArgs([ + 'log' => new NullLogger(), + 'mailer' => $symfonyMailer, + 'view' => $view, + 'translation' => $translator, + ]) ->onlyMethods(['sendView']) ->getMock(); - $this->setExpects($mailer, 'sendView', ['foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]]); + $this->setExpects( + $mailer, + 'sendView', + ['foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]], + true + ); $this->setExpects($translator, 'getLocales', null, ['de_DE' => 'de_DE', 'en_US' => 'en_US']); $this->setExpects($translator, 'getLocale', null, 'en_US'); $this->setExpects($translator, 'translate', ['translatable.text', ['dev' => true]], 'Lorem dolor'); @@ -75,13 +87,14 @@ public function testSendViewTranslated(): void ->method('setLocale') ->withConsecutive(['de_DE'], ['en_US']); - $mailer->sendViewTranslated( + $status = $mailer->sendViewTranslated( $user, 'translatable.text', 'test/template.tpl', ['dev' => true], 'de_DE' ); + $this->assertTrue($status); } /** @@ -104,13 +117,14 @@ public function testSend(): void $this->assertStringContainsString('Lorem Ipsum!', $message->toString()); }); - $mailer = new EngelsystemMailer($symfonyMailer); + $mailer = new EngelsystemMailer(new NullLogger(), $symfonyMailer); $mailer->setFromAddress('foo@bar.baz'); $mailer->setFromName('Foo Bar'); $mailer->setSubjectPrefix('Mail test'); $this->assertEquals('Mail test', $mailer->getSubjectPrefix()); - $mailer->send('to@xam.pel', 'Foo Bar ', 'Lorem Ipsum!'); + $status = $mailer->send('to@xam.pel', 'Foo Bar ', 'Lorem Ipsum!'); + $this->assertTrue($status); } } diff --git a/tests/Unit/Mail/MailerTest.php b/tests/Unit/Mail/MailerTest.php index 79f497ee8..8afd94f1c 100644 --- a/tests/Unit/Mail/MailerTest.php +++ b/tests/Unit/Mail/MailerTest.php @@ -7,7 +7,10 @@ use Engelsystem\Mail\Mailer; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Psr\Log\Test\TestLogger; use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\RawMessage; @@ -22,10 +25,11 @@ class MailerTest extends TestCase */ public function testInitAndSettersAndGetters(): void { + $log = new NullLogger(); /** @var MailerInterface|MockObject $symfonyMailer */ $symfonyMailer = $this->createMock(MailerInterface::class); - $mailer = new Mailer($symfonyMailer); + $mailer = new Mailer($log, $symfonyMailer); $mailer->setFromName('From Name'); $this->assertEquals('From Name', $mailer->getFromName()); @@ -39,6 +43,7 @@ public function testInitAndSettersAndGetters(): void */ public function testSend(): void { + $log = new NullLogger(); /** @var MailerInterface|MockObject $symfonyMailer */ $symfonyMailer = $this->createMock(MailerInterface::class); $symfonyMailer->expects($this->once()) @@ -51,10 +56,36 @@ public function testSend(): void $this->assertStringContainsString('Lorem Ipsum!', $message->toString()); }); - $mailer = new Mailer($symfonyMailer); + $mailer = new Mailer($log, $symfonyMailer); $mailer->setFromAddress('foo@bar.baz'); $mailer->setFromName('Test Tester'); - $mailer->send('to@xam.pel', 'Foo Bar', 'Lorem Ipsum!'); + $status = $mailer->send('to@xam.pel', 'Foo Bar', 'Lorem Ipsum!'); + $this->assertTrue($status); + } + + + /** + * @covers \Engelsystem\Mail\Mailer::send + */ + public function testSendException(): void + { + $log = new TestLogger(); + /** @var MailerInterface|MockObject $symfonyMailer */ + $symfonyMailer = $this->createMock(MailerInterface::class); + $symfonyMailer->expects($this->once()) + ->method('send') + ->willReturnCallback(function (RawMessage $message, Envelope $envelope = null): void { + throw new TransportException('Unable to connect to port 42'); + }); + + $mailer = new Mailer($log, $symfonyMailer); + $mailer->setFromAddress('foo@bar.baz'); + $mailer->setFromName('Test Tester'); + + $status = $mailer->send('to@xam.pel', 'Foo Bar', 'Lorem Ipsum!'); + $this->assertFalse($status); + + $this->assertTrue($log->hasErrorThatContains('Unable to send e-mail')); } } diff --git a/tests/Unit/Middleware/ApiRouteHandlerTest.php b/tests/Unit/Middleware/ApiRouteHandlerTest.php new file mode 100644 index 000000000..61b2eab3c --- /dev/null +++ b/tests/Unit/Middleware/ApiRouteHandlerTest.php @@ -0,0 +1,173 @@ +provideIsApi(), + ['/metrics', true, false], + ['/metrics/test', false, false], + ['/health', true, false], + ]; + } + + /** + * @covers \Engelsystem\Middleware\ApiRouteHandler::process + * @covers \Engelsystem\Middleware\ApiRouteHandler::processApi + * @covers \Engelsystem\Middleware\ApiRouteHandler::__construct + * @dataProvider provideIsApi + */ + public function testProcessIsApi(string $uri, bool $isApi): void + { + $request = Request::create($uri); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + $response = new Response('response content'); + + $handler->expects($this->once()) + ->method('handle') + ->willReturnCallback(function (ServerRequestInterface $request) use ($response, $isApi) { + $this->assertEquals($isApi, $request->getAttribute('route-api')); + return $response; + }); + + $middleware = new ApiRouteHandler(); + $apiResponse = $middleware->process($request, $handler); + + if ($isApi) { + $this->assertEquals('application/json', $apiResponse->getHeaderLine('content-type')); + $this->assertEquals('*', $apiResponse->getHeaderLine('access-control-allow-origin')); + $this->assertEquals('{"message":"response content"}', (string) $apiResponse->getBody()); + $this->assertNotEmpty($apiResponse->getHeaderLine('Etag')); + } else { + $this->assertEquals($response, $apiResponse); + } + } + + /** + * @covers \Engelsystem\Middleware\ApiRouteHandler::process + * @dataProvider provideIsApiAccessiblePath + */ + public function testProcessIsApiAccessiblePath(string $uri, bool $isApiAccessible, bool $isOnlyApi = true): void + { + $request = Request::create($uri); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + $response = new Response('response content'); + + $handler->expects($this->once()) + ->method('handle') + ->willReturnCallback(function (ServerRequestInterface $request) use ($response, $isApiAccessible) { + $this->assertEquals($isApiAccessible, $request->getAttribute('route-api-accessible')); + return $response; + }); + + $middleware = new ApiRouteHandler(); + $apiResponse = $middleware->process($request, $handler); + + if (!$isOnlyApi) { + $this->assertEquals($response, $apiResponse); + } + } + + /** + * @covers \Engelsystem\Middleware\ApiRouteHandler::processApi + */ + public function testProcessApiModelNotFoundException(): void + { + $request = Request::create('/api/test'); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + + $handler->expects($this->once()) + ->method('handle') + ->willReturnCallback(function (): void { + throw new ModelNotFoundException(User::class); + }); + + $middleware = new ApiRouteHandler(); + $response = $middleware->process($request, $handler); + + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEquals('{"message":"Not Found"}', (string) $response->getBody()); + } + + /** + * @covers \Engelsystem\Middleware\ApiRouteHandler::processApi + */ + public function testProcessApiHttpException(): void + { + $request = Request::create('/api/test'); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + + $handler->expects($this->once()) + ->method('handle') + ->willReturnCallback(function (): void { + throw new HttpNotFound(); + }); + + $middleware = new ApiRouteHandler(); + $response = $middleware->process($request, $handler); + + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEquals('{"message":"Not Found"}', (string) $response->getBody()); + } + + /** + * @covers \Engelsystem\Middleware\ApiRouteHandler::processApi + */ + public function testProcessGenericException(): void + { + $e = new Exception(); + $request = Request::create('/api/test'); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + $errorHandler = $this->createMock(Handler::class); + $this->setExpects($errorHandler, 'exceptionHandler', [$e, true], '', $this->once()); + $this->app->instance('error.handler', $errorHandler); + + $handler->expects($this->once()) + ->method('handle') + ->willReturnCallback(function () use ($e): void { + throw $e; + }); + + $middleware = new ApiRouteHandler(); + $response = $middleware->process($request, $handler); + + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals('{"message":"Internal Server Error"}', (string) $response->getBody()); + } +} diff --git a/tests/Unit/Middleware/ETagHandlerTest.php b/tests/Unit/Middleware/ETagHandlerTest.php new file mode 100644 index 000000000..ac5314368 --- /dev/null +++ b/tests/Unit/Middleware/ETagHandlerTest.php @@ -0,0 +1,57 @@ +getMockForAbstractClass(RequestHandlerInterface::class); + $request = Request::create('https://localhost') + ->withHeader('If-None-Match', 'FooBarBaz'); + $originalResponse = (new Response()) + ->withHeader('ETag', '"FooBarBaz"') + ->withHeader('original-header', 'value') + ->withContent('Foo bar!'); + $this->setExpects($handler, 'handle', [$request], $originalResponse); + + $middleware = new ETagHandler(); + $response = $middleware->process($request, $handler); + + $this->assertTrue($response->hasHeader('original-header')); + $this->assertEquals('value', $response->getHeader('original-header')[0]); + + $this->assertEquals(304, $response->getStatusCode()); + $this->assertEquals('', (string) $response->getBody()); + } + + /** + * @covers \Engelsystem\Middleware\ETagHandler::process + */ + public function testRegisterNoChange(): void + { + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + $request = Request::create('https://localhost'); + $originalResponse = new Response(); + $this->setExpects($handler, 'handle', [$request], $originalResponse); + + $middleware = new ETagHandler(); + $response = $middleware->process($request, $handler); + + $this->assertEquals($originalResponse, $response); + } +} diff --git a/tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php b/tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php index 57970a5f5..52b7c392e 100644 --- a/tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php +++ b/tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Server\MiddlewareInterface; +use stdClass; class ResolvesMiddlewareTraitTest extends TestCase { @@ -51,6 +52,20 @@ public function testResolveMiddleware(): void $middleware->callResolveMiddleware('UnresolvableClass'); } + /** + * @covers \Engelsystem\Middleware\ResolvesMiddlewareTrait::resolveMiddleware + */ + public function testResolveMiddlewareNotCallable(): void + { + /** @var Application|MockObject $container */ + $container = $this->createMock(Application::class); + + $middleware = new ResolvesMiddlewareTraitImplementation($container); + + $this->expectException(InvalidArgumentException::class); + $middleware->callResolveMiddleware([new stdClass(), 'test']); + } + /** * @covers \Engelsystem\Middleware\ResolvesMiddlewareTrait::resolveMiddleware */ diff --git a/tests/Unit/Middleware/SessionHandlerServiceProviderTest.php b/tests/Unit/Middleware/SessionHandlerServiceProviderTest.php deleted file mode 100644 index 61e1ed7ad..000000000 --- a/tests/Unit/Middleware/SessionHandlerServiceProviderTest.php +++ /dev/null @@ -1,46 +0,0 @@ -createMock(ContextualBindingBuilder::class); - $app = $this->getApp(['when']); - - $app->expects($this->once()) - ->method('when') - ->with(SessionHandler::class) - ->willReturn($bindingBuilder); - - $bindingBuilder->expects($this->once()) - ->method('needs') - ->with('$paths') - ->willReturn($bindingBuilder); - - $bindingBuilder->expects($this->once()) - ->method('give') - ->willReturnCallback(function (callable $callable): void { - $paths = $callable(); - - $this->assertIsArray($paths); - $this->assertTrue(in_array('/metrics', $paths)); - }); - - $serviceProvider = new SessionHandlerServiceProvider($app); - $serviceProvider->register(); - } -} diff --git a/tests/Unit/Middleware/SessionHandlerTest.php b/tests/Unit/Middleware/SessionHandlerTest.php index 51b3ea525..d24168fce 100644 --- a/tests/Unit/Middleware/SessionHandlerTest.php +++ b/tests/Unit/Middleware/SessionHandlerTest.php @@ -40,13 +40,8 @@ public function testProcess(): void $request->expects($this->exactly(2)) ->method('getAttribute') - ->with('route-request-path') - ->willReturnOnConsecutiveCalls('/foo', '/lorem'); - - $request->expects($this->exactly(2)) - ->method('withAttribute') - ->withConsecutive(['route-api', true], ['route-api', false]) - ->willReturn($request); + ->with('route-api-accessible') + ->willReturnOnConsecutiveCalls(true, false); $sessionStorage->expects($this->once()) ->method('getName') diff --git a/tests/Unit/Middleware/Stub/ResolvesMiddlewareTraitImplementation.php b/tests/Unit/Middleware/Stub/ResolvesMiddlewareTraitImplementation.php index a4c9b1f15..57d56c426 100644 --- a/tests/Unit/Middleware/Stub/ResolvesMiddlewareTraitImplementation.php +++ b/tests/Unit/Middleware/Stub/ResolvesMiddlewareTraitImplementation.php @@ -18,7 +18,7 @@ public function __construct(protected ?Application $container = null) } public function callResolveMiddleware( - string|callable|MiddlewareInterface|RequestHandlerInterface $middleware + string|callable|array|MiddlewareInterface|RequestHandlerInterface $middleware ): MiddlewareInterface|RequestHandlerInterface { return $this->resolveMiddleware($middleware); } diff --git a/tests/Unit/Middleware/TrimInputTest.php b/tests/Unit/Middleware/TrimInputTest.php new file mode 100644 index 000000000..8a0fedad9 --- /dev/null +++ b/tests/Unit/Middleware/TrimInputTest.php @@ -0,0 +1,106 @@ + + */ + private MockObject $handler; + + public function setUp(): void + { + $this->subject = new TrimInput(); + $this->handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + } + + /** + * @return Generator + */ + public function provideTrimTestData(): Generator + { + yield 'GET request' => ['GET', [], []]; + + foreach (['POST', 'PUT'] as $method) { + yield $method . ' request with empty data' => [$method, [], []]; + yield $method . ' request with mixed data' => [ + $method, + [ + 'fieldA' => 23, + 'fieldB' => ' bla ', + 'password' => ' pass1 ', + 'password2' => ' pass2 ', + 'new_password' => ' new_password ', + 'new_password2' => ' new_password2 ', + 'new_pw' => ' new_pw ', + 'new_pw2' => ' new_pw2 ', + 'password_confirmation' => ' password_confirmation ', + 'fieldC' => ['sub' => ' bla2 '], + 'fieldD' => null, + ], + [ + 'fieldA' => 23, + 'fieldB' => 'bla', + // password fields should keep their surrounding spaces + 'password' => ' pass1 ', + 'password2' => ' pass2 ', + 'new_password' => ' new_password ', + 'new_password2' => ' new_password2 ', + 'new_pw' => ' new_pw ', + 'new_pw2' => ' new_pw2 ', + 'password_confirmation' => ' password_confirmation ', + 'fieldC' => ['sub' => 'bla2'], + 'fieldD' => null, + ], + ]; + } + } + + /** + * @covers \Engelsystem\Middleware\TrimInput + * @dataProvider provideTrimTestData + */ + public function testTrim(string $method, mixed $body, mixed $expectedBody): void + { + $request = (new Request())->withMethod($method)->withParsedBody($body); + $this->handler->expects(self::once())->method('handle')->with( + self::callback(function (ServerRequestInterface $request) use ($expectedBody): bool { + self::assertSame($expectedBody, $request->getParsedBody()); + return true; + }) + ); + $this->subject->process($request, $this->handler); + } + + /** + * Special test case to cover null value parsed body. + * + * @covers \Engelsystem\Middleware\TrimInput + */ + public function testTrimPostNull(): void + { + $request = $this->getMockForAbstractClass(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('POST'); + $request->method('getParsedBody')->willReturn(null); + $this->handler->expects(self::once())->method('handle')->with( + self::callback(function (ServerRequestInterface $request): bool { + self::assertNull($request->getParsedBody()); + return true; + }) + ); + $this->subject->process($request, $this->handler); + } +} diff --git a/tests/Unit/Models/GroupTest.php b/tests/Unit/Models/GroupTest.php index 78d92108f..9be1df2ba 100644 --- a/tests/Unit/Models/GroupTest.php +++ b/tests/Unit/Models/GroupTest.php @@ -28,7 +28,7 @@ public function testPrivileges(): void $model->privileges()->attach($privilege2); /** @var Group $savedModel */ - $savedModel = Group::first(); + $savedModel = Group::all()->last(); $this->assertEquals('Some Group', $savedModel->name); $this->assertEquals($privilege1->name, $savedModel->privileges[0]->name); $this->assertEquals($privilege2->name, $savedModel->privileges[1]->name); @@ -52,7 +52,7 @@ public function testUsers(): void $model->users()->attach($user2); /** @var Group $savedModel */ - $savedModel = Group::first(); + $savedModel = Group::all()->last(); $this->assertEquals($user1->name, $savedModel->users[0]->name); $this->assertEquals($user2->name, $savedModel->users[1]->name); } diff --git a/tests/Unit/Models/LocationTest.php b/tests/Unit/Models/LocationTest.php new file mode 100644 index 000000000..7c542359a --- /dev/null +++ b/tests/Unit/Models/LocationTest.php @@ -0,0 +1,66 @@ + 'Test location']); + $location->save(); + + $schedule = Schedule::factory()->create(); + $location->activeForSchedules()->attach($schedule); + + $location = Location::find($location->id); + $this->assertCount(1, $location->activeForSchedules); + } + + /** + * @covers \Engelsystem\Models\Location::shifts + */ + public function testShifts(): void + { + $location = new Location(['name' => 'Test location']); + $location->save(); + + /** @var Shift $shift */ + Shift::factory()->create(['location_id' => 1]); + + $location = Location::find(1); + $this->assertCount(1, $location->shifts); + } + + /** + * @covers \Engelsystem\Models\Location::neededAngelTypes + */ + public function testNeededAngelTypes(): void + { + /** @var Collection|Location[] $shifts */ + $shifts = Location::factory(3)->create(); + + $this->assertCount(0, Location::find(1)->neededAngelTypes); + + (NeededAngelType::factory()->make(['location_id' => $shifts[0]->id, 'shift_id' => null]))->save(); + (NeededAngelType::factory()->make(['location_id' => $shifts[0]->id, 'shift_id' => null]))->save(); + (NeededAngelType::factory()->make(['location_id' => $shifts[1]->id, 'shift_id' => null]))->save(); + (NeededAngelType::factory()->make(['location_id' => $shifts[2]->id, 'shift_id' => null]))->save(); + + $this->assertCount(2, Location::find(1)->neededAngelTypes); + $this->assertEquals(1, Location::find(1)->neededAngelTypes[0]->id); + $this->assertEquals(2, Location::find(1)->neededAngelTypes[1]->id); + $this->assertEquals(3, Location::find(2)->neededAngelTypes->first()->id); + $this->assertEquals(4, Location::find(3)->neededAngelTypes->first()->id); + } +} diff --git a/tests/Unit/Models/LogEntryTest.php b/tests/Unit/Models/LogEntryTest.php index 4aca5d833..944d6ec3a 100644 --- a/tests/Unit/Models/LogEntryTest.php +++ b/tests/Unit/Models/LogEntryTest.php @@ -5,6 +5,7 @@ namespace Engelsystem\Test\Unit\Models; use Engelsystem\Models\LogEntry; +use Engelsystem\Models\User\User; use Psr\Log\LogLevel; class LogEntryTest extends ModelTest @@ -14,6 +15,8 @@ class LogEntryTest extends ModelTest */ public function testFilter(): void { + $user = User::factory()->create(); + (new LogEntry(['level' => LogLevel::DEBUG, 'message' => 'Some users fault', 'user_id' => $user->id]))->save(); foreach ( [ 'I\'m an info' => LogLevel::INFO, @@ -31,9 +34,10 @@ public function testFilter(): void (new LogEntry(['level' => $level, 'message' => $message]))->save(); } - $this->assertCount(10, LogEntry::filter()); + $this->assertCount(11, LogEntry::filter()); $this->assertCount(3, LogEntry::filter(LogLevel::INFO)); $this->assertCount(1, LogEntry::filter('Oops')); + $this->assertCount(1, LogEntry::filter(null, $user->id)); /** @var LogEntry $first */ $first = LogEntry::filter()->first(); diff --git a/tests/Unit/Models/RoomTest.php b/tests/Unit/Models/RoomTest.php deleted file mode 100644 index 19eef000b..000000000 --- a/tests/Unit/Models/RoomTest.php +++ /dev/null @@ -1,50 +0,0 @@ - 'Test room']); - $room->save(); - - /** @var Shift $shift */ - Shift::factory()->create(['room_id' => 1]); - - $room = Room::find(1); - $this->assertCount(1, $room->shifts); - } - - /** - * @covers \Engelsystem\Models\Room::neededAngelTypes - */ - public function testNeededAngelTypes(): void - { - /** @var Collection|Room[] $shifts */ - $shifts = Room::factory(3)->create(); - - $this->assertCount(0, Room::find(1)->neededAngelTypes); - - (NeededAngelType::factory()->make(['room_id' => $shifts[0]->id, 'shift_id' => null]))->save(); - (NeededAngelType::factory()->make(['room_id' => $shifts[0]->id, 'shift_id' => null]))->save(); - (NeededAngelType::factory()->make(['room_id' => $shifts[1]->id, 'shift_id' => null]))->save(); - (NeededAngelType::factory()->make(['room_id' => $shifts[2]->id, 'shift_id' => null]))->save(); - - $this->assertCount(2, Room::find(1)->neededAngelTypes); - $this->assertEquals(1, Room::find(1)->neededAngelTypes[0]->id); - $this->assertEquals(2, Room::find(1)->neededAngelTypes[1]->id); - $this->assertEquals(3, Room::find(2)->neededAngelTypes->first()->id); - $this->assertEquals(4, Room::find(3)->neededAngelTypes->first()->id); - } -} diff --git a/tests/Unit/Models/SessionTest.php b/tests/Unit/Models/SessionTest.php new file mode 100644 index 000000000..3055f84f3 --- /dev/null +++ b/tests/Unit/Models/SessionTest.php @@ -0,0 +1,43 @@ +create(); + Session::create([ + 'id' => 'foo', + 'payload' => 'lorem ipsum', + 'user_id' => $user->id, + 'last_activity' => Carbon::now(), + ]); + Session::create([ + 'id' => 'bar', + 'last_activity' => Carbon::now(), + ]); + + $session = Session::find('foo'); + $this->assertNotNull($session); + $this->assertEquals('lorem ipsum', $session->payload); + $this->assertInstanceOf(User::class, $session->user); + + $session = Session::find('bar'); + $this->assertNull($session->user); + } +} diff --git a/tests/Unit/Models/Shifts/NeededAngelTypeTest.php b/tests/Unit/Models/Shifts/NeededAngelTypeTest.php index cd4d4ac2a..f3413021d 100644 --- a/tests/Unit/Models/Shifts/NeededAngelTypeTest.php +++ b/tests/Unit/Models/Shifts/NeededAngelTypeTest.php @@ -5,37 +5,43 @@ namespace Engelsystem\Test\Unit\Models\Shifts; use Engelsystem\Models\AngelType; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Shift; +use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Test\Unit\Models\ModelTest; class NeededAngelTypeTest extends ModelTest { /** - * @covers \Engelsystem\Models\Shifts\NeededAngelType::room + * @covers \Engelsystem\Models\Shifts\NeededAngelType::location * @covers \Engelsystem\Models\Shifts\NeededAngelType::shift + * @covers \Engelsystem\Models\Shifts\NeededAngelType::shiftType * @covers \Engelsystem\Models\Shifts\NeededAngelType::angelType */ public function testShift(): void { - /** @var Room $room */ - $room = Room::factory()->create(); + /** @var Location $location */ + $location = Location::factory()->create(); /** @var Shift $shift */ $shift = Shift::factory()->create(); + /** @var ShiftType $shiftType */ + $shiftType = ShiftType::factory()->create(); /** @var AngelType $angelType */ $angelType = AngelType::factory()->create(); $model = new NeededAngelType(); - $model->room()->associate($room); + $model->location()->associate($location); $model->shift()->associate($shift); + $model->shiftType()->associate($shiftType); $model->angelType()->associate($angelType); $model->count = 3; $model->save(); $model = NeededAngelType::find(1); - $this->assertEquals($room->id, $model->room->id); + $this->assertEquals($location->id, $model->location->id); $this->assertEquals($shift->id, $model->shift->id); + $this->assertEquals($shiftType->id, $model->shiftType->id); $this->assertEquals($angelType->id, $model->angelType->id); $this->assertEquals(3, $model->count); } diff --git a/tests/Unit/Models/Shifts/ScheduleTest.php b/tests/Unit/Models/Shifts/ScheduleTest.php index 29a514cc5..77481830a 100644 --- a/tests/Unit/Models/Shifts/ScheduleTest.php +++ b/tests/Unit/Models/Shifts/ScheduleTest.php @@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Models\Shifts; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\Schedule; use Engelsystem\Models\Shifts\ScheduleShift; use Engelsystem\Models\Shifts\Shift; @@ -17,10 +18,26 @@ class ScheduleTest extends ModelTest 'url' => 'https://foo.bar/schedule.xml', 'name' => 'Testing', 'shift_type' => 1, + 'needed_from_shift_type' => false, 'minutes_before' => 10, 'minutes_after' => 10, ]; + /** + * @covers \Engelsystem\Models\Shifts\Schedule::activeLocations + */ + public function testActiveLocations(): void + { + $schedule = new Schedule($this->data); + $schedule->save(); + + $location = Location::factory()->create(); + $schedule->activeLocations()->attach($location); + + $schedule = Schedule::find($schedule->id); + $this->assertCount(1, $schedule->activeLocations); + } + /** * @covers \Engelsystem\Models\Shifts\Schedule::scheduleShifts */ diff --git a/tests/Unit/Models/Shifts/ShiftTest.php b/tests/Unit/Models/Shifts/ShiftTest.php index 1abe76371..b129a1129 100644 --- a/tests/Unit/Models/Shifts/ShiftTest.php +++ b/tests/Unit/Models/Shifts/ShiftTest.php @@ -5,7 +5,7 @@ namespace Engelsystem\Test\Unit\Models\Shifts; use Engelsystem\Helpers\Carbon; -use Engelsystem\Models\Room; +use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Schedule; use Engelsystem\Models\Shifts\ScheduleShift; @@ -20,7 +20,7 @@ class ShiftTest extends ModelTest { /** * @covers \Engelsystem\Models\Shifts\Shift::shiftType - * @covers \Engelsystem\Models\Shifts\Shift::room + * @covers \Engelsystem\Models\Shifts\Shift::location * @covers \Engelsystem\Models\Shifts\Shift::createdBy * @covers \Engelsystem\Models\Shifts\Shift::updatedBy */ @@ -32,8 +32,8 @@ public function testShiftType(): void $user2 = User::factory()->create(); /** @var ShiftType $shiftType */ $shiftType = ShiftType::factory()->create(); - /** @var Room $room */ - $room = Room::factory()->create(); + /** @var Location $location */ + $location = Location::factory()->create(); $model = new Shift([ 'title' => 'Test shift', @@ -42,7 +42,7 @@ public function testShiftType(): void 'start' => Carbon::now(), 'end' => Carbon::now(), 'shift_type_id' => $shiftType->id, - 'room_id' => $room->id, + 'location_id' => $location->id, 'transaction_id' => '', 'created_by' => $user1->id, 'updated_by' => $user2->id, @@ -52,7 +52,7 @@ public function testShiftType(): void $model = Shift::find(1); $this->assertEquals($shiftType->id, $model->shiftType->id); - $this->assertEquals($room->id, $model->room->id); + $this->assertEquals($location->id, $model->location->id); $this->assertEquals($user1->id, $model->createdBy->id); $this->assertEquals($user2->id, $model->updatedBy->id); } @@ -67,10 +67,10 @@ public function testNeededAngelTypes(): void $this->assertCount(0, Shift::find(1)->neededAngelTypes); - (NeededAngelType::factory()->make(['shift_id' => $shifts[0]->id, 'room_id' => null]))->save(); - (NeededAngelType::factory()->make(['shift_id' => $shifts[0]->id, 'room_id' => null]))->save(); - (NeededAngelType::factory()->make(['shift_id' => $shifts[1]->id, 'room_id' => null]))->save(); - (NeededAngelType::factory()->make(['shift_id' => $shifts[2]->id, 'room_id' => null]))->save(); + (NeededAngelType::factory()->make(['shift_id' => $shifts[0]->id, 'location_id' => null]))->save(); + (NeededAngelType::factory()->make(['shift_id' => $shifts[0]->id, 'location_id' => null]))->save(); + (NeededAngelType::factory()->make(['shift_id' => $shifts[1]->id, 'location_id' => null]))->save(); + (NeededAngelType::factory()->make(['shift_id' => $shifts[2]->id, 'location_id' => null]))->save(); $this->assertCount(2, Shift::find(1)->neededAngelTypes); $this->assertEquals(1, Shift::find(1)->neededAngelTypes[0]->id); diff --git a/tests/Unit/Models/Shifts/ShiftTypeTest.php b/tests/Unit/Models/Shifts/ShiftTypeTest.php index f1413de1c..32147fae5 100644 --- a/tests/Unit/Models/Shifts/ShiftTypeTest.php +++ b/tests/Unit/Models/Shifts/ShiftTypeTest.php @@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Models\Shifts; +use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Schedule; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftType; @@ -11,6 +12,19 @@ class ShiftTypeTest extends ModelTest { + /** + * @covers \Engelsystem\Models\Shifts\ShiftType::neededAngelTypes + */ + public function testNeededAngelTypes(): void + { + $shiftType = new ShiftType(['name' => 'Another type', 'description' => '']); + $shiftType->save(); + + NeededAngelType::factory()->create(['shift_type_id' => 1]); + + $this->assertCount(1, ShiftType::find(1)->neededAngelTypes); + } + /** * @covers \Engelsystem\Models\Shifts\ShiftType::schedules */ diff --git a/tests/Unit/Models/Stub/BaseModelImplementation.php b/tests/Unit/Models/Stub/BaseModelImplementation.php deleted file mode 100644 index fc103c66d..000000000 --- a/tests/Unit/Models/Stub/BaseModelImplementation.php +++ /dev/null @@ -1,32 +0,0 @@ - */ - protected $fillable = ['foo']; // phpcs:ignore - - public int $saveCount = 0; - - public static ?QueryBuilder $queryBuilder = null; - - public function save(array $options = []): bool - { - $this->saveCount++; - return true; - } - - public static function query(): QueryBuilder - { - return self::$queryBuilder; - } -} diff --git a/tests/Unit/Models/User/UserTest.php b/tests/Unit/Models/User/UserTest.php index 8f2092eae..65e2f6b74 100644 --- a/tests/Unit/Models/User/UserTest.php +++ b/tests/Unit/Models/User/UserTest.php @@ -15,6 +15,7 @@ use Engelsystem\Models\OAuth; use Engelsystem\Models\Privilege; use Engelsystem\Models\Question; +use Engelsystem\Models\Session; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; use Engelsystem\Models\User\Contact; @@ -226,7 +227,7 @@ public function testIsFreeloader(): void $user->save(); $this->assertFalse($user->isFreeloader()); - ShiftEntry::factory()->create(['user_id' => $user->id]); + ShiftEntry::factory()->create(['user_id' => $user->id, 'freeloaded' => false]); ShiftEntry::factory()->create(['user_id' => $user->id, 'freeloaded' => true]); $this->assertFalse($user->isFreeloader()); @@ -335,8 +336,9 @@ public function testPrivileges(): void */ public function testNewsComments(): void { + News::factory()->create(); ($user = new User($this->data))->save(); - $newsComment = NewsComment::create(['news_id' => 0, 'text' => 'test comment', 'user_id' => $user->id]); + $newsComment = NewsComment::create(['news_id' => 1, 'text' => 'test comment', 'user_id' => $user->id]); $comments = $user->newsComments; $this->assertCount(1, $comments); @@ -375,6 +377,22 @@ public function testShiftEntries(): void $this->assertCount(2, $user->shiftEntries); } + /** + * @covers \Engelsystem\Models\User\User::sessions + */ + public function testSessions(): void + { + $user = new User($this->data); + $user->save(); + + Session::factory(2)->create(); + Session::factory(3)->create(['user_id' => $user->id]); + Session::factory(2)->create(); + Session::factory(4)->create(['user_id' => $user->id]); + + $this->assertCount(7, $user->sessions); + } + /** * @covers \Engelsystem\Models\User\User::worklogs */ diff --git a/tests/Unit/Renderer/Twig/Extensions/GlobalsTest.php b/tests/Unit/Renderer/Twig/Extensions/GlobalsTest.php index 4076ac4cf..1f3d01b6c 100644 --- a/tests/Unit/Renderer/Twig/Extensions/GlobalsTest.php +++ b/tests/Unit/Renderer/Twig/Extensions/GlobalsTest.php @@ -6,6 +6,7 @@ use Engelsystem\Config\Config; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\Carbon; use Engelsystem\Http\Request; use Engelsystem\Models\User\Settings; use Engelsystem\Models\User\User; @@ -17,9 +18,20 @@ class GlobalsTest extends ExtensionTest { use HasDatabase; + public static function setUpBeforeClass(): void + { + Carbon::setTestNow(Carbon::createFromFormat('Y-m-d', '2023-08-15')); + } + + public static function tearDownAfterClass(): void + { + Carbon::setTestNow(); + } + /** * @covers \Engelsystem\Renderer\Twig\Extensions\Globals::__construct * @covers \Engelsystem\Renderer\Twig\Extensions\Globals::getGlobals + * @covers \Engelsystem\Renderer\Twig\Extensions\Globals::getGlobalValues */ public function testGetGlobals(): void { @@ -35,7 +47,17 @@ public function testGetGlobals(): void $user = User::factory() ->has(Settings::factory(['theme' => 42])) ->create(); - $config = new Config(['theme' => 23, 'themes' => [42 => $theme, 23 => $theme2, 1337 => $theme3]]); + $config = new Config( + [ + 'event_start' => Carbon::createFromFormat('Y-m-d', '2023-08-13'), + 'theme' => 23, + 'themes' => [ + 42 => $theme, + 23 => $theme2, + 1337 => $theme3, + ], + ] + ); $auth->expects($this->exactly(4)) ->method('user') @@ -49,9 +71,11 @@ public function testGetGlobals(): void $this->app->instance('config', $config); $extension = new Globals($auth, $request); + $globals = $extension->getGlobals(); + + $this->assertGlobalsExists('day_of_event', 3, $globals); // No user - $globals = $extension->getGlobals(); $this->assertGlobalsExists('user', [], $globals); $this->assertGlobalsExists('user_messages', null, $globals); $this->assertGlobalsExists('request', $request, $globals); @@ -59,6 +83,7 @@ public function testGetGlobals(): void $this->assertGlobalsExists('theme', $theme2, $globals); // User + $extension = new Globals($auth, $request); $globals = $extension->getGlobals(); $this->assertGlobalsExists('user', $user, $globals); $this->assertGlobalsExists('user_messages', 0, $globals); @@ -66,15 +91,21 @@ public function testGetGlobals(): void $this->assertGlobalsExists('theme', $theme, $globals); // User with not available theme configured + $extension = new Globals($auth, $request); $user->settings->theme = 9999; $globals = $extension->getGlobals(); $this->assertGlobalsExists('themeId', 42, $globals); // Request query parameter + $extension = new Globals($auth, $request); $request->query->set('theme', 1337); $globals = $extension->getGlobals(); $this->assertGlobalsExists('user', [], $globals); $this->assertGlobalsExists('themeId', 1337, $globals); $this->assertGlobalsExists('theme', $theme3, $globals); + + // Second retrieval is loaded directly + $globals = $extension->getGlobals(); + $this->assertGlobalsExists('themeId', 1337, $globals); } } diff --git a/tests/Unit/Renderer/TwigServiceProviderTest.php b/tests/Unit/Renderer/TwigServiceProviderTest.php index 5f1e94ff8..37d1f50c1 100644 --- a/tests/Unit/Renderer/TwigServiceProviderTest.php +++ b/tests/Unit/Renderer/TwigServiceProviderTest.php @@ -111,7 +111,7 @@ public function testBoot(): void */ public function testRegisterTwigEngine(): void { - /** @var TwigEngine|MockObject $htmlEngine */ + /** @var TwigEngine|MockObject $twigEngine */ $twigEngine = $this->createMock(TwigEngine::class); /** @var TwigLoader|MockObject $twigLoader */ $twigLoader = $this->createMock(TwigLoader::class); diff --git a/tests/Unit/ServiceProviderTest.php b/tests/Unit/ServiceProviderTest.php index cd9c5afe9..8af9ab798 100644 --- a/tests/Unit/ServiceProviderTest.php +++ b/tests/Unit/ServiceProviderTest.php @@ -5,6 +5,7 @@ namespace Engelsystem\Test\Unit; use Engelsystem\Application; +use Engelsystem\Config\Config; use PHPUnit\Framework\MockObject\MockObject; abstract class ServiceProviderTest extends TestCase @@ -15,4 +16,18 @@ protected function getApp(array $methods = ['make', 'instance']): Application|Mo ->onlyMethods($methods) ->getMock(); } + + /** + * Creates an Application instance with a Config set as 'config'. + * Also sets up the instance as global Application instance. + * + * @param string[] $methods Names of the methods to mock + */ + protected function createAndSetUpAppWithConfig(array $methods = ['make', 'instance']): Application|MockObject + { + $app = $this->getApp($methods); + $app->instance('config', new Config([])); + Application::setInstance($app); + return $app; + } } diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php index ef3978c1d..4f849d7f4 100644 --- a/tests/Unit/TestCase.php +++ b/tests/Unit/TestCase.php @@ -61,7 +61,7 @@ protected function setUp(): void /** * @return Translator&MockObject */ - protected function mockTranslator(bool $mockImplementation = true): Translator + protected function mockTranslator(bool|callable $mockImplementation = true): Translator { $translator = $this->getMockBuilder(Translator::class) ->disableOriginalConstructor() @@ -70,7 +70,11 @@ protected function mockTranslator(bool $mockImplementation = true): Translator if ($mockImplementation) { $translator->method('translate') - ->willReturnCallback(fn(string $key, array $replace = []) => $key); + ->willReturnCallback( + is_callable($mockImplementation) + ? $mockImplementation + : fn(string $key, array $replace = []) => $key + ); } $this->app->instance('translator', $translator); diff --git a/tests/Utils/FormFieldAssert.php b/tests/Utils/FormFieldAssert.php new file mode 100644 index 000000000..397282736 --- /dev/null +++ b/tests/Utils/FormFieldAssert.php @@ -0,0 +1,79 @@ +]*name="$NAME"/s', [ + '$TAG' => $tag, + '$NAME' => $name, + ]); + } + + private static function makeCheckedCheckboxPattern(string $name): string + { + return strtr('/]*type="checkbox"[^>]*name="$NAME"[^>]*checked.*?/s', [ + '$NAME' => $name, + ]); + } +} diff --git a/tests/Utils/SignUpConfig.php b/tests/Utils/SignUpConfig.php new file mode 100644 index 000000000..55900be89 --- /dev/null +++ b/tests/Utils/SignUpConfig.php @@ -0,0 +1,65 @@ + false, + 'firstname' => false, + 'lastname' => false, + 'tshirt_size' => true, + 'mobile' => false, + 'dect' => false, + ]; + $config->set('registration_enabled', true); + $config->set('enable_password', true); + $config->set('enable_pronoun', true); + $config->set('goodie_type', GoodieType::Tshirt->value); + $config->set('tshirt_sizes', [ + 'S' => 'Small Straight-Cut', + 'M' => 'Medium Straight-Cut', + ]); + // disallow numeric values in username for tests + $config->set('username_regex', '/\d+/'); + $config->set('min_password_length', 3); + $config->set('theme', 0); + $config->set('enable_planned_arrival', true); + $config->set('enable_user_name', true); + $config->set('enable_mobile_show', true); + $config->set('enable_dect', true); + $config->set('required_user_fields', $requiredFields); + } + + public static function setMinimumConfig(Config $config): void + { + $requiredFields = [ + 'pronoun' => false, + 'firstname' => false, + 'lastname' => false, + 'tshirt_size' => true, + 'mobile' => false, + 'dect' => false, + ]; + $config->set('registration_enabled', true); + $config->set('enable_password', true); + $config->set('enable_pronoun', false); + $config->set('goodie_type', GoodieType::None->value); + // disallow numeric values in username for tests + $config->set('username_regex', '/\d+/'); + $config->set('min_password_length', 3); + $config->set('theme', 0); + $config->set('enable_planned_arrival', false); + $config->set('enable_user_name', false); + $config->set('enable_mobile_show', false); + $config->set('enable_dect', false); + $config->set('required_user_fields', $requiredFields); + } +} diff --git a/yarn.lock b/yarn.lock index 85b00703f..6a48d7893 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -17,6 +22,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5": version "7.20.14" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" @@ -61,6 +74,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -126,6 +149,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -141,6 +169,14 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -148,6 +184,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05" @@ -231,16 +274,33 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" @@ -274,11 +334,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.20.13", "@babel/parser@^7.20.7": +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.20.7": version "7.20.15" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -888,19 +962,28 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473" - integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.13" - "@babel/types" "^7.20.7" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" @@ -913,19 +996,40 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + +"@eslint/eslintrc@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d" + integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.4.0" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -933,10 +1037,15 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@humanwhocodes/config-array@^0.11.8": - version "0.11.8" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== +"@eslint/js@8.44.0": + version "8.44.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" + integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== + +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -993,6 +1102,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -1011,6 +1125,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -1019,6 +1138,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -1047,6 +1174,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@one-ini/wasm@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" + integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== + "@popperjs/core@^2.11.6": version "2.11.6" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" @@ -1290,11 +1422,16 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: +acorn@^8.5.0, acorn@^8.7.1: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" + integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== + adjust-sourcemap-loader@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" @@ -1456,6 +1593,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1498,7 +1642,7 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz#022225b91200589196b814b51b1bbe45144cf74f" integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1592,7 +1736,12 @@ colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -commander@^2.19.0, commander@^2.20.0: +commander@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" + integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== + +commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -1824,20 +1973,20 @@ domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" -editorconfig-checker@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/editorconfig-checker/-/editorconfig-checker-5.0.1.tgz#cb4bf7a9b80f1b63b141b8897d7cc326fce0d3ae" - integrity sha512-6hXq9VVDkyCxVYKdGtIj+yhVR1fi/6W6Ykz/+kItLPARulJvr2/VXgWZ5OGWx1UYm2RD6XOzWyx1JF6DLgQ/8Q== +editorconfig-checker@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/editorconfig-checker/-/editorconfig-checker-5.1.1.tgz#97b314647068b68bf1210f3d6b597c1fc9b20ded" + integrity sha512-IBE7K48+6jLlhfmJ0TOTjogvO9Asw+4B8CoKn71KtZ8Qrvc7/WgrMyJxQRSdq6VGdh+D65/6JuQuHioMs2O1sw== -editorconfig@^0.15.0: - version "0.15.3" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" - integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== +editorconfig@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.3.tgz#9a1893d249bc1b8c49a1b421d0e6bf81c8fb86f2" + integrity sha512-SLHUig+v3PpjlCGenNDSMVj5caWTJ+aDXaqR1ucZCbXcotV3D7+ycT1jwbICxiPC6gju/rS+iRw8SC7kQukSig== dependencies: - commander "^2.19.0" - lru-cache "^4.1.5" - semver "^5.6.0" - sigmund "^1.0.1" + "@one-ini/wasm" "0.1.1" + commander "^11.0.0" + minimatch "9.0.1" + semver "^7.5.3" electron-to-chromium@^1.4.284: version "1.4.286" @@ -1894,13 +2043,13 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-editorconfig@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-editorconfig/-/eslint-plugin-editorconfig-4.0.2.tgz#8f0bab78276ceed2f7fc6976c2c612021e4217a8" - integrity sha512-soh4tQsztLlX2c9xb+HjeaZiF6Os/S6An5ZsF1fYCm5mZYm+5m7f3y+eUWqJpT3sKi0JjxuRLtxE/VpmLtDzfA== +eslint-plugin-editorconfig@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-editorconfig/-/eslint-plugin-editorconfig-4.0.3.tgz#d0ab6179f16cc487eeacb4c9f54cf867b2841d6e" + integrity sha512-5YeDxm6mlv75DrTbRBK9Jw2ogqhjiz8ZCvv9bkuz/MXq0603q9FpQvQlamtas4bX1Gji4YcksY7dq7stPeGaLQ== dependencies: - editorconfig "^0.15.0" - eslint "^8.0.1" + editorconfig "^1.0.2" + eslint "^8.40.0" klona "^2.0.4" eslint-scope@5.1.1: @@ -1911,38 +2060,34 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== +eslint-scope@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" + integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: +eslint-visitor-keys@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@^8.0.1, eslint@^8.33.0: - version "8.33.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.33.0.tgz#02f110f32998cb598c6461f24f4d306e41ca33d7" - integrity sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA== - dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== + +eslint@^8.40.0, eslint@^8.44.0: + version "8.44.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500" + integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.1.0" + "@eslint/js" "8.44.0" + "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" ajv "^6.10.0" @@ -1951,49 +2096,46 @@ eslint@^8.0.1, eslint@^8.33.0: debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" + eslint-scope "^7.2.0" + eslint-visitor-keys "^3.4.1" + espree "^9.6.0" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" find-up "^5.0.0" glob-parent "^6.0.2" globals "^13.19.0" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-sdsl "^4.1.4" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" + optionator "^0.9.3" strip-ansi "^6.0.1" strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.4.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" - integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== +espree@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f" + integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.1" -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" @@ -2181,10 +2323,10 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== has-flag@^3.0.0: version "3.0.0" @@ -2346,11 +2488,6 @@ jest-worker@^29.1.2: merge-stream "^2.0.0" supports-color "^8.0.0" -js-sdsl@^4.1.4: - version "4.3.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" - integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2474,14 +2611,6 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lru-cache@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -2532,6 +2661,13 @@ mini-css-extract-plugin@^2.7.2: dependencies: schema-utils "^4.0.0" +minimatch@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -2544,10 +2680,10 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nanoid@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" - integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== natural-compare@^1.4.0: version "1.4.0" @@ -2593,17 +2729,17 @@ once@^1.3.0: dependencies: wrappy "1" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" p-limit@^2.2.0: version "2.3.0" @@ -2944,12 +3080,12 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.14, postcss@^8.4.17, postcss@^8.4.19, postcss@^8.4.21: - version "8.4.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== +postcss@^8.2.14, postcss@^8.4.17, postcss@^8.4.19, postcss@^8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: - nanoid "^3.3.4" + nanoid "^3.3.6" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -2963,11 +3099,6 @@ prettier@^2.8.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.3.tgz#ab697b1d3dd46fb4626fbe2f543afe0cc98d8632" integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw== -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== - punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -3035,11 +3166,6 @@ regex-parser@^2.2.11: resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - regexpu-core@^5.2.1: version "5.2.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.2.tgz#3e4e5d12103b64748711c3aad69934d7718e75fc" @@ -3166,20 +3292,10 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== +semver@7.5.3, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^7.3.8, semver@^7.5.3: + version "7.5.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" + integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== dependencies: lru-cache "^6.0.0" @@ -3209,11 +3325,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -sigmund@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g== - source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -3502,21 +3613,11 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== - yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"