diff --git a/.env.dist b/.env.dist index e7c8893..2deb8a3 100644 --- a/.env.dist +++ b/.env.dist @@ -54,3 +54,14 @@ GRAYLOG_CLIENT_CERT= GRAYLOG_CLIENT_KEY= IMPORT_TARGET_DIR=data + +# Sentry (you can add your local sentry dsn here for easier debugging.) +SENTRY_DSN="" + +# XDebug config +XDEBUG_CONFIG="remote_host=" +PHP_IDE_CONFIG="" + +###> symfony/mailer ### +# MAILER_DSN=smtp://mailer:1025 +###< symfony/mailer ### diff --git a/.env.test b/.env.test index 35dc886..2876d08 100644 --- a/.env.test +++ b/.env.test @@ -42,4 +42,4 @@ GRAYLOG_CLIENT_KEY= IMPORT_TARGET_DIR=data # Sentry -SENTRY_DSN=https://0a3b51c0452b4e1fa0bc6c54db908f59@sentry.digio.ch/26 +SENTRY_DSN="" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7f94ea..324448f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,9 +15,9 @@ jobs: - name: setup test env run: cp -f .env.test .env - name: Build test image - run: docker-compose -f docker/docker-compose.yml build --build-arg BUILD_TEST=1 healthcheck-core + run: docker compose -f docker/docker-compose.yml build --build-arg BUILD_TEST=1 healthcheck-core - name: Start compose - run: docker-compose -f docker/docker-compose.yml up -d + run: docker compose -f docker/docker-compose.yml up -d - name: Install dependencies run: docker exec --user=$(id -u) healthcheck-core-local composer install --no-interaction --no-scripts - name: Lint PSR12 @@ -36,6 +36,6 @@ jobs: - name: docker cleanup run: | cp -f .env.test .env - docker-compose -f docker/docker-compose.yml down + docker compose -f docker/docker-compose.yml down docker image prune -f docker volume prune -f diff --git a/README.md b/README.md index c3929d6..cc5a196 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ We use the PSR-12 PHP standard. You can check your code using the following comm `docker exec healthcheck-core-local php vendor/bin/phpcs --standard=PSR12 --report=full --ignore=src/Migrations/ --runtime-set ignore_warnings_on_exit 1 src/` +Auto linting: +`docker exec healthcheck-core-local php vendor/bin/phpcbf --standard=PSR12 --report=full --ignore=src/Migrations/ --runtime-set ignore_warnings_on_exit 1 src/` + #### Running Tests To run tests locally make sure to use the `env.test` instead of the created `.env`. You can do that by replacing the @@ -85,4 +88,4 @@ restart the healthcheck-core service container in order for the changes to take Once you are up and running with the new env run: -`docker exec healthcheck-core-local php bin/phpunit` \ No newline at end of file +`docker exec healthcheck-core-local php bin/phpunit` diff --git a/composer.json b/composer.json index 7b86645..5c0a113 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "symfony/dotenv": "5.1.*", "symfony/flex": "1.17.*", "symfony/framework-bundle": "5.1.*", + "symfony/mailer": "5.1.*", "symfony/polyfill-intl-messageformatter": "^1.18", "symfony/security-bundle": "5.1.*", "symfony/serializer": "5.1.*", diff --git a/composer.lock b/composer.lock index 924b711..3aca5a0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5627d0de7954d2d77d81c1a216c42b16", + "content-hash": "36f0bfa5bce9a9a94a5ccd9aa6ea841f", "packages": [ { "name": "clue/stream-filter", @@ -1533,6 +1533,70 @@ ], "time": "2022-05-23T21:33:49+00:00" }, + { + "name": "egulias/email-validator", + "version": "2.1.25", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0dbf5d78455d4d6a41d186da50adc1122ec066f4", + "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.10" + }, + "require-dev": { + "dominicsayers/isemail": "^3.0.7", + "phpunit/phpunit": "^4.8.36|^7.5.15", + "satooshi/php-coveralls": "^1.0.1" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2020-12-29T14:50:06+00:00" + }, { "name": "friendsofphp/proxy-manager-lts", "version": "v1.0.16", @@ -1616,7 +1680,7 @@ "version": "v2.13.0", "source": { "type": "git", - "url": "git@github.com:maxmind/GeoIP2-php.git", + "url": "https://github.com/maxmind/GeoIP2-php.git", "reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23" }, "dist": { @@ -5273,6 +5337,155 @@ ], "time": "2023-05-27T08:06:30+00:00" }, + { + "name": "symfony/mailer", + "version": "v5.1.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "3c7ab7a402acdb453dcdd6e0b2982caacfcc9b9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/3c7ab7a402acdb453dcdd6e0b2982caacfcc9b9f", + "reference": "3c7ab7a402acdb453dcdd6e0b2982caacfcc9b9f", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10", + "php": ">=7.2.5", + "psr/log": "~1.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/http-kernel": "<4.4" + }, + "require-dev": { + "symfony/amazon-mailer": "^4.4|^5.0", + "symfony/google-mailer": "^4.4|^5.0", + "symfony/http-client-contracts": "^1.1|^2", + "symfony/mailchimp-mailer": "^4.4|^5.0", + "symfony/mailgun-mailer": "^4.4|^5.0", + "symfony/messenger": "^4.4|^5.0", + "symfony/postmark-mailer": "^4.4|^5.0", + "symfony/sendgrid-mailer": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "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": "2021-01-27T10:01:46+00:00" + }, + { + "name": "symfony/mime", + "version": "v5.1.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "d7d899822da1fa89bcf658e8e8d836f5578e6f7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/d7d899822da1fa89bcf658e8e8d836f5578e6f7a", + "reference": "d7d899822da1fa89bcf658e8e8d836f5578e6f7a", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10", + "symfony/dependency-injection": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "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": "2021-01-27T10:01:46+00:00" + }, { "name": "symfony/options-resolver", "version": "v5.4.21", diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml new file mode 100644 index 0000000..a495759 --- /dev/null +++ b/config/packages/mailer.yaml @@ -0,0 +1,3 @@ +framework: + mailer: + dsn: 'null://null' # Disable until further notice '%env(MAILER_DSN)%' diff --git a/config/packages/sentry.yaml b/config/packages/sentry.yaml index 1803ef5..03ac559 100644 --- a/config/packages/sentry.yaml +++ b/config/packages/sentry.yaml @@ -1,8 +1,9 @@ sentry: dsn: "%env(SENTRY_DSN)%" options: + environment: "%env(APP_ENV)%" before_send: 'sentry.callback.before_send' - # send_default_pii: true + send_default_pii: true services: sentry.callback.before_send: diff --git a/config/routes/app_routes.yml b/config/routes/app_routes.yml index 2f50377..1218533 100644 --- a/config/routes/app_routes.yml +++ b/config/routes/app_routes.yml @@ -10,3 +10,21 @@ quap: resource: "apps/quap.yml" prefix: /quap name_prefix: quap_ + +# General routes +general: + resource: "apps/general.yml" + prefix: /general + name_prefix: general_ + +# Census +census: + resource: "apps/census.yml" + prefix: /census + name_prefix: census_ + +# Gamification +gamification: + resource: "apps/gamification.yml" + prefix: /gamification + name_prefix: gamification_ diff --git a/config/routes/apps/census.yml b/config/routes/apps/census.yml new file mode 100644 index 0000000..7ee376e --- /dev/null +++ b/config/routes/apps/census.yml @@ -0,0 +1,14 @@ +preview: + path: /preview + methods: GET + controller: App\Controller\Api\Apps\CensusController::getPreview + +getFilter: + path: /filter + methods: GET + controller: App\Controller\Api\Apps\CensusController:getFilterData + +postFilter: + path: /filter + methods: POST + controller: App\Controller\Api\Apps\CensusController:postFilterData diff --git a/config/routes/apps/gamification.yml b/config/routes/apps/gamification.yml new file mode 100644 index 0000000..6e18c74 --- /dev/null +++ b/config/routes/apps/gamification.yml @@ -0,0 +1,34 @@ +groupChange: + path: /group-change + methods: POST + controller: App\Controller\Api\GamificationController::postGroupChange + +logCardLayer: + path: /card-layer + methods: PATCH + controller: App\Controller\Api\GamificationController::usedCardLayer + +logDataFilter: + path: /data-filter + methods: PATCH + controller: App\Controller\Api\GamificationController::usedDataFilter + +logTimeFilter: + path: /time-filter + methods: PATCH + controller: App\Controller\Api\GamificationController::usedTimeFilter + +getPersonProfile: + path: /person + methods: GET + controller: App\Controller\Api\GamificationController::getUserProfile + +reset: + path: /reset + methods: POST + controller: App\Controller\Api\GamificationController::resetGamification + +betaAccess: + path: /beta + methods: PATCH + controller: App\Controller\Api\GamificationController::requestBetaAccess diff --git a/config/routes/apps/general.yml b/config/routes/apps/general.yml new file mode 100644 index 0000000..59565f6 --- /dev/null +++ b/config/routes/apps/general.yml @@ -0,0 +1,4 @@ +post_role_overview_filter: + path: /filter + methods: POST + controller: App\Controller\Api\GroupSettingsController::postRoleOverviewFilter diff --git a/config/routes/apps/widgets.yml b/config/routes/apps/widgets.yml index fb0f1d8..b98fb6d 100644 --- a/config/routes/apps/widgets.yml +++ b/config/routes/apps/widgets.yml @@ -43,3 +43,29 @@ geo_location: path: /geo-location methods: GET controller: App\Controller\Api\Apps\Widgets\GeoLocationController::getGeoLocations + +role_overview: + path: /role-overview + methods: GET + controller: App\Controller\Api\Apps\Widgets\RoleOverviewController:getRoleOverview + +census_table: + path: /census-table + methods: GET + controller: App\Controller\Api\Apps\CensusController::getTableData + +census_members: + path: /census-members + methods: GET + controller: App\Controller\Api\Apps\CensusController::getMembersData + +census_development: + path: /census-development + methods: GET + controller: App\Controller\Api\Apps\CensusController::getDevelopmentData + +census_treemap: + path: /census-treemap + methods: GET + controller: App\Controller\Api\Apps\CensusController::getTreemapData + diff --git a/config/services.yaml b/config/services.yaml index 06af17e..73e6711 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -27,7 +27,7 @@ services: # as action arguments even if you don't extend any base controller class App\Controller\: resource: '../src/Controller' - tags: ['controller.service_arguments'] + tags: [ 'controller.service_arguments' ] # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones @@ -132,10 +132,24 @@ services: tags: - { name: 'widget.aggregator', key: 'widget.quap' } + App\Service\Aggregator\RoleAggregator: + tags: + - { name: 'widget.aggregator', key: 'widget.roles' } + # This must be the last aggregator App\Service\Aggregator\DateAggregator: tags: - { name: 'widget.aggregator', key: 'general.date' } App\Service\Aggregator\AggregatorRegistry: - arguments: [!tagged { tag: 'widget.aggregator', index_by: 'key' }] + arguments: [ !tagged { tag: 'widget.aggregator', index_by: 'key' } ] + + App\Service\GroupStructureAPIService: + arguments: + $apiToken: '%env(GROUP_STRUCTURE_TOKEN)%' + $url: '%env(GROUP_STRUCTURE_URL)%' + + App\Service\Census\CensusAPIService: + arguments: + $apiToken: '%env(CENSUS_TOKEN)%' + $url: '%env(CENSUS_URL)%' diff --git a/crontab b/crontab index 89059cb..8ef3e02 100644 --- a/crontab +++ b/crontab @@ -3,3 +3,6 @@ # Run every 3 months at 00:00 (AM) 0 0 1 */3 * /usr/local/bin/php /srv/bin/console app:import-geo-addresses + +# Run once a year on the first of february at 01:00 (AM) +0 1 1 2 * /usr/local/bin/php /srv/bin/console app:fetch-census diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7bad977..3b4e03e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -59,6 +59,20 @@ services: healthcheck-dev: ipv4_address: 172.20.0.5 + ###> symfony/mailer ### + mailer: + image: axllent/mailpit + ports: + - "1025:1025" + - "8025:8025" + environment: + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 + networks: + healthcheck-dev: + ipv4_address: 172.20.0.6 + ###< symfony/mailer ### + networks: healthcheck-dev: driver: bridge diff --git a/imports/gamification.json b/imports/gamification.json new file mode 100644 index 0000000..d6e56ca --- /dev/null +++ b/imports/gamification.json @@ -0,0 +1,283 @@ +{ + "levels": [ + { + "key": 0, + "next_key": 1, + "type": 0, + "required": 0, + "de_title": "Newcomer*in", + "fr_title": "", + "it_title": "" + }, + { + "key": 1, + "next_key": 2, + "type": 0, + "required": 3, + "de_title": "Datenentdecker*in", + "fr_title": "", + "it_title": "" + }, + { + "key": 2, + "next_key": 3, + "type": 0, + "required": 3, + "de_title": "Statistikforscher*in", + "fr_title": "", + "it_title": "" + }, + { + "key": 3, + "next_key": null, + "type": 0, + "required": 2, + "de_title": "Healthcheck Guru", + "fr_title": "", + "it_title": "" + } + ], + "goals": [ + { + "key": "FIRST_LOGIN", + "level": 1, + "required": true, + "de": { + "title": "Erstes Login", + "information": "Login Information (Testing: Ist immer erfüllt)", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "CARD_LAYERS", + "level": 1, + "required": true, + "de": { + "title": "Unterschiedliche Layer der Karte benutzt", + "information": "Kartenlayer information", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "DATAFILTER", + "level": 1, + "required": false, + "de": { + "title": "Datenfilter in Übersicht benutzt", + "information": "Datenfilter information", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "TIMEFILTER", + "level": 1, + "required": false, + "de": { + "title": "zeitbereichsansicht benutzt", + "information": "Zeitbereichsansicht information (Im Zeitfilter Zeitspanne auswählen)", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "SHARE_WITH_PARENTS", + "level": 1, + "required": false, + "de": { + "title": "Freigabe für übergeordnete Ebene.", + "information": "EL Freigabe information (EL an Kanton freigeben)", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "EL_FILL_OUT", + "level": 2, + "required": true, + "de": { + "title": "Alle Erfolgslogik Kasten einer Gruppe Ausgefüllt.", + "information": "EL alle Kasten information (WIP)", + "help": "EL alle kasten ausfüllen hilfe" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "SHARE_1", + "level": 2, + "required": false, + "de": { + "title": "Freigabe für mindestens eine andere Person.", + "information": "Freigabe1 information", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "EL_IRRELEVANT", + "level": 2, + "required": false, + "de": { + "title": "In der Erfolgslogik nicht relevante Fragen ausgebledet", + "information": "EL irrelevant information", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "EL_CHANGE", + "level": 2, + "required": false, + "de": { + "title": "Einen Erfolgslogik Kasten überarbeitet.", + "information": "El überarbeiten information", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "EL_IMPROVE", + "level": 3, + "required": true, + "de": { + "title": "Verbesserung in einem Erfolgslogik Kasten.", + "information": "El verbessern information", + "help": "(nicht nötig)" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "LOGIN_FOUR_A_YEAR", + "level": 3, + "required": false, + "de": { + "title": "Vier mal innerhalb eines Jahres eingeloggt.", + "information": "Login information", + "help": "Du hast dich % mal eingeloggt" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + }, + { + "key": "SHARE_THREE", + "level": 3, + "required": false, + "de": { + "title": "Eine Abteilung an 3 oder mehr Personen freigegeben.", + "information": "Freigabe3 information", + "help": "Du hast % von 3 Freigaben" + }, + "fr": { + "title": "french", + "information": "", + "help": "" + }, + "it": { + "title": "italian", + "information": "", + "help": "" + } + } + ] +} + diff --git a/imports/questionnaire_imports.json b/imports/questionnaire_imports.json index 7f2f5ae..0f59d44 100644 --- a/imports/questionnaire_imports.json +++ b/imports/questionnaire_imports.json @@ -3680,9 +3680,9 @@ "url": "https://pfadi.swiss/it/formazione/" } ], - "help_de": "Folgende in der Regel (inter)kantonal organisierten Kurse werden zurzeit in eurem KV nicht angeboten:\n...\n[auflisten anhand Abfrage]\n...", - "help_fr": "Les cours suivants, qui sont normalement organisés au niveau (inter)cantonal, ne sont actuellement pas proposés au sein de votre AC :\n...\n[énumérer à l'aide de la consultation]\n…", - "help_it": "I corsi seguenti, che sono normalmente organizzati a livello (inter)cantonale, non sono attualmente proposti in seno alla vostra AC: ... [elencare grazie alla consultazione] ..." + "help_de": "Zu den Kursen, die normalerweise von den KVs und Regionen organisiert werden, gehören die Vorbasis-, Basis- und Aufbaukurse, die Spezialisierungen und Einführungskurse.", + "help_fr": "Les cours habituellement organisés par les AC et régions sont les cours préparatoires, les cours de base et de responsable d’unité, les spécialisations et les cours d’introduction.", + "help_it": "I corsi solitamente organizzati dai cantoni sono corsi preparatori, Base, Campo, corsi di intruduzione e specializzazioni." }, "question_de": "In Zusammenarbeit mit anderen Kantonalverbänden bieten wir unseren Leitenden alle im Ausbildungsmodell vorgesehenen kantonalen Kurse an.", "question_fr": "En collaboration avec d'autres associations cantonales, nous proposons à nos responsables tous les cours cantonaux figurant dans le modèle de formation.", diff --git a/run-import.sh b/run-import.sh index 22cff94..a171efa 100755 --- a/run-import.sh +++ b/run-import.sh @@ -10,7 +10,10 @@ set -e /usr/local/bin/php /srv/bin/console app:map-peoples-addresses /usr/local/bin/php /srv/bin/console app:quap:import-questionnaire +/usr/local/bin/php /srv/bin/console app:fetch-all-groups # Also fetches Group Meeting points. + /usr/local/bin/php /srv/bin/console app:aggregate-data /usr/local/bin/php /srv/bin/console app:quap:compute-answers /usr/local/bin/php /srv/bin/console app:compute-permissions + diff --git a/src/Command/AggregateCommand.php b/src/Command/AggregateCommand.php index 8ba8c2d..56bcc16 100644 --- a/src/Command/AggregateCommand.php +++ b/src/Command/AggregateCommand.php @@ -45,8 +45,7 @@ protected function configure() $this ->setName('app:aggregate-data') ->setDescription('Aggregate data') - ->addArgument('specific', InputArgument::OPTIONAL) - ; + ->addArgument('specific', InputArgument::OPTIONAL); } /** @@ -66,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln(['Start aggregation...']); $io = new SymfonyStyle($input, $output); - $aggregators = $this->aggregatorRegistry->getAggregators(); + $aggregators = $this->aggregatorRegistry->getAggregators(); $specific = $input->getArgument('specific'); diff --git a/src/Command/FetchAllGroupsCommand.php b/src/Command/FetchAllGroupsCommand.php new file mode 100644 index 0000000..ae40dda --- /dev/null +++ b/src/Command/FetchAllGroupsCommand.php @@ -0,0 +1,267 @@ +em = $em; + $this->apiService = $apiService; + $this->statisticGroupRepository = $statisticGroupRepository; + $this->groupTypeRepository = $groupTypeRepository; + $this->groupRepository = $groupRepository; + $this->geoLocationRepository = $geoLocationRepository; + $this->gelfLogger = $gelfLogger; + parent::__construct(); + } + + + public function configure() + { + $this->setName('app:fetch-all-groups') + ->setDescription('Fetches all groups from MiData (' . $this->apiService->getUrl() . ') using the groups endpoint.'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->io = new SymfonyStyle($input, $output); + + $start = microtime(true); + $this->geoLocationRepository->deleteAll(); + $this->statisticGroupRepository->deleteAll(); + + $batchedStatisticsRepository = new BatchedRepository($this->statisticGroupRepository); + $batchedGeoRepository = new BatchedRepository($this->geoLocationRepository); + $this->fetchGroupRecursive(2, null, null, $batchedStatisticsRepository, $batchedGeoRepository); + $batchedStatisticsRepository->flush(); + $batchedGeoRepository->flush(); + + //$this->fetchAllRemaining(); + $this->logMissingBranches(); + + $this->stats[0] = microtime(true) - $start; + return Command::SUCCESS; + } + + private function logMissingBranches() + { + $groups = $this->statisticGroupRepository->findBy(['parent_group' => null]); + foreach ($groups as $group) { + if ($group->getId() === 2) { + continue; + } + $this->gelfLogger->warning(new SimpleLogMessage($this->recursiveGetChildrenAsJsonString($group))); + $this->io->warning('This branch is missing the parent: {' . $this->recursiveGetChildrenAsJsonString($group) . '}'); + } + } + + /** + * Return all children of a group as JSON like string + */ + private function recursiveGetChildrenAsJsonString(StatisticGroup $group) + { + $result = '"' . $group->getId(); + $children = $group->getChildren(); + if (sizeof($children) === 0) { + return $result . '": null,'; + } + $result .= '": {'; + foreach ($children as $child) { + $result .= $this->recursiveGetChildrenAsJsonString($child); + } + return $result . '},'; + } + + /** + * Brutforces through all groups and fetches them. Takes longer because you have to wait for every 404 and you don't + * know which index is the last. Don't use unless necessary + */ + private function fetchAllRemaining() + { + for ($i = 2; $i < 11600; $i++) { + $group = $this->statisticGroupRepository->findOneBy(['id' => $i]); + if (is_null($group)) { + $result = $this->fetchGroup($i); + $content = $result->getContent()['groups'][0]; + $statisticGroup = new StatisticGroup(); + $rawGroup = $content; + + $name = trim($rawGroup['name']); + $parentGroup = $rawGroup['links']['parent'] ?? null; + if (!is_null($parentGroup)) { + $parentGroup = $this->statisticGroupRepository->findOneBy(['id' => $parentGroup]); + } + /** @var GroupType $groupType */ + $groupType = $this->groupTypeRepository->findOneBy(['deLabel' => $rawGroup['group_type']]); + $statisticGroup->setId($i); + $statisticGroup->setName($name); + $statisticGroup->setParentGroup($parentGroup); + $statisticGroup->setGroupType($groupType); + $this->io->writeln('Adding ' . $i); + $this->statisticGroupRepository->add($statisticGroup); + } + } + } + + private function fetchGroup(int $id, bool $stopOnFail = false) + { + try { + $result = $this->apiService->getGroup($id); + if ($result->getStatusCode() !== 200) { + $this->io->error([ + 'API call for group with id ' . $id . ' failed!', + 'HTTP status code: ' . $result->getStatusCode() + ]); + } + return $result; + } catch (ClientException $e) { + $this->io->error('Fetch for ' . $id . ' resultet in an error (' . $e->getCode() . ')'); + if ($stopOnFail) { + $this->gelfLogger('Fetching Group (' . $id . ') failed with error code (' . $e->getCode() . '), stopped fetching.'); + throw new ApiException('Fetching Group (' . $id . ') failed, stopping fetching.'); + } + return null; + } + } + + /** + * Fetch a group and its children recursively + */ + private function fetchGroupRecursive(int $id, ?StatisticGroup $parent, ?StatisticGroup $canton, BatchedRepository $batchedStatisticsRepository, BatchedRepository $batchedGeoRepository) + { + $result = $this->fetchGroup($id, true); + $statisticGroup = new StatisticGroup(); + + $rawGroup = $result->getContent()['groups'][0]; + $name = trim($rawGroup['name']); + $children = $rawGroup['links']['children'] ?? []; + /** + * Sadly the group type we get from the regular group endpoint (statistic_group) is not the same one, + * that we get from the group type endpoint (JSON file). So here we have to map the german label of a group type + * to the group type key. + * A side effect of this is that we can get Group types which just don't exist. (eg. Erziehungsberechtigter) + * To prevent this from destroying this function we must ignore such groups. + * @var GroupType $groupType + */ + $groupType = $this->groupTypeRepository->findOneBy(['deLabel' => $rawGroup['group_type']]); + $invalid_group_type = is_null($groupType); + if ($invalid_group_type) { + $this->gelfLogger->warning( + new SimpleLogMessage('Invalid grouptype detected, skipping group: ' . $id) + ); + return; + } + + $statisticGroup->setId($id); + $statisticGroup->setCanton($canton); + $statisticGroup->setName($name); + $statisticGroup->setParentGroup($parent); + $statisticGroup->setGroupType($groupType); + $batchedStatisticsRepository->add($statisticGroup); + $this->stats[1]++; + + /** @var array $geoLocations */ + $geoLocations = $result->getContent()['linked']['geolocations'] ?? []; + $this->createGeoLocations($statisticGroup, $geoLocations, $batchedGeoRepository); + $this->fillHealthGroupWithStatisticGroup($statisticGroup); + + if ($groupType->getGroupType() === GroupType::CANTON) { + $canton = $statisticGroup; + } + foreach ($children as $child) { + $this->fetchGroupRecursive($child, $statisticGroup, $canton, $batchedStatisticsRepository, $batchedGeoRepository); + } + } + + private function createGeoLocations(StatisticGroup $group, ?array $geoLocations, BatchedRepository $batchedGeoRepository) + { + if (is_null($geoLocations)) { + return; + } + foreach ($geoLocations as $rawGeoLocation) { + $geoLocation = new GroupGeoLocation(); + $geoLocation->setId($rawGeoLocation['id']); + $geoLocation->setGroup($group); + $geoLocation->setLat($rawGeoLocation['lat']); + $geoLocation->setLong($rawGeoLocation['long']); + $batchedGeoRepository->add($geoLocation); + } + } + + /** + * Fills in the parent and canton of the equivalent Health group with the Statistics group + */ + private function fillHealthGroupWithStatisticGroup(StatisticGroup $statisticGroup) + { + /** @var Group $group */ + $group = $this->groupRepository->findOneBy(['id' => $statisticGroup->getId()]); + if (!is_null($group)) { + if (!is_null($statisticGroup->getCanton())) { + $group->setCantonId($statisticGroup->getCanton()->getId()); + $group->setCantonName($statisticGroup->getCanton()->getName()); + } + if (!is_null($statisticGroup->getParentGroup())) { + /** @var Group $parent */ + $parent = $this->groupRepository->findOneBy(['id' => $statisticGroup->getParentGroup()->getId()]); + if (!is_null($parent)) { + $group->setParentGroup($parent); + } + } + } + } + + public function getStats(): CommandStatistics + { + return new CommandStatistics($this->stats[0], 'Fetched ' . $this->stats[1] . ' Groups in ' . number_format($this->stats[0], 2) . ' Seconds', $this->stats[1]); + } +} diff --git a/src/Command/FetchCensusCommand.php b/src/Command/FetchCensusCommand.php new file mode 100644 index 0000000..496d730 --- /dev/null +++ b/src/Command/FetchCensusCommand.php @@ -0,0 +1,105 @@ +apiService = $apiService; + $this->censusGroupRepository = $censusGroupRepository; + $this->groupTypeRepository = $groupTypeRepository; + parent::__construct(); + } + + + public function configure() + { + $this->setName('app:fetch-census') + ->setDescription('Fetch and aggregate census data'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->start = microtime(true); + $this->io = new SymfonyStyle($input, $output); + + $year = (int) date('Y'); + $minYear = $year - 6; + // Fetch groups + while ($year > $minYear) { + $this->io->writeln('year ' . $year); + $rawCensusData = $this->apiService->getCensusData($year); + $rawCensusGroups = $rawCensusData->getContent()['census_evaluations']['groups']; + foreach ($rawCensusGroups as $rawCensusGroup) { + $exists = $this->censusGroupRepository->findOneBy(['group_id' => $rawCensusGroup['group_id'], 'year' => $year]); + if (is_null($exists)) { + $this->mapRawCensusGroupToCensusGroup($rawCensusGroup, $year); + } + } + $year--; + } + return Command::SUCCESS; + } + + private function mapRawCensusGroupToCensusGroup(array $rawCensusGroup, int $year) + { + $censusGroup = new CensusGroup(); + $censusGroup->setGroupId($this->sanitizeValue($rawCensusGroup['group_id'])); + $censusGroup->setYear($year); + $censusGroup->setGroupType($this->groupTypeRepository->findOneBy(['groupType' => $rawCensusGroup['group_type']])); + $censusGroup->setName($rawCensusGroup['group_name']); + + $censusGroup->setTotalCount($this->sanitizeValue($rawCensusGroup['total']['total'])); + $censusGroup->setTotalFCount($this->sanitizeValue($rawCensusGroup['total']['f'])); + $censusGroup->setTotalMCount($this->sanitizeValue($rawCensusGroup['total']['m'])); + + $censusGroup->setLeiterFCount($this->sanitizeValue($rawCensusGroup['f']['leiter'])); + $censusGroup->setBiberFCount($this->sanitizeValue($rawCensusGroup['f']['biber'])); + $censusGroup->setWoelfeFCount($this->sanitizeValue($rawCensusGroup['f']['woelfe'])); + $censusGroup->setPfadisFCount($this->sanitizeValue($rawCensusGroup['f']['pfadis'])); + $censusGroup->setPiosFCount($this->sanitizeValue($rawCensusGroup['f']['pios'])); + $censusGroup->setRoverFCount($this->sanitizeValue($rawCensusGroup['f']['rover'])); + $censusGroup->setPtaFCount($this->sanitizeValue($rawCensusGroup['f']['pta'])); + + $censusGroup->setLeiterMCount($this->sanitizeValue($rawCensusGroup['m']['leiter'])); + $censusGroup->setBiberMCount($this->sanitizeValue($rawCensusGroup['m']['biber'])); + $censusGroup->setWoelfeMCount($this->sanitizeValue($rawCensusGroup['m']['woelfe'])); + $censusGroup->setPfadisMCount($this->sanitizeValue($rawCensusGroup['m']['pfadis'])); + $censusGroup->setPiosMCount($this->sanitizeValue($rawCensusGroup['m']['pios'])); + $censusGroup->setRoverMCount($this->sanitizeValue($rawCensusGroup['m']['rover'])); + $censusGroup->setPtaMCount($this->sanitizeValue($rawCensusGroup['m']['pta'])); + $this->censusGroupRepository->add($censusGroup); + } + + private function sanitizeValue($raw): int + { + return is_null($raw) ? 0 : $raw; + } + + + public function getStats(): CommandStatistics + { + return new CommandStatistics(microtime(true) - $this->start, ''); + } +} diff --git a/src/Command/FetchDataCommand.php b/src/Command/FetchDataCommand.php index fedc753..406c275 100644 --- a/src/Command/FetchDataCommand.php +++ b/src/Command/FetchDataCommand.php @@ -3,6 +3,7 @@ namespace App\Command; use App\Model\CommandStatistics; +use App\Repository\Statistics\StatisticGroupRepository; use App\Service\Pbs\PbsApiService; use Exception; use Symfony\Component\Console\Input\InputInterface; diff --git a/src/Command/GamificationLevelUpReportCommand.php b/src/Command/GamificationLevelUpReportCommand.php new file mode 100644 index 0000000..0ae68ab --- /dev/null +++ b/src/Command/GamificationLevelUpReportCommand.php @@ -0,0 +1,80 @@ +em = $em; + $this->levelUpLogRepository = $levelUpLogRepository; + $this->mailer = $mailer; + } + + protected function configure() + { + $this + ->setName("app:send-levelup-report"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $start = microtime(true); + $levelChanges = $this->levelUpLogRepository->retrieveLastMonth(); + $mailContent = "Dear PBS Team\n\nFollowing Users have increased their rank in the last month:\n\n"; + foreach ($levelChanges as $levelChange) { + $mailContent .= '(' . $levelChange->getDate()->format("d.m H:i") . ') ' + . $levelChange->getPerson()->getNickname() + . '#' + . $levelChange->getPerson()->getPbsNumber() + . ': ' + . $levelChange->getLevel()->getDeTitle() + . "\n"; + } + $mailContent .= "\nKind Regards\nDigio Team +-------------------------------- +Digio AG | Business made digital +Birmensdorferstrasse 94 +8003 Zürich + ++41 44 523 40 40 +www.digio.swiss"; + + $email = new Email(); + $email->from(new Address('no-reply@hc-prod.cust.digio.ch', 'Digio')) + ->to('ss@digio.ch') + ->subject('Level Up Report') + ->text($mailContent); + $this->mailer->send($email); + + $this->duration = microtime(true) - $start; + return 0; + } + + public function getStats(): CommandStatistics + { + return new CommandStatistics($this->duration, ''); + } +} diff --git a/src/Command/ImportFromJsonCommand.php b/src/Command/ImportFromJsonCommand.php index e5c81dc..6c72470 100644 --- a/src/Command/ImportFromJsonCommand.php +++ b/src/Command/ImportFromJsonCommand.php @@ -2,6 +2,8 @@ namespace App\Command; +use App\Entity\Aggregated\AggregatedPersonRole; +use App\Entity\General\GroupSettings; use App\Entity\Midata\Camp; use App\Entity\Midata\CampState; use App\Entity\Midata\Course; @@ -169,9 +171,12 @@ private function importRoleTypes(OutputInterface $output) $role->setItLabel($roleType['label_it']); $this->em->persist($role); - $this->em->flush(); + if (0 === ($i % $this->batchSize)) { + $this->em->flush(); + } $i++; } + $this->em->flush(); $timeElapsed = microtime(true) - $start; $this->stats[] = ['role_types.json', $timeElapsed, $i]; $output->writeln([sprintf('%s rows imported from roles_types.json', $i)]); @@ -384,14 +389,31 @@ private function importGroups(OutputInterface $output) $i = 0; foreach ($groups as $gr) { $group = $this->em->getRepository(Group::class)->findOneBy(['id' => $gr['id']]); + $createGroupSettings = false; if (!$group) { $group = new Group(); $group->setId($gr['id']); $metadata = $this->em->getClassMetaData(get_class($group)); $metadata->setIdGenerator(new AssignedGenerator()); + + /** @var GroupType $gt */ + $gt = $this->em->getRepository(GroupType::class)->findOneBy(['groupType' => $gr['type']]); + $group->setGroupType($gt); + + // create group settings + $groupSettings = new GroupSettings(); + $groupSettings->setGroup($group); + if ($group->getGroupType()->getGroupType() === GroupType::DEPARTMENT) { + $groupSettings->setRoleOverviewFilter(GroupSettings::DEFAULT_DEPARMENT_ROLES); + } elseif ($group->getGroupType()->getGroupType() === GroupType::REGION) { + $groupSettings->setRoleOverviewFilter(GroupSettings::DEFAULT_REGION_ROLES); + } elseif ($group->getGroupType()->getGroupType() === GroupType::CANTON) { + $groupSettings->setRoleOverviewFilter(GroupSettings::DEFAULT_CANTONAL_ROLES); + } + $this->em->persist($groupSettings); } - $group->setName($gr['name']); + $group->setName(trim($gr['name'])); $group->setCantonId($gr['canton_id'] ?? null); $group->setCantonName($gr['canton_name'] ?? null); $group->setCreatedAt(new DateTimeImmutable($gr['created_at'])); @@ -403,6 +425,20 @@ private function importGroups(OutputInterface $output) $gt = $this->em->getRepository(GroupType::class)->findOneBy(['groupType' => $gr['type']]); $group->setGroupType($gt); + if ($createGroupSettings) { + // create group settings + $groupSettings = new GroupSettings(); + $groupSettings->setGroup($group); + if ($group->getGroupType()->getGroupType() === GroupType::DEPARTMENT) { + $groupSettings->setRoleOverviewFilter(GroupSettings::DEFAULT_DEPARMENT_ROLES); + } elseif ($group->getGroupType()->getGroupType() === GroupType::REGION) { + $groupSettings->setRoleOverviewFilter(GroupSettings::DEFAULT_REGION_ROLES); + } elseif ($group->getGroupType()->getGroupType() === GroupType::CANTON) { + $groupSettings->setRoleOverviewFilter(GroupSettings::DEFAULT_CANTONAL_ROLES); + } + $this->em->persist($groupSettings); + } + if ($gr['parent_id'] !== null) { $pg = $this->em->getRepository(Group::class)->find($gr['parent_id']); $group->setParentGroup($pg); @@ -501,7 +537,7 @@ private function importCamps(OutputInterface $output) $metadata->setIdGenerator(new AssignedGenerator()); } $camp->setState($c['state']); - $camp->setLocation(substr($c['location'], 0, 255)); + $camp->setLocation(mb_convert_encoding(substr($c['location'], 0, 255), 'UTF-8', 'US-ASCII')); if (isset($c['name'])) { $camp->setName($c['name']); @@ -534,7 +570,7 @@ private function importCamps(OutputInterface $output) } $this->em->persist($camp); - if ($i % 10) { + if (0 == $i % 10) { $this->em->flush(); } $i++; @@ -624,22 +660,46 @@ private function importPeople(OutputInterface $output) $person = $personRepository->find($id); - foreach ($this->em->getRepository(PersonEvent::class)->findBy(['person' => $person->getId()]) as $personEvent) { + foreach ( + $this->em->getRepository(PersonEvent::class)->findBy(['person' => $person->getId()]) as $personEvent + ) { $this->em->remove($personEvent); } - foreach ($this->em->getRepository(PersonQualification::class)->findBy(['person' => $person->getId()]) as $personQualification) { + foreach ( + $this->em->getRepository(PersonQualification::class)->findBy(['person' => $person->getId()]) as $personQualification + ) { $this->em->remove($personQualification); } - foreach ($this->em->getRepository(PersonRole::class)->findBy(['person' => $person->getId()]) as $personRole) { + foreach ( + $this->em->getRepository(PersonRole::class)->findBy(['person' => $person->getId()]) as $personRole + ) { $this->em->remove($personRole); } - foreach ($this->em->getRepository(Permission::class)->findBy(['person' => $person->getId()]) as $permission) { + foreach ( + $this->em->getRepository(Permission::class)->findBy(['person' => $person->getId()]) as $permission + ) { $this->em->remove($permission); } + /** + * We do not actually delete in aggregatedPersonRole but rather cut all information we have about it and just keep the information that someone once worked there in that role. + * @var $aggregatedPersonRole AggregatedPersonRole + */ + foreach ( + $this->em->getRepository(AggregatedPersonRole::class)->findBy(['person' => $person->getId()]) as $aggregatedPersonRole + ) { + $aggregatedPersonRole->setNickname('Deleted'); + $aggregatedPersonRole->setPerson(null); + $aggregatedPersonRole->setMidata(null); + if (is_null($aggregatedPersonRole->getEndAt())) { + $aggregatedPersonRole->setEndAt(new DateTimeImmutable()); + } + $this->em->persist($aggregatedPersonRole); + } + $this->em->remove($person); $this->em->flush(); @@ -786,9 +846,10 @@ private function importRoles(OutputInterface $output) $personRole->setPerson($person); $role = $this->em->getRepository(Role::class)->getOneByRoleType($r['type']); - if ($role) { - $personRole->setRole($role); + if (is_null($role)) { + continue; } + $personRole->setRole($role); $group = $this->em->getRepository(Group::class)->find($r['group_id']); if ($group) { @@ -799,7 +860,9 @@ private function importRoles(OutputInterface $output) if ($r['deleted_at']) { $deletedAt = new DateTimeImmutable($r['deleted_at']); if ($deletedAt < new DateTimeImmutable('0001-01-01T00:00:00+00:00')) { - $this->gelfLogger->warning(new SimpleLogMessage('person_role entity with invalid deleted_at date skipped')); + $this->gelfLogger->warning( + new SimpleLogMessage('person_role entity with invalid deleted_at date skipped') + ); continue; } $personRole->setDeletedAt(new DateTimeImmutable($r['deleted_at'])); diff --git a/src/Command/ImportGamificationCommand.php b/src/Command/ImportGamificationCommand.php new file mode 100644 index 0000000..a4c488e --- /dev/null +++ b/src/Command/ImportGamificationCommand.php @@ -0,0 +1,125 @@ +em = $em; + $this->levelRepository = $levelRepository; + $this->goalRepository = $goalRepository; + } + + protected function configure() + { + $this + ->setName("app:import-gamification"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $start = microtime(true); + $json = json_decode(file_get_contents($this->pathToJson), true); + + $this->em->getConnection()->executeQuery('DELETE FROM gamification_person_profile'); + $this->em->getConnection()->executeQuery('DELETE FROM goal'); + $this->em->getConnection()->executeQuery('DELETE FROM level'); + if (is_null($json['levels'])) { + $output->writeln('No levels found.'); + return 1; + } + $output->writeln('importing levels'); + $this->importLevels($json['levels'], $output); + + if (is_null($json["goals"])) { + $output->writeln('No goals found.'); + return 1; + } + $this->importGoals($json['goals'], $output); + + $this->duration = microtime(true) - $start; + return 0; + } + + protected function importLevels(array $jsonLevels, OutputInterface $output) + { + foreach ($jsonLevels as $jsonLevel) { + $level = $this->levelRepository->findOneBy(["key" => $jsonLevel["key"]]); + if (is_null($level)) { + $level = new Level(); + $level->setKey(intval($jsonLevel["key"])); + if (!is_null($jsonLevel["next_key"])) { + $level->setNextKey(intval($jsonLevel["next_key"])); + } + $output->writeln("Creating " . $jsonLevel["de_title"] . " (" . $jsonLevel["key"] . ")"); + } + $level->setRequired($jsonLevel["required"]); + $level->setType($jsonLevel["type"]); + $level->setDeTitle($jsonLevel["de_title"]); + $level->setFrTitle($jsonLevel["fr_title"]); + $level->setItTitle($jsonLevel["it_title"]); + + $this->em->persist($level); + } + $this->em->flush(); + } + + protected function importGoals(array $jsonGoals, OutputInterface $output) + { + foreach ($jsonGoals as $jsonGoal) { + $goal = $this->goalRepository->findOneBy(['key' => $jsonGoal['key']]); + if (is_null($goal)) { + $goal = new Goal(); + $goal->setKey($jsonGoal['key']); + $output->writeln("Creating " . $jsonGoal["de"]["title"] . " (" . $jsonGoal["key"] . ")"); + } + $level = $this->levelRepository->findOneBy(['key' => $jsonGoal['level']]); + $goal->setLevel($level); + $goal->setRequired($jsonGoal['required']); + + $goal->setDeTitle($jsonGoal['de']['title']); + $goal->setDeInformation($jsonGoal['de']['information']); + $goal->setDeHelp($jsonGoal['de']['help']); + $goal->setFrTitle($jsonGoal['fr']['title']); + $goal->setFrInformation($jsonGoal['fr']['information']); + $goal->setFrHelp($jsonGoal['fr']['help']); + $goal->setItTitle($jsonGoal['it']['title']); + $goal->setItInformation($jsonGoal['it']['information']); + $goal->setItHelp($jsonGoal['it']['help']); + + $this->em->persist($goal); + } + $this->em->flush(); + } + + public function getStats(): CommandStatistics + { + return new CommandStatistics($this->duration, ''); + } +} diff --git a/src/Command/PseudonymizeLoginCommand.php b/src/Command/PseudonymizeLoginCommand.php new file mode 100644 index 0000000..86294ed --- /dev/null +++ b/src/Command/PseudonymizeLoginCommand.php @@ -0,0 +1,56 @@ +loginRepository = $loginRepository; + } + + protected function configure() + { + $this + ->setName("app:pseudonomize-login") + ->addOption("log", '', InputArgument::OPTIONAL, "List all pseudonymized Logins.", false); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $start = microtime(true); + $pseudonymizedLogins = $this->loginRepository->pseudonymizeAllOlderThan18Months(function ($personId) { + return hash('sha256', $personId); + }); // sha256 is mostly collision free and currently irreversible, sufficient for our purposes. + $log = $input->getOption('log'); + if ($log) { + $output->writeln('Following Logins (id) have been pseudonymized:'); + foreach ($pseudonymizedLogins as $login) { + $output->writeln('id: ' . $login->getId()); + } + $output->writeln('Total of ' . sizeof($pseudonymizedLogins) . ' logins have been pseudonymized.'); + } + $this->duration = microtime(true) - $start; + return 0; + } + + public function getStats(): CommandStatistics + { + return new CommandStatistics($this->duration, ''); + } +} diff --git a/src/Controller/Api/Apps/CensusController.php b/src/Controller/Api/Apps/CensusController.php new file mode 100644 index 0000000..3ebd63a --- /dev/null +++ b/src/Controller/Api/Apps/CensusController.php @@ -0,0 +1,116 @@ +censusDataProvider = $censusDataProvider; + $this->censusFilterDataProvider = $censusFilterDataProvider; + } + + /** + * @param Group $group + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function getPreview(Group $group) + { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + return $this->json($this->censusDataProvider->getPreviewData($group)); + } + + /** + * @param Group $group + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function getTableData(Group $group, CensusRequestData $censusRequestData) + { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + $data = $this->censusDataProvider->getTableData($group, $censusRequestData); + return $this->json($data); + } + + /** + * @param Group $group + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function getDevelopmentData(Group $group, CensusRequestData $censusRequestData) + { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + $data = $this->censusDataProvider->getDevelopmentData($group, $censusRequestData); + return $this->json($data); + } + + + /** + * @param Group $group + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function getMembersData(Group $group, CensusRequestData $censusRequestData) + { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + $data = $this->censusDataProvider->getMembersData($group, $censusRequestData); + return $this->json($data); + } + + /** + * @param Group $group + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function getTreemapData(Group $group, CensusRequestData $censusRequestData) + { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + $data = $this->censusDataProvider->getTreemapData($group, $censusRequestData); + return $this->json($data); + } + + /** + * @param Group $group + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function getFilterData(Group $group) + { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + return $this->json($this->censusFilterDataProvider->getFilterData($group)); + } + + /** + * @param Group $group + * @param CensusRequestData $censusRequestData + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function postFilterData(Group $group, CensusRequestData $censusRequestData) + { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + return $this->json($this->censusFilterDataProvider->setFilterData($group, $censusRequestData)); + } +} diff --git a/src/Controller/Api/Apps/QuapController.php b/src/Controller/Api/Apps/QuapController.php index 64a2223..44d80e5 100644 --- a/src/Controller/Api/Apps/QuapController.php +++ b/src/Controller/Api/Apps/QuapController.php @@ -9,6 +9,8 @@ use App\Exception\ApiException; use App\Service\Apps\Quap\QuapService; use App\Service\DataProvider\QuapSubdepartmentDateDataProvider; +use App\Service\Gamification\PersonGamificationService; +use App\Service\Gamification\QuapGamificationService; use App\Service\Security\PermissionVoter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -128,7 +130,8 @@ public function getQuestionnaireData( */ public function submitAnswers( Group $group, - Request $request + Request $request, + QuapGamificationService $quapGamificationService ): JsonResponse { $this->denyAccessUnlessGranted(PermissionVoter::EDITOR, $group); @@ -137,6 +140,7 @@ public function submitAnswers( throw new ApiException(400, "Invalid JSON"); } + $quapGamificationService->processQuapEvent($json, $group, $this->getUser()); // has to be before answers are saved! $savedWidgetQuap = $this->quapService->submitAnswers($group, $json); return $this->json($savedWidgetQuap->getAnswers()); @@ -151,7 +155,8 @@ public function submitAnswers( */ public function setAccess( Group $group, - Request $request + Request $request, + PersonGamificationService $personGamificationService ): JsonResponse { $this->denyAccessUnlessGranted(PermissionVoter::OWNER, $group); @@ -161,6 +166,7 @@ public function setAccess( } $this->quapService->updateAllowAccess($group, $payload['allow_access']); + $personGamificationService->genericGoalProgress($this->getUser(), 'shareEL'); return $this->json([], JsonResponse::HTTP_NO_CONTENT); } diff --git a/src/Controller/Api/Apps/Widgets/FilterController.php b/src/Controller/Api/Apps/Widgets/FilterController.php index 6042aa4..27d1bd9 100644 --- a/src/Controller/Api/Apps/Widgets/FilterController.php +++ b/src/Controller/Api/Apps/Widgets/FilterController.php @@ -31,4 +31,22 @@ public function getFilterData( return $this->json($data); } + + /** + * @param Request $request + * @param Group $group + * @param FilterDataProvider $filterDataProvider + * @return JsonResponse + * + * @ParamConverter("group", options={"mapping": {"groupId": "id"}}) + */ + public function getGroupTypes( + Request $request, + Group $group, + FilterDataProvider $filterDataProvider + ): JsonResponse { + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + $data = $filterDataProvider->getGroupTypes($group, $request->getLocale()); + return $this->json($data); + } } diff --git a/src/Controller/Api/Apps/Widgets/RoleOverviewController.php b/src/Controller/Api/Apps/Widgets/RoleOverviewController.php new file mode 100644 index 0000000..467192b --- /dev/null +++ b/src/Controller/Api/Apps/Widgets/RoleOverviewController.php @@ -0,0 +1,24 @@ +denyAccessUnlessGranted(PermissionVoter::VIEWER, $widgetRequestData->getGroup()); + $result = $roleOverviewDateRangeDataProvider->getData($widgetRequestData->getGroup(), $dateAndDateRangeRequestData->getFrom()->format('Y-m-d'), $dateAndDateRangeRequestData->getTo()->format('Y-m-d')); + return $this->json($result); + } +} diff --git a/src/Controller/Api/GamificationController.php b/src/Controller/Api/GamificationController.php new file mode 100644 index 0000000..2ee1711 --- /dev/null +++ b/src/Controller/Api/GamificationController.php @@ -0,0 +1,87 @@ +getContent(), true); + if (is_null($json) || is_null($json['group'])) { + throw new ApiException(400, "Invalid JSON"); + } + $group = $groupRepository->find($json['group']); + if (is_null($group)) { + throw new ApiException(400, "Invalid Group"); + } + $this->denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + $loginService->logByPersonAndGroup($this->getUser(), $group); + return new Response('', 201); + } + + public function usedCardLayer( + Request $request, + PersonGamificationService $personGamificationService + ) { + $personGamificationService->genericGoalProgress($this->getUser(), 'card'); + return new Response('', 200); + } + + public function usedDataFilter( + Request $request, + PersonGamificationService $personGamificationService + ) { + $personGamificationService->genericGoalProgress($this->getUser(), 'data'); + return new Response('', 200); + } + + public function usedTimeFilter( + Request $request, + PersonGamificationService $personGamificationService + ) { + $personGamificationService->genericGoalProgress($this->getUser(), 'time'); + return new Response('', 200); + } + + public function getUserProfile( + Request $request, + PersonGamificationService $personGamificationService + ) { + $dto = $personGamificationService->getPersonGamificationDTO($this->getUser(), $request->getLocale()); + return $this->json($dto); + } + + public function resetGamification(Request $request, PersonGamificationService $personGamificationService): Response + { + $personGamificationService->reset($this->getUser()); + return new Response(''); + } + + public function requestBetaAccess(Request $request, PersonGamificationService $personGamificationService) + { + $user = $this->getUser(); + $result = $personGamificationService->getBetaAccess($user); + return $result ? new Response('', 200) : new Response('', 403); + } +} diff --git a/src/Controller/Api/GroupSettingsController.php b/src/Controller/Api/GroupSettingsController.php new file mode 100644 index 0000000..d16a48b --- /dev/null +++ b/src/Controller/Api/GroupSettingsController.php @@ -0,0 +1,36 @@ +denyAccessUnlessGranted(PermissionVoter::VIEWER, $group); + $groupSettings = $group->getGroupSettings(); + $groupSettings->setRoleOverviewFilter(json_decode($request->getContent())); + $entityManager->persist($groupSettings); + $entityManager->flush(); + return new Response('', 204); + } +} diff --git a/src/Controller/Api/InviteController.php b/src/Controller/Api/InviteController.php index 549b440..e526e3e 100644 --- a/src/Controller/Api/InviteController.php +++ b/src/Controller/Api/InviteController.php @@ -6,12 +6,14 @@ use App\Entity\Midata\Group; use App\Entity\Security\Permission; use App\Exception\ApiException; +use App\Service\Gamification\PersonGamificationService; use App\Service\PermissionService; use App\Service\Security\PermissionVoter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -52,7 +54,8 @@ public function createInvite( Request $request, Group $group, SerializerInterface $serializer, - ValidatorInterface $validator + ValidatorInterface $validator, + PersonGamificationService $personGamificationService ): JsonResponse { $this->denyAccessUnlessGranted(PermissionVoter::OWNER, $group); @@ -63,30 +66,31 @@ public function createInvite( ]); } catch (\Exception $exception) { throw new ApiException( - $this->translator->trans('api.error.invalidRequest'), - JsonResponse::HTTP_NOT_ACCEPTABLE + Response::HTTP_NOT_ACCEPTABLE, + $this->translator->trans('api.error.invalidRequest') ); } $errors = $validator->validate($inviteDTO); if (count($errors) > 0) { throw new ApiException( - $this->translator->trans('api.error.invalidEntries'), - JsonResponse::HTTP_UNPROCESSABLE_ENTITY + Response::HTTP_UNPROCESSABLE_ENTITY, + $this->translator->trans('api.error.invalidEntries') ); } if ($inviteDTO->getPermissionType() === PermissionVoter::OWNER) { - throw new ApiException('You may not add group Owners.', JsonResponse::HTTP_FORBIDDEN); + throw new ApiException(Response::HTTP_FORBIDDEN, 'You may not add group Owners.'); } if ($this->inviteService->inviteExists($group, $inviteDTO->getEmail())) { $invite = $this->translator->trans('api.entity.invite'); $message = $this->translator->trans('api.error.exists', ['entityName' => $invite]); - throw new ApiException($message, JsonResponse::HTTP_UNPROCESSABLE_ENTITY); + throw new ApiException(Response::HTTP_UNPROCESSABLE_ENTITY, $message); } $createdInviteDTO = $this->inviteService->createInvite($group, $inviteDTO); + $personGamificationService->genericGoalProgress($this->getUser(), 'invite'); return $this->json($createdInviteDTO, JsonResponse::HTTP_CREATED); } diff --git a/src/Controller/AuthController.php b/src/Controller/AuthController.php index 28e35a3..25fe745 100644 --- a/src/Controller/AuthController.php +++ b/src/Controller/AuthController.php @@ -4,6 +4,7 @@ use App\DTO\Model\PbsUserDTO; use App\Model\LogMessage\SimpleLogMessage; +use App\Service\Gamification\LoginService; use Digio\Logging\GelfLogger; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; @@ -18,14 +19,16 @@ class AuthController extends AbstractController * @var GelfLogger */ private $logger; + private LoginService $loginService; /** * AuthController constructor. * @param GelfLogger $logger */ - public function __construct(GelfLogger $logger) + public function __construct(GelfLogger $logger, LoginService $loginService) { $this->logger = $logger; + $this->loginService = $loginService; } public function login() @@ -34,8 +37,9 @@ public function login() $user = $this->getUser(); if ($user instanceof PbsUserDTO) { $this->logger->info(new SimpleLogMessage(md5($user->getNickname()) . ' logged in.')); + $this->loginService->logByUserDTOForLogin($user); } else { - $this->logger->info(new SimpleLogMessage(md5($user->getUsername()) . ' logged in.')); + $this->logger->info('Non User was logged in.'); } return $this->json($user, JsonResponse::HTTP_OK, [], [ diff --git a/src/DTO/Mapper/CensusMapper.php b/src/DTO/Mapper/CensusMapper.php new file mode 100644 index 0000000..4e15067 --- /dev/null +++ b/src/DTO/Mapper/CensusMapper.php @@ -0,0 +1,210 @@ +setId($statisticGroup->getId()); + $dto->setName($statisticGroup->getName()); + $dto->setType($statisticGroup->getGroupType()->getGroupType()); + $parent = $statisticGroup->getParentGroup(); + $parentId = !is_null($parent) ? $parent->getId() : null; + $dto->setParentId($parentId); + + if (sizeof($censusGroups) < 1) { + $dto->setMissing(true); + return $dto; + } + $dto->setMissing(false); + + foreach ($censusGroups as $censusGroup) { + self::filterCensusGroup($censusGroup, $censusRequestData); + } + + $incomplete = false; + $totalCounts = []; + foreach ($relevantYears as $year) { + $found = false; + foreach ($censusGroups as $censusGroup) { + if ($censusGroup->getYear() == $year) { + $totalCounts[] = $censusGroup->getCalculatedTotal(); + $found = true; + } + } + if (!$found) { + $totalCounts[] = null; + $incomplete = true; + } + } + $dto->setAbsoluteMemberCounts($totalCounts); + + $improvementVsLastYear = null; + $improvementVs3YearsAgo = null; + $improvementVsAvg5Years = null; + if (!is_null($totalCounts[count($totalCounts) - 1]) && $totalCounts[count($totalCounts) - 1] !== 0) { + if (!is_null($totalCounts[count($totalCounts) - 2]) && $totalCounts[count($totalCounts) - 2] !== 0) { + $improvementVsLastYear = (100 / $totalCounts[count($totalCounts) - 2]) * $totalCounts[count($totalCounts) - 1] - 100; + } + if (!is_null($totalCounts[count($totalCounts) - 4]) && $totalCounts[count($totalCounts) - 4] !== 0) { + $improvementVs3YearsAgo = (100 / $totalCounts[count($totalCounts) - 4]) * $totalCounts[count($totalCounts) - 1] - 100; + } + } + $fiveYearTotal = $totalCounts[0] + $totalCounts[1] + $totalCounts[2] + $totalCounts[3] + $totalCounts[4]; + if (!$incomplete && $fiveYearTotal !== 0) { + $improvementVsAvg5Years = (100 / ($fiveYearTotal / 5)) * $totalCounts[count($totalCounts) - 1] - 100; + } + $dto->setRelativeMemberCounts([$improvementVsLastYear, $improvementVs3YearsAgo, $improvementVsAvg5Years]); + return $dto; + } + + /** + * @param StatisticGroup $statisticGroup + * @param CensusGroup[] $censusGroups + * @param int[] $relevantYears + */ + public static function mapToLineChart(StatisticGroup $statisticGroup, array $censusGroups, array $relevantYears, CensusRequestData $censusRequestData) + { + $absolute = []; + $relative = []; + $firstRelevantTotal = null; + foreach ($censusGroups as $censusGroup) { + self::filterCensusGroup($censusGroup, $censusRequestData); + if ($censusGroup->getYear() == $relevantYears[0]) { + $firstRelevantTotal = $censusGroup->getCalculatedTotal(); + } + } + foreach ($relevantYears as $year) { + $found = false; + foreach ($censusGroups as $censusGroup) { + if ($censusGroup->getYear() == $year) { + $found = true; + $absolute[] = $censusGroup->getCalculatedTotal(); + $relative[] = $firstRelevantTotal ? 100 / $firstRelevantTotal * $censusGroup->getCalculatedTotal() - 100 : null; + } + } + if (!$found) { + $absolute[] = null; + $relative[] = null; + } + } + $absoluteDTO = new LineChartDataDTO(); + $relativeDTO = new LineChartDataDTO(); + + $absoluteDTO->setColor(self::getColorForId($statisticGroup->getId())); + $absoluteDTO->setLabel($statisticGroup->getName()); + $absoluteDTO->setData($absolute); + $relativeDTO->setColor(self::getColorForId($statisticGroup->getId())); + $relativeDTO->setLabel($statisticGroup->getName()); + $relativeDTO->setData($relative); + + $return = new DevelopmentWidgetDTO(); + $return->setAbsolute([$absoluteDTO]); + $return->setRelative([$relativeDTO]); + return $return; + } + + public static function filterCensusGroup(CensusGroup $group, CensusRequestData $censusRequestData) + { + if (self::isFiltered('biber', $censusRequestData->getRoles()) || !$censusRequestData->isFilterMales()) { + $group->setBiberMCount(0); + } + if (self::isFiltered('biber', $censusRequestData->getRoles()) || !$censusRequestData->isFilterFemales()) { + $group->setBiberFCount(0); + } + if (self::isFiltered('woelfe', $censusRequestData->getRoles()) || !$censusRequestData->isFilterMales()) { + $group->setWoelfeMCount(0); + } + if (self::isFiltered('woelfe', $censusRequestData->getRoles()) || !$censusRequestData->isFilterFemales()) { + $group->setWoelfeFCount(0); + } + if (self::isFiltered('pfadis', $censusRequestData->getRoles()) || !$censusRequestData->isFilterMales()) { + $group->setPfadisMCount(0); + } + if (self::isFiltered('pfadis', $censusRequestData->getRoles()) || !$censusRequestData->isFilterFemales()) { + $group->setPfadisFCount(0); + } + if (self::isFiltered('rover', $censusRequestData->getRoles()) || !$censusRequestData->isFilterMales()) { + $group->setRoverMCount(0); + } + if (self::isFiltered('rover', $censusRequestData->getRoles()) || !$censusRequestData->isFilterFemales()) { + $group->setRoverFCount(0); + } + if (self::isFiltered('pio', $censusRequestData->getRoles()) || !$censusRequestData->isFilterMales()) { + $group->setPiosMCount(0); + } + if (self::isFiltered('pio', $censusRequestData->getRoles()) || !$censusRequestData->isFilterFemales()) { + $group->setPiosFCount(0); + } + if (self::isFiltered('pta', $censusRequestData->getRoles()) || !$censusRequestData->isFilterMales()) { + $group->setPtaMCount(0); + } + if (self::isFiltered('pta', $censusRequestData->getRoles()) || !$censusRequestData->isFilterFemales()) { + $group->setPtaFCount(0); + } + if (self::isFiltered('leiter', $censusRequestData->getRoles()) || !$censusRequestData->isFilterMales()) { + $group->setLeiterMCount(0); + } + if (self::isFiltered('leiter', $censusRequestData->getRoles()) || !$censusRequestData->isFilterFemales()) { + $group->setLeiterFCount(0); + } + } + + public static function isFiltered($needle, $haystack) + { + return stripos(json_encode($haystack ?? []), $needle) !== false; + } + + public static function getColorForId($id): string + { + return '#' . substr(md5($id), 0, 6); + } + + /** + * Retuns a hex color string where each color (R,G,B) is withing 100-230, so that text is always readable on this color. + * @param int $id + * @return string + */ + public static function getLightColorForId(int $id): string + { + $color = self::getColorForId($id); + $r = hexdec(substr($color, 1, 2)); + $g = hexdec(substr($color, 3, 2)); + $b = hexdec(substr($color, 5, 2)); + if ($r < 100) { + $r += 100; + } + if ($r > 230) { + $r -= 25; + } + if ($g < 100) { + $g += 100; + } + if ($g > 230) { + $g -= 25; + } + if ($b < 100) { + $b += 100; + } + if ($g > 230) { + $g -= 25; + } + return "#" . dechex($r) . dechex($g) . dechex($b); + } +} diff --git a/src/DTO/Mapper/FilterDataMapper.php b/src/DTO/Mapper/FilterDataMapper.php index 1a1689d..e8cbf5e 100644 --- a/src/DTO/Mapper/FilterDataMapper.php +++ b/src/DTO/Mapper/FilterDataMapper.php @@ -25,4 +25,13 @@ public static function createFromEntities(array $groupTypes, array $dates, strin $filterData->setGroupTypes($groupTypeDTOs); return $filterData; } + + public static function createGroupTypes(array $groupTypes, string $locale) + { + $groupTypeDTOs = []; + foreach ($groupTypes as $type) { + $groupTypeDTOs[] = GroupTypeMapper::createGroupTypeFromQueryResult($type, $locale); + } + return $groupTypeDTOs; + } } diff --git a/src/DTO/Mapper/GamificationGoalMapper.php b/src/DTO/Mapper/GamificationGoalMapper.php new file mode 100644 index 0000000..a6e3c40 --- /dev/null +++ b/src/DTO/Mapper/GamificationGoalMapper.php @@ -0,0 +1,32 @@ +setRequired($goal->getRequired()); + $dto->setCompleted($completed); + $dto->setProgress($progress); + $dto->setKey($goal->getKey()); + if ($locale === 'de') { + $dto->setTitle($goal->getDeTitle()); + $dto->setInformation($goal->getDeInformation()); + $dto->setHelp($goal->getDeHelp()); + } elseif ($locale === 'it') { + $dto->setTitle($goal->getItTitle()); + $dto->setInformation($goal->getItInformation()); + $dto->setHelp($goal->getItHelp()); + } else { + $dto->setTitle($goal->getFrTitle()); + $dto->setInformation($goal->getFrInformation()); + $dto->setHelp($goal->getFrHelp()); + } + return $dto; + } +} diff --git a/src/DTO/Mapper/GamificationLevelMapper.php b/src/DTO/Mapper/GamificationLevelMapper.php new file mode 100644 index 0000000..b8785c1 --- /dev/null +++ b/src/DTO/Mapper/GamificationLevelMapper.php @@ -0,0 +1,28 @@ +setActive(false); + $dto->setKey($level->getKey()); + $dto->setRequired($level->getRequired()); + + if ($locale === 'de') { + $dto->setTitle($level->getDeTitle()); + } elseif ($locale === 'it') { + $dto->setTitle($level->getItTitle()); + } else { + $dto->setTitle($level->getFrTitle()); + } + return $dto; + } +} diff --git a/src/DTO/Mapper/GamificationPersonProfileMapper.php b/src/DTO/Mapper/GamificationPersonProfileMapper.php new file mode 100644 index 0000000..c515e0e --- /dev/null +++ b/src/DTO/Mapper/GamificationPersonProfileMapper.php @@ -0,0 +1,28 @@ +setName($profile->getPerson()->getNickname()); + $dto->setLevelKey($profile->getLevel()->getKey()); + $dto->setBetaRequested($profile->getBetaStatus()); + + if ($locale === 'de') { + $dto->setTitle($profile->getLevel()->getDeTitle()); + } elseif ($locale === 'it') { + $dto->setTitle($profile->getLevel()->getItTitle()); + } else { + $dto->setTitle($profile->getLevel()->getFrTitle()); + } + return $dto; + } +} diff --git a/src/DTO/Mapper/RoleOverviewMapper.php b/src/DTO/Mapper/RoleOverviewMapper.php new file mode 100644 index 0000000..35b38c1 --- /dev/null +++ b/src/DTO/Mapper/RoleOverviewMapper.php @@ -0,0 +1,70 @@ + ['#EEE09F', '#a1976c'], + 'Woelfe' => ['#3BB5DC', '#27758f'], + 'Pfadi' => ['#9A7A54', '#574530'], + 'Pio' => ['#DD1F19', '#6b110c'], + 'Rover' => ['#1DA650', '#127336'], + 'Pta' => ['#d9b826', '#947d16'], + ]; + + public static function createRoleOverviewDTO(Group $group): RoleOverviewDTO + { + $groupSettings = $group->getGroupSettings(); + $filter = $groupSettings->getRoleOverviewFilter(); + if (!$filter || !sizeof($filter)) { + if ($group->getGroupType()->getGroupType() === GroupType::DEPARTMENT) { + $filter = GroupSettings::DEFAULT_DEPARMENT_ROLES; + } elseif ($group->getGroupType()->getGroupType() === GroupType::REGION) { + $filter = GroupSettings::DEFAULT_REGION_ROLES; + } elseif ($group->getGroupType()->getGroupType() === GroupType::CANTON) { + $filter = GroupSettings::DEFAULT_CANTONAL_ROLES; + } + } + + return new RoleOverviewDTO($filter); + } + + public static function createRoleOccupationWrapper(Role $role, string $locale): RoleOccupationWrapper + { + if (str_contains($locale, 'it')) { + $roleName = $role->getItLabel(); + } elseif (str_contains($locale, 'fr')) { + $roleName = $role->getFrLabel(); + } else { + $roleName = $role->getDeLabel(); + } + return new RoleOccupationWrapper($roleName, $role->getRoleType(), self::getRoleColor($role->getRoleType())); // Colors not yet implemented + } + + private static function getRoleColor(string $roleType) + { + foreach (RoleOverviewMapper::GROUP_TYPE_COLORS as $key => $value) { + if (str_contains($roleType, $key)) { + return $value; + } + } + return ['#da70d6', '#8c488a']; + } + + public static function createRoleOccupation(AggregatedPersonRole $aggregatedPersonRole, string $from, string $to): RoleOccupation + { + $start = new \DateTime($from) < $aggregatedPersonRole->getStartAt() ? $aggregatedPersonRole->getStartAt()->format('Y-m-d') : $from; + $end = is_null($aggregatedPersonRole->getEndAt()) || new \DateTime($to) <= $aggregatedPersonRole->getEndAt() ? $to : $aggregatedPersonRole->getEndAt()->format('Y-m-d'); + return new RoleOccupation($aggregatedPersonRole->getNickname(), $start, $end); + } +} diff --git a/src/DTO/Model/Apps/Census/CensusFilterDTO.php b/src/DTO/Model/Apps/Census/CensusFilterDTO.php new file mode 100644 index 0000000..585b5c0 --- /dev/null +++ b/src/DTO/Model/Apps/Census/CensusFilterDTO.php @@ -0,0 +1,85 @@ +roles; + } + + /** + * @param array|null $roles + */ + public function setRoles(?array $roles): void + { + $this->roles = $roles; + } + + /** + * @return array|null + */ + public function getGroups(): ?array + { + return $this->groups; + } + + /** + * @param array|null $groups + */ + public function setGroups(?array $groups): void + { + $this->groups = $groups; + } + + + /** + * @return bool + */ + public function isFilterMales(): bool + { + return $this->filterMales; + } + + /** + * @param bool $filterMales + */ + public function setFilterMales(bool $filterMales): void + { + $this->filterMales = $filterMales; + } + + /** + * @return bool + */ + public function isFilterFemales(): bool + { + return $this->filterFemales; + } + + /** + * @param bool $filterFemales + */ + public function setFilterFemales(bool $filterFemales): void + { + $this->filterFemales = $filterFemales; + } +} diff --git a/src/DTO/Model/Apps/Census/DevelopmentWidgetDTO.php b/src/DTO/Model/Apps/Census/DevelopmentWidgetDTO.php new file mode 100644 index 0000000..fc13df7 --- /dev/null +++ b/src/DTO/Model/Apps/Census/DevelopmentWidgetDTO.php @@ -0,0 +1,65 @@ +years; + } + + /** + * @param array $years + */ + public function setYears(array $years): void + { + $this->years = $years; + } + + /** + * @return array + */ + public function getAbsolute(): array + { + return $this->absolute; + } + + /** + * @param array $absolute + */ + public function setAbsolute(array $absolute): void + { + $this->absolute = $absolute; + } + + /** + * @return array + */ + public function getRelative(): array + { + return $this->relative; + } + + /** + * @param array $relative + */ + public function setRelative(array $relative): void + { + $this->relative = $relative; + } +} diff --git a/src/DTO/Model/Apps/Census/GroupCensusDTO.php b/src/DTO/Model/Apps/Census/GroupCensusDTO.php new file mode 100644 index 0000000..8cb0dbc --- /dev/null +++ b/src/DTO/Model/Apps/Census/GroupCensusDTO.php @@ -0,0 +1,296 @@ +year; + } + + /** + * @param string $year + */ + public function setYear(string $year): void + { + $this->year = $year; + } + + /** + * @return int + */ + public function getTotalMCount(): int + { + return $this->total_m_count; + } + + /** + * @param int $total_m_count + */ + public function setTotalMCount(int $total_m_count): void + { + $this->total_m_count = $total_m_count; + } + + /** + * @return int + */ + public function getTotalFCount(): int + { + return $this->total_f_count; + } + + /** + * @param int $total_f_count + */ + public function setTotalFCount(int $total_f_count): void + { + $this->total_f_count = $total_f_count; + } + + /** + * @return int + */ + public function getLeiterMCount(): int + { + return $this->leiter_m_count; + } + + /** + * @param int $leiter_m_count + */ + public function setLeiterMCount(int $leiter_m_count): void + { + $this->leiter_m_count = $leiter_m_count; + } + + /** + * @return int + */ + public function getLeiterFCount(): int + { + return $this->leiter_f_count; + } + + /** + * @param int $leiter_f_count + */ + public function setLeiterFCount(int $leiter_f_count): void + { + $this->leiter_f_count = $leiter_f_count; + } + + /** + * @return int + */ + public function getBiberMCount(): int + { + return $this->biber_m_count; + } + + /** + * @param int $biber_m_count + */ + public function setBiberMCount(int $biber_m_count): void + { + $this->biber_m_count = $biber_m_count; + } + + /** + * @return int + */ + public function getBiberFCount(): int + { + return $this->biber_f_count; + } + + /** + * @param int $biber_f_count + */ + public function setBiberFCount(int $biber_f_count): void + { + $this->biber_f_count = $biber_f_count; + } + + /** + * @return int + */ + public function getWoelfeMCount(): int + { + return $this->woelfe_m_count; + } + + /** + * @param int $woelfe_m_count + */ + public function setWoelfeMCount(int $woelfe_m_count): void + { + $this->woelfe_m_count = $woelfe_m_count; + } + + /** + * @return int + */ + public function getWoelfeFCount(): int + { + return $this->woelfe_f_count; + } + + /** + * @param int $woelfe_f_count + */ + public function setWoelfeFCount(int $woelfe_f_count): void + { + $this->woelfe_f_count = $woelfe_f_count; + } + + /** + * @return int + */ + public function getPfadisMCount(): int + { + return $this->pfadis_m_count; + } + + /** + * @param int $pfadis_m_count + */ + public function setPfadisMCount(int $pfadis_m_count): void + { + $this->pfadis_m_count = $pfadis_m_count; + } + + /** + * @return int + */ + public function getPfadisFCount(): int + { + return $this->pfadis_f_count; + } + + /** + * @param int $pfadis_f_count + */ + public function setPfadisFCount(int $pfadis_f_count): void + { + $this->pfadis_f_count = $pfadis_f_count; + } + + /** + * @return int + */ + public function getPiosMCount(): int + { + return $this->pios_m_count; + } + + /** + * @param int $pios_m_count + */ + public function setPiosMCount(int $pios_m_count): void + { + $this->pios_m_count = $pios_m_count; + } + + /** + * @return int + */ + public function getPiosFCount(): int + { + return $this->pios_f_count; + } + + /** + * @param int $pios_f_count + */ + public function setPiosFCount(int $pios_f_count): void + { + $this->pios_f_count = $pios_f_count; + } + + /** + * @return int + */ + public function getRoverMCount(): int + { + return $this->rover_m_count; + } + + /** + * @param int $rover_m_count + */ + public function setRoverMCount(int $rover_m_count): void + { + $this->rover_m_count = $rover_m_count; + } + + /** + * @return int + */ + public function getRoverFCount(): int + { + return $this->rover_f_count; + } + + /** + * @param int $rover_f_count + */ + public function setRoverFCount(int $rover_f_count): void + { + $this->rover_f_count = $rover_f_count; + } + + /** + * @return int + */ + public function getPtaMCount(): int + { + return $this->pta_m_count; + } + + /** + * @param int $pta_m_count + */ + public function setPtaMCount(int $pta_m_count): void + { + $this->pta_m_count = $pta_m_count; + } + + /** + * @return int + */ + public function getPtaFCount(): int + { + return $this->pta_f_count; + } + + /** + * @param int $pta_f_count + */ + public function setPtaFCount(int $pta_f_count): void + { + $this->pta_f_count = $pta_f_count; + } +} diff --git a/src/DTO/Model/Apps/Census/LineChartDataDTO.php b/src/DTO/Model/Apps/Census/LineChartDataDTO.php new file mode 100644 index 0000000..05dc0f6 --- /dev/null +++ b/src/DTO/Model/Apps/Census/LineChartDataDTO.php @@ -0,0 +1,59 @@ +color; + } + + /** + * @param string $color + */ + public function setColor(string $color): void + { + $this->color = $color; + } + + /** + * @return string + */ + public function getLabel(): string + { + return $this->label; + } + + /** + * @param string $label + */ + public function setLabel(string $label): void + { + $this->label = $label; + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + /** + * @param array $data + */ + public function setData(array $data): void + { + $this->data = $data; + } +} diff --git a/src/DTO/Model/Apps/Census/MembersWidgetDTO.php b/src/DTO/Model/Apps/Census/MembersWidgetDTO.php new file mode 100644 index 0000000..25e601e --- /dev/null +++ b/src/DTO/Model/Apps/Census/MembersWidgetDTO.php @@ -0,0 +1,27 @@ +data; + } + + /** + * @param array $data + */ + public function setData(array $data): void + { + $this->data = $data; + } +} diff --git a/src/DTO/Model/Apps/Census/StackedBarElementDTO.php b/src/DTO/Model/Apps/Census/StackedBarElementDTO.php new file mode 100644 index 0000000..a6a2fb6 --- /dev/null +++ b/src/DTO/Model/Apps/Census/StackedBarElementDTO.php @@ -0,0 +1,70 @@ +y = $y; + $this->x = $x; + $this->color = $color; + } + + /** + * @return int + */ + public function getY(): int + { + return $this->y; + } + + /** + * @param int $y + */ + public function setY(int $y): void + { + $this->y = $y; + } + + /** + * @return string + */ + public function getX(): string + { + return $this->x; + } + + /** + * @param string $x + */ + public function setX(string $x): void + { + $this->x = $x; + } + + /** + * @return string + */ + public function getColor(): string + { + return $this->color; + } + + /** + * @param string $color + */ + public function setColor(string $color): void + { + $this->color = $color; + } +} diff --git a/src/DTO/Model/Apps/Census/TableDTO.php b/src/DTO/Model/Apps/Census/TableDTO.php new file mode 100644 index 0000000..b84f9f2 --- /dev/null +++ b/src/DTO/Model/Apps/Census/TableDTO.php @@ -0,0 +1,132 @@ +id; + } + + /** + * @param int $id + */ + public function setId(int $id): void + { + $this->id = $id; + } + + /** + * @return int + */ + public function getParentId(): int + { + return $this->parentId; + } + + /** + * @param int $parentId + */ + public function setParentId(int $parentId): void + { + $this->parentId = $parentId; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @param string $type + */ + public function setType(string $type): void + { + $this->type = $type; + } + + /** + * @return bool + */ + public function isMissing(): bool + { + return $this->missing; + } + + /** + * @param bool $missing + */ + public function setMissing(bool $missing): void + { + $this->missing = $missing; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @return array + */ + public function getAbsoluteMemberCounts(): array + { + return $this->absoluteMemberCounts; + } + + /** + * @param array $absoluteMemberCounts + */ + public function setAbsoluteMemberCounts(array $absoluteMemberCounts): void + { + $this->absoluteMemberCounts = $absoluteMemberCounts; + } + + /** + * @return array + */ + public function getRelativeMemberCounts(): array + { + return $this->relativeMemberCounts; + } + + /** + * @param array $relativeMemberCounts + */ + public function setRelativeMemberCounts(array $relativeMemberCounts): void + { + $this->relativeMemberCounts = $relativeMemberCounts; + } +} diff --git a/src/DTO/Model/Apps/Census/TreemapWidgetDTO.php b/src/DTO/Model/Apps/Census/TreemapWidgetDTO.php new file mode 100644 index 0000000..d68cd19 --- /dev/null +++ b/src/DTO/Model/Apps/Census/TreemapWidgetDTO.php @@ -0,0 +1,77 @@ +name; + } + + /** + * @param string $name + */ + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @return string + */ + public function getRegion(): string + { + return $this->region; + } + + /** + * @param string $region + */ + public function setRegion(string $region): void + { + $this->region = $region; + } + + /** + * @return string + */ + public function getColor(): string + { + return $this->color; + } + + /** + * @param string $color + */ + public function setColor(string $color): void + { + $this->color = $color; + } + + /** + * @return int + */ + public function getValue(): int + { + return $this->value; + } + + /** + * @param int $value + */ + public function setValue(int $value): void + { + $this->value = $value; + } +} diff --git a/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOccupation.php b/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOccupation.php new file mode 100644 index 0000000..0e93937 --- /dev/null +++ b/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOccupation.php @@ -0,0 +1,81 @@ +name = $name; + $this->from = $from; + $this->to = $to; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @return string + */ + public function getFrom(): string + { + return $this->from; + } + + /** + * @param string $from + */ + public function setFrom(string $from): void + { + $this->from = $from; + } + + /** + * @return string + */ + public function getTo(): string + { + return $this->to; + } + + /** + * @param string $to + */ + public function setTo(string $to): void + { + $this->to = $to; + } +} diff --git a/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOccupationWrapper.php b/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOccupationWrapper.php new file mode 100644 index 0000000..734c28a --- /dev/null +++ b/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOccupationWrapper.php @@ -0,0 +1,103 @@ +role = $role; + $this->roleType = $roleType; + $this->colors = $colors; + } + + /** + * @return string + */ + public function getRole(): string + { + return $this->role; + } + + /** + * @param string $role + */ + public function setRole(string $role): void + { + $this->role = $role; + } + + /** + * @return string[] + */ + public function getColors(): array + { + return $this->colors; + } + + /** + * @param string[] $colors + */ + public function setColors(array $colors): void + { + $this->colors = $colors; + } + + /** + * @return RoleOccupation[]|null + */ + public function getData(): array + { + return $this->data; + } + + /** + * @param RoleOccupation[]|null $data + */ + public function setData(array $data): void + { + $this->data = $data; + } + + public function addData(RoleOccupation $occupation): void + { + $this->data[] = $occupation; + } + + /** + * @return string + */ + public function getRoleType(): string + { + return $this->roleType; + } + + /** + * @param string $roleType + */ + public function setRoleType(string $roleType): void + { + $this->roleType = $roleType; + } +} diff --git a/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOverviewDTO.php b/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOverviewDTO.php new file mode 100644 index 0000000..51a9862 --- /dev/null +++ b/src/DTO/Model/Apps/Widgets/RoleOverview/RoleOverviewDTO.php @@ -0,0 +1,63 @@ +filter = $filter; + } + + /** + * @return string[]|null + */ + public function getFilter(): ?array + { + return $this->filter; + } + + /** + * @param string[]|null $filter + */ + public function setFilter(?array $filter): void + { + $this->filter = $filter; + } + + + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + /** + * @param array $data + */ + public function setData(array $data): void + { + $this->data = $data; + } + + public function addData(RoleOccupationWrapper $roleOccupationWrapper): void + { + $this->data[] = $roleOccupationWrapper; + } +} diff --git a/src/DTO/Model/FilterRequestData/CensusRequestData.php b/src/DTO/Model/FilterRequestData/CensusRequestData.php new file mode 100644 index 0000000..5f599aa --- /dev/null +++ b/src/DTO/Model/FilterRequestData/CensusRequestData.php @@ -0,0 +1,105 @@ +group; + } + + /** + * @param Group $group + */ + public function setGroup(Group $group): void + { + $this->group = $group; + } + + /** + * @return array|null + */ + public function getRoles(): ?array + { + return $this->roles; + } + + /** + * @param array|null $roles + */ + public function setRoles(?array $roles): void + { + $this->roles = $roles; + } + + /** + * @return array|null + */ + public function getGroups(): ?array + { + return $this->groups; + } + + /** + * @param array|null $groups + */ + public function setGroups(?array $groups): void + { + $this->groups = $groups; + } + + + /** + * @return bool + */ + public function isFilterMales(): bool + { + return $this->filterMales; + } + + /** + * @param bool $filterMales + */ + public function setFilterMales(bool $filterMales): void + { + $this->filterMales = $filterMales; + } + + /** + * @return bool + */ + public function isFilterFemales(): bool + { + return $this->filterFemales; + } + + /** + * @param bool $filterFemales + */ + public function setFilterFemales(bool $filterFemales): void + { + $this->filterFemales = $filterFemales; + } +} diff --git a/src/DTO/Model/Gamification/GoalDTO.php b/src/DTO/Model/Gamification/GoalDTO.php new file mode 100644 index 0000000..47f4022 --- /dev/null +++ b/src/DTO/Model/Gamification/GoalDTO.php @@ -0,0 +1,126 @@ +key; + } + + /** + * @param string $key + */ + public function setKey(string $key): void + { + $this->key = $key; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle(string $title): void + { + $this->title = $title; + } + + /** + * @return string + */ + public function getInformation(): string + { + return $this->information; + } + + /** + * @param string $information + */ + public function setInformation(string $information): void + { + $this->information = $information; + } + + /** + * @return string + */ + public function getHelp(): string + { + return $this->help; + } + + /** + * @param string $help + */ + public function setHelp(string $help): void + { + $this->help = $help; + } + + /** + * @return int + */ + public function getProgress(): int + { + return $this->progress; + } + + /** + * @param int $progress + */ + public function setProgress(int $progress): void + { + $this->progress = $progress; + } + + /** + * @return bool + */ + public function isCompleted(): bool + { + return $this->completed; + } + + /** + * @param bool $completed + */ + public function setCompleted(bool $completed): void + { + $this->completed = $completed; + } + + /** + * @return bool + */ + public function isRequired(): bool + { + return $this->required; + } + + /** + * @param bool $required + */ + public function setRequired(bool $required): void + { + $this->required = $required; + } +} diff --git a/src/DTO/Model/Gamification/LevelDTO.php b/src/DTO/Model/Gamification/LevelDTO.php new file mode 100644 index 0000000..97aa26a --- /dev/null +++ b/src/DTO/Model/Gamification/LevelDTO.php @@ -0,0 +1,93 @@ +required; + } + + /** + * @param int $required + */ + public function setRequired(int $required): void + { + $this->required = $required; + } + + /** + * @return bool + */ + public function isActive(): bool + { + return $this->active; + } + + /** + * @param bool $active + */ + public function setActive(bool $active): void + { + $this->active = $active; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle(string $title): void + { + $this->title = $title; + } + + /** + * @return int + */ + public function getKey(): int + { + return $this->key; + } + + /** + * @param int $key + */ + public function setKey(int $key): void + { + $this->key = $key; + } + + /** + * @return array + */ + public function getGoals(): array + { + return $this->goals; + } + + /** + * @param array $goals + */ + public function setGoals(array $goals): void + { + $this->goals = $goals; + } +} diff --git a/src/DTO/Model/Gamification/PersonGamificationDTO.php b/src/DTO/Model/Gamification/PersonGamificationDTO.php new file mode 100644 index 0000000..7691a81 --- /dev/null +++ b/src/DTO/Model/Gamification/PersonGamificationDTO.php @@ -0,0 +1,110 @@ +betaRequested; + } + + /** + * @param bool $betaRequested + */ + public function setBetaRequested(bool $betaRequested): void + { + $this->betaRequested = $betaRequested; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle(string $title): void + { + $this->title = $title; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @return string + */ + public function getLevelKey(): string + { + return $this->levelKey; + } + + /** + * @param string $levelKey + */ + public function setLevelKey(string $levelKey): void + { + $this->levelKey = $levelKey; + } + + /** + * @return bool + */ + public function isLevelUp(): bool + { + return $this->levelUp; + } + + /** + * @param bool $levelUp + */ + public function setLevelUp(bool $levelUp): void + { + $this->levelUp = $levelUp; + } + + /** + * @return array + */ + public function getLevels(): array + { + return $this->levels; + } + + /** + * @param array $levels + */ + public function setLevels(array $levels): void + { + $this->levels = $levels; + } +} diff --git a/src/DataFixtures/Aggregator/AggregatorTestFixture.php b/src/DataFixtures/Aggregator/AggregatorTestFixture.php index 39337b3..f110ec6 100644 --- a/src/DataFixtures/Aggregator/AggregatorTestFixture.php +++ b/src/DataFixtures/Aggregator/AggregatorTestFixture.php @@ -278,9 +278,10 @@ protected function importRoles(ObjectManager $em) } $role = $em->getRepository(Role::class)->getOneByRoleType($r['type']); - if ($role) { - $personRole->setRole($role); + if (is_null($role)) { + continue; } + $personRole->setRole($role); $group = $em->getRepository(Group::class)->find($r['group_id']); if ($group) { diff --git a/src/Entity/Aggregated/AggregatedPersonRole.php b/src/Entity/Aggregated/AggregatedPersonRole.php new file mode 100644 index 0000000..7b5be86 --- /dev/null +++ b/src/Entity/Aggregated/AggregatedPersonRole.php @@ -0,0 +1,147 @@ +id; + } + + public function getRole(): ?Role + { + return $this->role; + } + + public function setRole(?Role $role): self + { + $this->role = $role; + + return $this; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + return $this; + } + + public function getMidata(): ?PersonRole + { + return $this->midata; + } + + public function setMidata(?PersonRole $midata): self + { + $this->midata = $midata; + + return $this; + } + + public function getNickname(): ?string + { + return $this->nickname; + } + + public function setNickname(string $nickname): self + { + $this->nickname = $nickname; + + return $this; + } + + public function getStartAt(): ?\DateTimeInterface + { + return $this->start_at; + } + + public function setStartAt(\DateTimeInterface $start_at): self + { + $this->start_at = $start_at; + + return $this; + } + + public function getEndAt(): ?\DateTimeInterface + { + return $this->end_at; + } + + public function setEndAt(?\DateTimeInterface $end_at): self + { + $this->end_at = $end_at; + + return $this; + } + + public function setGroup($group): self + { + $this->group = $group; + + return $this; + } + + public function getGroup() + { + return $this->group; + } +} diff --git a/src/Entity/Gamification/GamificationPersonProfile.php b/src/Entity/Gamification/GamificationPersonProfile.php new file mode 100644 index 0000000..9d7b2ea --- /dev/null +++ b/src/Entity/Gamification/GamificationPersonProfile.php @@ -0,0 +1,232 @@ +id; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(Person $person): self + { + $this->person = $person; + + return $this; + } + + public function getLevel(): ?Level + { + return $this->level; + } + + public function setLevel(?Level $level): self + { + $this->level = $level; + + return $this; + } + + public function getHasUsedCardLayer(): ?bool + { + return $this->has_used_card_layer; + } + + public function setHasUsedCardLayer(bool $has_used_card_layer): self + { + $this->has_used_card_layer = $has_used_card_layer; + + return $this; + } + + public function getHasUsedDatafilter(): ?bool + { + return $this->has_used_datafilter; + } + + public function setHasUsedDatafilter(bool $has_used_datafilter): self + { + $this->has_used_datafilter = $has_used_datafilter; + + return $this; + } + + public function getHasUsedTimefilter(): ?bool + { + return $this->has_used_timefilter; + } + + public function setHasUsedTimefilter(bool $has_used_timefilter): self + { + $this->has_used_timefilter = $has_used_timefilter; + + return $this; + } + + public function getHasSharedEl(): ?bool + { + return $this->has_shared_el; + } + + public function setHasSharedEl(bool $has_shared_el): self + { + $this->has_shared_el = $has_shared_el; + + return $this; + } + + public function getAccessGrantedCount(): ?int + { + return $this->access_granted_count; + } + + public function setAccessGrantedCount(int $access_granted_count): self + { + $this->access_granted_count = $access_granted_count; + + return $this; + } + + public function getElFilledOut(): ?bool + { + return $this->el_filled_out; + } + + public function setElFilledOut(bool $el_filled_out): self + { + $this->el_filled_out = $el_filled_out; + + return $this; + } + + public function getElRevised(): ?bool + { + return $this->el_revised; + } + + public function setElRevised(bool $el_revised): self + { + $this->el_revised = $el_revised; + + return $this; + } + + public function getElIrrelevant(): ?bool + { + return $this->el_irrelevant; + } + + public function setElIrrelevant(bool $el_irrelevant): self + { + $this->el_irrelevant = $el_irrelevant; + + return $this; + } + + public function getElImproved(): ?bool + { + return $this->el_improved; + } + + public function setElImproved(bool $el_improved): self + { + $this->el_improved = $el_improved; + + return $this; + } + + public function getBetaStatus(): ?bool + { + return $this->beta_status; + } + + public function setBetaStatus(bool $beta_status): self + { + $this->beta_status = $beta_status; + + return $this; + } +} diff --git a/src/Entity/Gamification/GamificationQuapEvent.php b/src/Entity/Gamification/GamificationQuapEvent.php new file mode 100644 index 0000000..e62fd97 --- /dev/null +++ b/src/Entity/Gamification/GamificationQuapEvent.php @@ -0,0 +1,124 @@ +id; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + return $this; + } + + public function getGroup(): ?Group + { + return $this->group; + } + + public function setGroup(?Group $group): self + { + $this->group = $group; + + return $this; + } + + public function getDate(): ?\DateTimeImmutable + { + return $this->date; + } + + public function setDate(\DateTimeImmutable $date): self + { + $this->date = $date; + + return $this; + } + + public function getLocalChangeIndex(): ?int + { + return $this->local_change_index; + } + + public function setLocalChangeIndex(int $local_change_index): self + { + $this->local_change_index = $local_change_index; + + return $this; + } + + public function getQuestionnaire(): ?Questionnaire + { + return $this->questionnaire; + } + + public function setQuestionnaire(?Questionnaire $questionnaire): self + { + $this->questionnaire = $questionnaire; + + return $this; + } +} diff --git a/src/Entity/Gamification/Goal.php b/src/Entity/Gamification/Goal.php new file mode 100644 index 0000000..07950e5 --- /dev/null +++ b/src/Entity/Gamification/Goal.php @@ -0,0 +1,231 @@ +id; + } + + public function getLevel(): ?Level + { + return $this->level; + } + + public function setLevel(?Level $level): self + { + $this->level = $level; + + return $this; + } + + public function getRequired(): ?bool + { + return $this->required; + } + + public function setRequired(bool $required): self + { + $this->required = $required; + + return $this; + } + + public function getDeTitle(): ?string + { + return $this->de_title; + } + + public function setDeTitle(string $de_title): self + { + $this->de_title = $de_title; + + return $this; + } + + public function getDeInformation(): ?string + { + return $this->de_information; + } + + public function setDeInformation(string $de_information): self + { + $this->de_information = $de_information; + + return $this; + } + + public function getDeHelp(): ?string + { + return $this->de_help; + } + + public function setDeHelp(?string $de_help): self + { + $this->de_help = $de_help; + + return $this; + } + + public function getFrTitle(): ?string + { + return $this->fr_title; + } + + public function setFrTitle(string $fr_title): self + { + $this->fr_title = $fr_title; + + return $this; + } + + public function getFrInformation(): ?string + { + return $this->fr_information; + } + + public function setFrInformation(string $fr_information): self + { + $this->fr_information = $fr_information; + + return $this; + } + + public function getFrHelp(): ?string + { + return $this->fr_help; + } + + public function setFrHelp(?string $fr_help): self + { + $this->fr_help = $fr_help; + + return $this; + } + + public function getItTitle(): ?string + { + return $this->it_title; + } + + public function setItTitle(string $it_title): self + { + $this->it_title = $it_title; + + return $this; + } + + public function getItInformation(): ?string + { + return $this->it_information; + } + + public function setItInformation(string $it_information): self + { + $this->it_information = $it_information; + + return $this; + } + + public function getItHelp(): ?string + { + return $this->it_help; + } + + public function setItHelp(?string $it_help): self + { + $this->it_help = $it_help; + + return $this; + } + + public function getKey(): ?string + { + return $this->key; + } + + public function setKey(string $key): self + { + $this->key = $key; + + return $this; + } +} diff --git a/src/Entity/Gamification/Level.php b/src/Entity/Gamification/Level.php new file mode 100644 index 0000000..7fa3c41 --- /dev/null +++ b/src/Entity/Gamification/Level.php @@ -0,0 +1,201 @@ +required; + } + + /** + * @param int $required + */ + public function setRequired($required): void + { + $this->required = $required; + } + + /** + * @return mixed + */ + public function getNextKey() + { + return $this->next_key; + } + + /** + * @param mixed $next_key + */ + public function setNextKey($next_key): void + { + $this->next_key = $next_key; + } + + + public function __construct() + { + $this->goals = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getType(): ?int + { + return $this->type; + } + + public function setType(int $type): self + { + $this->type = $type; + + return $this; + } + + public function getDeTitle(): ?string + { + return $this->de_title; + } + + public function setDeTitle(string $de_title): self + { + $this->de_title = $de_title; + + return $this; + } + + public function getFrTitle(): ?string + { + return $this->fr_title; + } + + public function setFrTitle(string $fr_title): self + { + $this->fr_title = $fr_title; + + return $this; + } + + public function getItTitle(): ?string + { + return $this->it_title; + } + + public function setItTitle(string $it_title): self + { + $this->it_title = $it_title; + + return $this; + } + + /** + * @return Collection + */ + public function getGoals(): Collection + { + return $this->goals; + } + + public function addGoal(Goal $goal): self + { + if (!$this->goals->contains($goal)) { + $this->goals[] = $goal; + $goal->setLevel($this); + } + + return $this; + } + + public function removeGoal(Goal $goal): self + { + if ($this->goals->removeElement($goal)) { + // set the owning side to null (unless already changed) + if ($goal->getLevel() === $this) { + $goal->setLevel(null); + } + } + + return $this; + } + + /** + * @return mixed + */ + public function getKey() + { + return $this->key; + } + + /** + * @param mixed $key + */ + public function setKey($key): void + { + $this->key = $key; + } +} diff --git a/src/Entity/Gamification/LevelUpLog.php b/src/Entity/Gamification/LevelUpLog.php new file mode 100644 index 0000000..f0f602a --- /dev/null +++ b/src/Entity/Gamification/LevelUpLog.php @@ -0,0 +1,95 @@ +id; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + return $this; + } + + public function getLevel(): ?Level + { + return $this->level; + } + + public function setLevel(?Level $level): self + { + $this->level = $level; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): self + { + $this->date = $date; + + return $this; + } + + public function getDisplayed(): ?bool + { + return $this->displayed; + } + + public function setDisplayed(bool $displayed): self + { + $this->displayed = $displayed; + + return $this; + } +} diff --git a/src/Entity/Gamification/Login.php b/src/Entity/Gamification/Login.php new file mode 100644 index 0000000..5e090a5 --- /dev/null +++ b/src/Entity/Gamification/Login.php @@ -0,0 +1,134 @@ +id; + } + + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): self + { + $this->date = $date; + + return $this; + } + + public function getIsGroupChange(): ?bool + { + return $this->is_group_change; + } + + public function setIsGroupChange(bool $group_change): self + { + $this->is_group_change = $group_change; + + return $this; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + return $this; + } + + public function getGroup(): ?Group + { + return $this->group; + } + + public function setGroup(?Group $group): self + { + $this->group = $group; + + return $this; + } + + public function getHashedPersonId(): ?string + { + return $this->hashed_person_id; + } + + public function setHashedPersonId(?string $hashed_person_id): self + { + $this->hashed_person_id = $hashed_person_id; + + return $this; + } + + public function getRole(): ?string + { + return $this->role; + } + + public function setRole(?string $role): self + { + $this->role = $role; + + return $this; + } +} diff --git a/src/Entity/General/GroupSettings.php b/src/Entity/General/GroupSettings.php new file mode 100644 index 0000000..6a22845 --- /dev/null +++ b/src/Entity/General/GroupSettings.php @@ -0,0 +1,135 @@ +id; + } + + public function getGroup(): ?Group + { + return $this->group; + } + + public function setGroup(?Group $group): self + { + $this->group = $group; + + return $this; + } + + public function getRoleOverviewFilter(): ?array + { + return $this->roleOverviewFilter; + } + + public function setRoleOverviewFilter(?array $roleOverviewFilter): self + { + $this->roleOverviewFilter = $roleOverviewFilter; + + return $this; + } + + public function getCensusRoles(): ?array + { + return $this->census_roles; + } + + public function setCensusRoles(?array $census_roles): self + { + $this->census_roles = $census_roles; + + return $this; + } + + public function getCensusGroups(): ?array + { + return $this->census_groups; + } + + public function setCensusGroups(?array $census_groups): self + { + $this->census_groups = $census_groups; + + return $this; + } + + public function getCensusFilterMales(): ?bool + { + return $this->census_filter_males; + } + + public function setCensusFilterMales(?bool $census_filter_males): self + { + $this->census_filter_males = $census_filter_males; + + return $this; + } + + public function getCensusFilterFemales(): ?bool + { + return $this->census_filter_females; + } + + public function setCensusFilterFemales(?bool $census_filter_females): self + { + $this->census_filter_females = $census_filter_females; + + return $this; + } +} diff --git a/src/Entity/Midata/CensusGroup.php b/src/Entity/Midata/CensusGroup.php new file mode 100644 index 0000000..409a3b1 --- /dev/null +++ b/src/Entity/Midata/CensusGroup.php @@ -0,0 +1,467 @@ +id; + } + + /** + * @return mixed + */ + public function getGroupType() + { + return $this->group_type; + } + + /** + * @param mixed $group_type + */ + public function setGroupType($group_type): void + { + $this->group_type = $group_type; + } + + /** + * @return mixed + */ + public function getTotalCount() + { + return $this->total_count; + } + + /** + * @param mixed $total_count + */ + public function setTotalCount($total_count): void + { + $this->total_count = $total_count; + } + + /** + * @return mixed + */ + public function getTotalMCount() + { + return $this->total_m_count; + } + + /** + * @param mixed $total_m_count + */ + public function setTotalMCount($total_m_count): void + { + $this->total_m_count = $total_m_count; + } + + /** + * @return mixed + */ + public function getTotalFCount() + { + return $this->total_f_count; + } + + /** + * @param mixed $total_f_count + */ + public function setTotalFCount($total_f_count): void + { + $this->total_f_count = $total_f_count; + } + + /** + * @return mixed + */ + public function getLeiterMCount() + { + return $this->leiter_m_count; + } + + /** + * @param mixed $leiter_m_count + */ + public function setLeiterMCount($leiter_m_count): void + { + $this->leiter_m_count = $leiter_m_count; + } + + /** + * @return mixed + */ + public function getLeiterFCount() + { + return $this->leiter_f_count; + } + + /** + * @param mixed $leiter_f_count + */ + public function setLeiterFCount($leiter_f_count): void + { + $this->leiter_f_count = $leiter_f_count; + } + + /** + * @return mixed + */ + public function getBiberMCount() + { + return $this->biber_m_count; + } + + /** + * @param mixed $biber_m_count + */ + public function setBiberMCount($biber_m_count): void + { + $this->biber_m_count = $biber_m_count; + } + + /** + * @return mixed + */ + public function getBiberFCount() + { + return $this->biber_f_count; + } + + /** + * @param mixed $biber_f_count + */ + public function setBiberFCount($biber_f_count): void + { + $this->biber_f_count = $biber_f_count; + } + + /** + * @return mixed + */ + public function getWoelfeMCount() + { + return $this->woelfe_m_count; + } + + /** + * @param mixed $woelfe_m_count + */ + public function setWoelfeMCount($woelfe_m_count): void + { + $this->woelfe_m_count = $woelfe_m_count; + } + + /** + * @return mixed + */ + public function getWoelfeFCount() + { + return $this->woelfe_f_count; + } + + /** + * @param mixed $woelfe_f_count + */ + public function setWoelfeFCount($woelfe_f_count): void + { + $this->woelfe_f_count = $woelfe_f_count; + } + + /** + * @return mixed + */ + public function getPfadisMCount() + { + return $this->pfadis_m_count; + } + + /** + * @param mixed $pfadis_m_count + */ + public function setPfadisMCount($pfadis_m_count): void + { + $this->pfadis_m_count = $pfadis_m_count; + } + + /** + * @return mixed + */ + public function getPfadisFCount() + { + return $this->pfadis_f_count; + } + + /** + * @param mixed $pfadis_f_count + */ + public function setPfadisFCount($pfadis_f_count): void + { + $this->pfadis_f_count = $pfadis_f_count; + } + + /** + * @return mixed + */ + public function getPiosMCount() + { + return $this->pios_m_count; + } + + /** + * @param mixed $pios_m_count + */ + public function setPiosMCount($pios_m_count): void + { + $this->pios_m_count = $pios_m_count; + } + + /** + * @return mixed + */ + public function getPiosFCount() + { + return $this->pios_f_count; + } + + /** + * @param mixed $pios_f_count + */ + public function setPiosFCount($pios_f_count): void + { + $this->pios_f_count = $pios_f_count; + } + + /** + * @return mixed + */ + public function getRoverMCount() + { + return $this->rover_m_count; + } + + /** + * @param mixed $rover_m_count + */ + public function setRoverMCount($rover_m_count): void + { + $this->rover_m_count = $rover_m_count; + } + + /** + * @return mixed + */ + public function getRoverFCount() + { + return $this->rover_f_count; + } + + /** + * @param mixed $rover_f_count + */ + public function setRoverFCount($rover_f_count): void + { + $this->rover_f_count = $rover_f_count; + } + + /** + * @return mixed + */ + public function getPtaMCount() + { + return $this->pta_m_count; + } + + /** + * @param mixed $pta_m_count + */ + public function setPtaMCount($pta_m_count): void + { + $this->pta_m_count = $pta_m_count; + } + + /** + * @return mixed + */ + public function getPtaFCount() + { + return $this->pta_f_count; + } + + /** + * @param mixed $pta_f_count + */ + public function setPtaFCount($pta_f_count): void + { + $this->pta_f_count = $pta_f_count; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } + + /** + * @param mixed $name + */ + public function setName($name): void + { + $this->name = $name; + } + + /** + * @return mixed + */ + public function getGroupId() + { + return $this->group_id; + } + + /** + * @param mixed $group_id + */ + public function setGroupId($group_id): void + { + $this->group_id = $group_id; + } + + /** + * @return mixed + */ + public function getYear() + { + return $this->year; + } + + /** + * @param mixed $year + */ + public function setYear($year): void + { + $this->year = $year; + } + + public function getCalculatedTotal(): int + { + $total = 0; + $total += $this->getPiosMCount(); + $total += $this->getPiosFCount(); + $total += $this->getPtaMCount(); + $total += $this->getPtaFCount(); + $total += $this->getBiberMCount(); + $total += $this->getBiberFCount(); + $total += $this->getWoelfeMCount(); + $total += $this->getWoelfeFCount(); + $total += $this->getRoverMCount(); + $total += $this->getRoverFCount(); + $total += $this->getLeiterMCount(); + $total += $this->getLeiterFCount(); + $total += $this->getPfadisMCount(); + $total += $this->getPfadisFCount(); + return $total; + } +} diff --git a/src/Entity/Midata/Group.php b/src/Entity/Midata/Group.php index 8c38d93..209aa51 100644 --- a/src/Entity/Midata/Group.php +++ b/src/Entity/Midata/Group.php @@ -2,9 +2,13 @@ namespace App\Entity\Midata; +use App\Entity\Gamification\GamificationQuapEvent; +use App\Entity\Gamification\Login; +use App\Entity\General\GroupSettings; use App\Repository\Midata\GroupRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -78,9 +82,26 @@ class Group */ private $personRoles; + /** + * @ORM\OneToOne(targetEntity=GroupSettings::class, mappedBy="group", cascade={"persist", "remove"}) + */ + private $groupSettings; + + /** + * @ORM\OneToMany(targetEntity=Login::class, mappedBy="group") + */ + private $logins; + + /** + * @ORM\OneToMany(targetEntity=GamificationQuapEvent::class, mappedBy="group") + */ + private $gamificationQuapEvents; + public function __construct() { $this->events = new ArrayCollection(); + $this->logins = new ArrayCollection(); + $this->gamificationQuapEvents = new ArrayCollection(); } /** @@ -215,4 +236,86 @@ public function __toString() { return (string)$this->id; } + + public function getGroupSettings(): ?GroupSettings + { + return $this->groupSettings; + } + + public function setGroupSettings(?GroupSettings $groupSettings): self + { + // unset the owning side of the relation if necessary + if ($groupSettings === null && $this->groupSettings !== null) { + $this->groupSettings->setGroup(null); + } + + // set the owning side of the relation if necessary + if ($groupSettings !== null && $groupSettings->getGroup() !== $this) { + $groupSettings->setGroup($this); + } + + $this->roleOverviewFilter = $groupSettings; + + return $this; + } + + /** + * @return Collection + */ + public function getLogins(): Collection + { + return $this->logins; + } + + public function addLogin(Login $login): self + { + if (!$this->logins->contains($login)) { + $this->logins[] = $login; + $login->setGgroup($this); + } + + return $this; + } + + public function removeLogin(Login $login): self + { + if ($this->logins->removeElement($login)) { + // set the owning side to null (unless already changed) + if ($login->getGgroup() === $this) { + $login->setGgroup(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getGamificationQuapEvents(): Collection + { + return $this->gamificationQuapEvents; + } + + public function addGamificationQuapEvent(GamificationQuapEvent $gamificationQuapEvent): self + { + if (!$this->gamificationQuapEvents->contains($gamificationQuapEvent)) { + $this->gamificationQuapEvents[] = $gamificationQuapEvent; + $gamificationQuapEvent->setGroup($this); + } + + return $this; + } + + public function removeGamificationQuapEvent(GamificationQuapEvent $gamificationQuapEvent): self + { + if ($this->gamificationQuapEvents->removeElement($gamificationQuapEvent)) { + // set the owning side to null (unless already changed) + if ($gamificationQuapEvent->getGroup() === $this) { + $gamificationQuapEvent->setGroup(null); + } + } + + return $this; + } } diff --git a/src/Entity/Midata/Person.php b/src/Entity/Midata/Person.php index 344e3cb..dded0b7 100644 --- a/src/Entity/Midata/Person.php +++ b/src/Entity/Midata/Person.php @@ -3,9 +3,15 @@ namespace App\Entity\Midata; use App\Entity\Admin\GeoAddress; +use App\Entity\Gamification\GamificationQuapEvent; +use App\Entity\Gamification\LevelUpLog; +use App\Entity\Gamification\Login; +use App\Entity\Gamification\GamificationPersonProfile; use App\Repository\Midata\PersonRepository; use DateTimeImmutable; use DateTimeInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -104,6 +110,33 @@ class Person */ private $geoAddress; + /** + * @ORM\OneToMany(targetEntity=Login::class, mappedBy="person") + */ + private $logins; + + /** + * @ORM\OneToOne(targetEntity=GamificationPersonProfile::class, mappedBy="person", cascade={"persist", "remove"}) + */ + private $gamification; + + /** + * @ORM\OneToMany(targetEntity=LevelUpLog::class, mappedBy="person", cascade={"persist", "remove"}) + */ + private $levelUps; + + /** + * @ORM\OneToMany(targetEntity=GamificationQuapEvent::class, mappedBy="person", orphanRemoval=true) + */ + private $gamificationQuapEvents; + + public function __construct() + { + $this->logins = new ArrayCollection(); + $this->levelUps = new ArrayCollection(); + $this->gamificationQuapEvents = new ArrayCollection(); + } + /** * @param int $id */ @@ -311,4 +344,111 @@ public function setGeoAddress(GeoAddress $geoAddress): void { $this->geoAddress = $geoAddress; } + + /** + * @return Collection + */ + public function getLogins(): Collection + { + return $this->logins; + } + + public function addLogin(Login $login): self + { + if (!$this->logins->contains($login)) { + $this->logins[] = $login; + $login->setPerson($this); + } + + return $this; + } + + public function removeLogin(Login $login): self + { + if ($this->logins->removeElement($login)) { + // set the owning side to null (unless already changed) + if ($login->getPerson() === $this) { + $login->setPerson(null); + } + } + + return $this; + } + + public function getGamification(): ?GamificationPersonProfile + { + return $this->gamification; + } + + public function setGamification(GamificationPersonProfile $gamification): self + { + // set the owning side of the relation if necessary + if ($gamification->getPerson() !== $this) { + $gamification->setPerson($this); + } + + $this->gamification = $gamification; + + return $this; + } + + /** + * @return Collection + */ + public function getLevelUps(): Collection + { + return $this->levelUps; + } + + public function addLevelUp(LevelUpLog $displayed): self + { + if (!$this->levelUps->contains($displayed)) { + $this->levelUps[] = $displayed; + $displayed->setPerson($this); + } + + return $this; + } + + public function removeLevelUp(LevelUpLog $displayed): self + { + if ($this->levelUps->removeElement($displayed)) { + // set the owning side to null (unless already changed) + if ($displayed->getPerson() === $this) { + $displayed->setPerson(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getGamificationQuapEvents(): Collection + { + return $this->gamificationQuapEvents; + } + + public function addGamificationQuapEvent(GamificationQuapEvent $gamificationQuapEvent): self + { + if (!$this->gamificationQuapEvents->contains($gamificationQuapEvent)) { + $this->gamificationQuapEvents[] = $gamificationQuapEvent; + $gamificationQuapEvent->setPerson($this); + } + + return $this; + } + + public function removeGamificationQuapEvent(GamificationQuapEvent $gamificationQuapEvent): self + { + if ($this->gamificationQuapEvents->removeElement($gamificationQuapEvent)) { + // set the owning side to null (unless already changed) + if ($gamificationQuapEvent->getPerson() === $this) { + $gamificationQuapEvent->setPerson(null); + } + } + + return $this; + } } diff --git a/src/Entity/Midata/PersonRole.php b/src/Entity/Midata/PersonRole.php index 0fe0ba0..64de1e6 100644 --- a/src/Entity/Midata/PersonRole.php +++ b/src/Entity/Midata/PersonRole.php @@ -155,7 +155,7 @@ public function getGroup() /** * @param Role|null $role */ - public function setRole(?Role $role) + public function setRole(Role $role) { $this->role = $role; } @@ -163,7 +163,7 @@ public function setRole(?Role $role) /** * @return Role|null */ - public function getRole(): Role + public function getRole(): ?Role { return $this->role; } diff --git a/src/Entity/Midata/Role.php b/src/Entity/Midata/Role.php index 0e008c2..fa74574 100644 --- a/src/Entity/Midata/Role.php +++ b/src/Entity/Midata/Role.php @@ -25,6 +25,8 @@ class Role + public const DEPARTMENT_FINANCIER = 'Group::Abteilung::Kassier'; + public const DEPARTMENT_LEADER = 'Group::Abteilung::Abteilungsleitung'; public const DEPARTMENT_LEADER_PTA = 'Group::Abteilung::StufenleitungPta'; public const DEPARTMENT_LEADER_ROVER = 'Group::Abteilung::StufenleitungRover'; public const DEPARTMENT_LEADER_PIO = 'Group::Abteilung::StufenleitungPio'; @@ -33,6 +35,8 @@ class Role public const DEPARTMENT_LEADER_BIBER = 'Group::Abteilung::StufenleitungBiber'; public const CANTONAL_LEADER = 'Group::Kantonalverband::Kantonsleitung'; + public const CANTONAL_FINANCIER = 'Group::Kantonalverband::Kassier'; + public const CANTONAL_COACH = 'Group::Kantonalverband::Coach'; public const CANTONAL_PRESIDENT = 'Group::Kantonalverband::Praesidium'; public const CANTONAL_BIBERSTUFE_V = 'Group::Kantonalverband::VerantwortungBiberstufe'; public const CANTONAL_WOLFSTUFE_V = 'Group::Kantonalverband::VerantwortungWolfstufe'; @@ -58,6 +62,8 @@ class Role public const CANTONAL_NACHHALTIGKEIT_V = 'Group::Kantonalverband::VerantwortungNachhaltigkeit'; public const REGIONAL_LEADER = 'Group::Region::Regionalleitung'; + public const REGIONAL_FINANCIER = 'Group::Region::Kassier'; + public const REGIONAL_COACH = 'Group::Region::Coach'; public const REGIONAL_PRESIDENT = 'Group::Region::Praesidium'; public const REGIONAL_BIBERSTUFE_V = 'Group::Region::VerantwortungBiberstufe'; public const REGIONAL_WOLFSTUFE_V = 'Group::Region::VerantwortungWolfstufe'; @@ -280,20 +286,4 @@ public function setFrLabel(?string $label) { $this->frLabel = $label; } - - /** - * @return null|string - */ - public function getValidity(): ?string - { - return $this->validity; - } - - /** - * @param null|string $validity - */ - public function setValidity(?string $validity) - { - $this->validity = $validity; - } } diff --git a/src/Entity/Statistics/GroupGeoLocation.php b/src/Entity/Statistics/GroupGeoLocation.php new file mode 100644 index 0000000..4b1564a --- /dev/null +++ b/src/Entity/Statistics/GroupGeoLocation.php @@ -0,0 +1,81 @@ +id; + } + + public function setId(int $id): self + { + $this->id = $id; + return $this; + } + + public function getGroup(): ?StatisticGroup + { + return $this->group; + } + + public function setGroup(?StatisticGroup $group): self + { + $this->group = $group; + + return $this; + } + + public function getLat(): ?string + { + return $this->lat; + } + + public function setLat(string $lat): self + { + $this->lat = $lat; + + return $this; + } + + public function getLong(): ?string + { + return $this->long; + } + + public function setLong(string $long): self + { + $this->long = $long; + + return $this; + } +} diff --git a/src/Entity/Statistics/StatisticGroup.php b/src/Entity/Statistics/StatisticGroup.php new file mode 100644 index 0000000..5c21559 --- /dev/null +++ b/src/Entity/Statistics/StatisticGroup.php @@ -0,0 +1,183 @@ +children = new ArrayCollection(); + $this->geoLocations = new ArrayCollection(); + } + + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getParentGroup(): ?self + { + return $this->parent_group; + } + + public function setParentGroup(?self $parent_group): self + { + $this->parent_group = $parent_group; + + return $this; + } + + /** + * @return Collection + */ + public function getChildren(): ?Collection + { + return $this->children; + } + + public function addChild(self $child): self + { + if (!$this->children->contains($child)) { + $this->children[] = $child; + $child->setParentGroup($this); + } + + return $this; + } + + public function removeChild(self $child): self + { + if ($this->children->removeElement($child)) { + // set the owning side to null (unless already changed) + if ($child->getParentGroup() === $this) { + $child->setParentGroup(null); + } + } + + return $this; + } + + public function getGroupType(): ?GroupType + { + return $this->group_type; + } + + public function setGroupType(?GroupType $group_type): self + { + $this->group_type = $group_type; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getCanton(): ?self + { + return $this->canton; + } + + public function setCanton(?self $canton): self + { + $this->canton = $canton; + + return $this; + } + + + /** + * @return Collection + */ + public function getGeoLocations(): Collection + { + return $this->geoLocations; + } + + public function addGeoLocation(GroupGeoLocation $geoLocation): self + { + if (!$this->geoLocations->contains($geoLocation)) { + $this->geoLocations[] = $geoLocation; + $geoLocation->setGroups($this); + } + + return $this; + } + + public function removeGeoLocation(GroupGeoLocation $geoLocation): self + { + if ($this->geoLocations->removeElement($geoLocation)) { + // set the owning side to null (unless already changed) + if ($geoLocation->getGroups() === $this) { + $geoLocation->setGroups(null); + } + } + + return $this; + } +} diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php index 822b267..8da1273 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -52,16 +52,14 @@ public function onKernelException(ExceptionEvent $event): void $exception = $event->getThrowable(); + $apiError = new ApiError(); if ($exception instanceof AccessDeniedHttpException) { - $apiError = new ApiError(); $apiError->setCode(JsonResponse::HTTP_FORBIDDEN); $apiError->setMessage($this->translator->trans('api.error.accessDenied')); } elseif (!($exception instanceof ApiException)) { - $apiError = new ApiError(); $apiError->setCode(JsonResponse::HTTP_INTERNAL_SERVER_ERROR); $apiError->setMessage($this->translator->trans('api.error.unknown')); } else { - $apiError = new ApiError(); $apiError->setCode($exception->getStatusCode()); $apiError->setMessage($exception->getMessage()); } diff --git a/src/EventListener/WidgetControllerListener.php b/src/EventListener/WidgetControllerListener.php index 54d0eea..3447641 100644 --- a/src/EventListener/WidgetControllerListener.php +++ b/src/EventListener/WidgetControllerListener.php @@ -2,6 +2,7 @@ namespace App\EventListener; +use App\DTO\Model\FilterRequestData\CensusRequestData; use App\DTO\Model\FilterRequestData\DateAndDateRangeRequestData; use App\DTO\Model\FilterRequestData\DateRangeRequestData; use App\DTO\Model\FilterRequestData\DateRequestData; @@ -75,7 +76,7 @@ private function bindData(callable $controller, Request $request) if (is_null($argument->getClass())) { continue; } - if (!is_a($argument->getClass()->getName(), FilterRequestData::class, true)) { + if (!(is_a($argument->getClass()->getName(), FilterRequestData::class, true) || is_a($argument->getClass()->getName(), CensusRequestData::class, true))) { continue; } $data = $this->validateRequest($request, $argument); @@ -89,9 +90,9 @@ private function bindData(callable $controller, Request $request) /** * @param Request $request * @param ReflectionParameter $parameter - * @return FilterRequestData|null + * @return FilterRequestData|null|CensusRequestData */ - private function validateRequest(Request $request, ReflectionParameter $parameter): ?FilterRequestData + private function validateRequest(Request $request, ReflectionParameter $parameter) { $groupId = $request->get('groupId'); $group = $this->groupRepository->findOneByIdAndType($groupId, [ @@ -117,6 +118,8 @@ private function validateRequest(Request $request, ReflectionParameter $paramete return $this->validateDateRangeRequest($group, $request); case WidgetRequestData::class: return $this->validateWidgetRequest($group, $request); + case CensusRequestData::class: + return $this->validateCensusRequest($group, $request); } return null; @@ -216,6 +219,32 @@ private function validateWidgetRequest(Group $group, Request $request): WidgetRe return $data; } + private function validateCensusRequest(Group $group, Request $request): CensusRequestData + { + $m = $request->get('census-filter-males'); + $f = $request->get('census-filter-females'); + $groups = $request->get('census-filter-departments'); + $roles = $request->get('census-filter-roles'); + $rolesChoice = new Choice(WidgetDataProvider::CENSUS_ROLES); + $rolesChoice->multiple = true; + $rolesChoice->max = count(WidgetDataProvider::CENSUS_ROLES); + $rolesErrors = $this->validator->validate($roles, $rolesChoice); + + if (count($rolesErrors) > 0) { + $message = $this->translator->trans('api.error.invalidRequest'); + throw new ApiException(Response::HTTP_UNPROCESSABLE_ENTITY, $message); + } + + $data = new CensusRequestData(); + $data->setGroup($group); + $data->setGroups($groups); + $data->setRoles($roles); + $data->setFilterMales($m === 'true'); + $data->setFilterFemales($f === 'true'); + + return $data; + } + /** * @param $from * @param $to diff --git a/src/Helper/BatchedRepository.php b/src/Helper/BatchedRepository.php new file mode 100644 index 0000000..fdb42e9 --- /dev/null +++ b/src/Helper/BatchedRepository.php @@ -0,0 +1,40 @@ +repository = $repository; + $this->batchSize = $batchSize; + } + + public function add($entity) + { + $this->batchCount++; + $this->repository->add($entity, false); + if ($this->batchCount >= $this->batchSize) { + $this->batchCount = 0; + $this->flush(); + } + } + + public function flush() + { + $this->repository->flush(); + } +} diff --git a/src/Migrations/Version20230627132305.php b/src/Migrations/Version20230627132305.php new file mode 100644 index 0000000..aad23ce --- /dev/null +++ b/src/Migrations/Version20230627132305.php @@ -0,0 +1,58 @@ +addSql('CREATE SEQUENCE aggregated_person_role_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE aggregated_person_role (id INT NOT NULL, role_id INT DEFAULT NULL, group_id INT DEFAULT NULL, person_id INT DEFAULT NULL, midata_id INT DEFAULT NULL, nickname VARCHAR(255) NOT NULL, start_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, end_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_40B8716BD60322AC ON aggregated_person_role (role_id)'); + $this->addSql('CREATE INDEX IDX_40B8716BFE54D947 ON aggregated_person_role (group_id)'); + $this->addSql('CREATE INDEX IDX_40B8716B217BBB47 ON aggregated_person_role (person_id)'); + $this->addSql('CREATE INDEX IDX_40B8716B2E18B78F ON aggregated_person_role (midata_id)'); + $this->addSql('ALTER TABLE aggregated_person_role ADD CONSTRAINT FK_40B8716BD60322AC FOREIGN KEY (role_id) REFERENCES midata_role (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE aggregated_person_role ADD CONSTRAINT FK_40B8716BFE54D947 FOREIGN KEY (group_id) REFERENCES midata_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE aggregated_person_role ADD CONSTRAINT FK_40B8716B217BBB47 FOREIGN KEY (person_id) REFERENCES midata_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE aggregated_person_role ADD CONSTRAINT FK_40B8716B2E18B78F FOREIGN KEY (midata_id) REFERENCES midata_person_role (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + + $this->addSql( + "INSERT INTO aggregated_person_role (id, person_id, role_id, group_id, midata_id, nickname, start_at, end_at) + SELECT + nextval('aggregated_person_role_id_seq'), + midata_person_role.person_id, + midata_person_role.role_id, + midata_person_role.group_id, + midata_person_role.id, + midata_person.nickname, + midata_person_role.created_at, + midata_person_role.deleted_at + FROM + midata_person_role + JOIN midata_person ON midata_person_role.person_id = midata_person.id;" + ); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE aggregated_person_role_id_seq CASCADE'); + $this->addSql('DROP TABLE aggregated_person_role'); + } +} diff --git a/src/Migrations/Version20230628111910.php b/src/Migrations/Version20230628111910.php new file mode 100644 index 0000000..d027a58 --- /dev/null +++ b/src/Migrations/Version20230628111910.php @@ -0,0 +1,43 @@ +addSql('CREATE SEQUENCE group_settings_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE group_settings (id INT NOT NULL, group_id INT DEFAULT NULL, role_overview_filter TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_EC75ECBAFE54D947 ON group_settings (group_id)'); + $this->addSql('COMMENT ON COLUMN group_settings.role_overview_filter IS \'(DC2Type:array)\''); + $this->addSql('ALTER TABLE group_settings ADD CONSTRAINT FK_EC75ECBAFE54D947 FOREIGN KEY (group_id) REFERENCES midata_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql("INSERT INTO group_settings (id, group_id) + SELECT + nextval('group_settings_id_seq'), + midata_group.id + FROM + midata_group;"); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE group_settings_id_seq CASCADE'); + $this->addSql('DROP TABLE group_settings'); + } +} diff --git a/src/Migrations/Version20230630154534.php b/src/Migrations/Version20230630154534.php new file mode 100644 index 0000000..a6a7d00 --- /dev/null +++ b/src/Migrations/Version20230630154534.php @@ -0,0 +1,41 @@ +addSql('CREATE SEQUENCE statistic_group_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE statistic_group (id INT NOT NULL, parent_group_id INT NULL, group_type_id INT NOT NULL, canton_id INT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_7F70D6D161997596 ON statistic_group (parent_group_id)'); + $this->addSql('CREATE INDEX IDX_7F70D6D1434CD89F ON statistic_group (group_type_id)'); + $this->addSql('CREATE INDEX IDX_7F70D6D18D070D0B ON statistic_group (canton_id)'); + $this->addSql('ALTER TABLE statistic_group ADD CONSTRAINT FK_7F70D6D161997596 FOREIGN KEY (parent_group_id) REFERENCES statistic_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE statistic_group ADD CONSTRAINT FK_7F70D6D1434CD89F FOREIGN KEY (group_type_id) REFERENCES midata_group_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE statistic_group ADD CONSTRAINT FK_7F70D6D18D070D0B FOREIGN KEY (canton_id) REFERENCES statistic_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE statistic_group DROP CONSTRAINT FK_7F70D6D161997596'); + $this->addSql('ALTER TABLE statistic_group DROP CONSTRAINT FK_7F70D6D18D070D0B'); + $this->addSql('DROP SEQUENCE statistic_group_id_seq CASCADE'); + $this->addSql('DROP TABLE statistic_group'); + } +} diff --git a/src/Migrations/Version20230704135822.php b/src/Migrations/Version20230704135822.php new file mode 100644 index 0000000..2d9132a --- /dev/null +++ b/src/Migrations/Version20230704135822.php @@ -0,0 +1,36 @@ +addSql('CREATE SEQUENCE group_geo_location_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE group_geo_location (id INT NOT NULL, group_id INT DEFAULT NULL, lat VARCHAR(255) NOT NULL, long VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5C61B453FE54D947 ON group_geo_location (group_id)'); + $this->addSql('ALTER TABLE group_geo_location ADD CONSTRAINT FK_5C61B453FE54D947 FOREIGN KEY (group_id) REFERENCES statistic_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE group_geo_location_id_seq CASCADE'); + $this->addSql('DROP TABLE group_geo_location'); + } +} diff --git a/src/Migrations/Version20230720133258.php b/src/Migrations/Version20230720133258.php new file mode 100644 index 0000000..903a267 --- /dev/null +++ b/src/Migrations/Version20230720133258.php @@ -0,0 +1,36 @@ +addSql('CREATE SEQUENCE census_group_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE census_group (id INT NOT NULL, group_type_id INT NOT NULL, total_count INT NOT NULL, total_m_count INT NOT NULL, total_f_count INT NOT NULL, leiter_m_count INT NOT NULL, leiter_f_count INT NOT NULL, biber_m_count INT NOT NULL, biber_f_count INT NOT NULL, woelfe_m_count INT NOT NULL, woelfe_f_count INT NOT NULL, pfadis_m_count INT NOT NULL, pfadis_f_count INT NOT NULL, pios_m_count INT NOT NULL, pios_f_count INT NOT NULL, rover_m_count INT NOT NULL, rover_f_count INT NOT NULL, pta_m_count INT NOT NULL, pta_f_count INT NOT NULL, name VARCHAR(255) NOT NULL, group_id INT NOT NULL, year VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_6C90D586434CD89F ON census_group (group_type_id)'); + $this->addSql('ALTER TABLE census_group ADD CONSTRAINT FK_6C90D586434CD89F FOREIGN KEY (group_type_id) REFERENCES midata_group_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE census_group_id_seq CASCADE'); + $this->addSql('DROP TABLE census_group'); + } +} diff --git a/src/Migrations/Version20230916160452.php b/src/Migrations/Version20230916160452.php new file mode 100644 index 0000000..8378e7d --- /dev/null +++ b/src/Migrations/Version20230916160452.php @@ -0,0 +1,40 @@ +addSql('ALTER TABLE group_settings ADD census_roles TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE group_settings ADD census_groups TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE group_settings ADD census_filter_males BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE group_settings ADD census_filter_females BOOLEAN DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN group_settings.census_roles IS \'(DC2Type:array)\''); + $this->addSql('COMMENT ON COLUMN group_settings.census_groups IS \'(DC2Type:array)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE group_settings DROP census_roles'); + $this->addSql('ALTER TABLE group_settings DROP census_groups'); + $this->addSql('ALTER TABLE group_settings DROP census_filter_males'); + $this->addSql('ALTER TABLE group_settings DROP census_filter_females'); + } +} diff --git a/src/Migrations/Version20240827122505.php b/src/Migrations/Version20240827122505.php new file mode 100644 index 0000000..1ab37e6 --- /dev/null +++ b/src/Migrations/Version20240827122505.php @@ -0,0 +1,38 @@ +addSql('CREATE SEQUENCE login_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE login (id INT NOT NULL, person_id INT DEFAULT NULL, group_id INT DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_group_change BOOLEAN NOT NULL, hashed_person_id VARCHAR(255) DEFAULT NULL, role VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_AA08CB10217BBB47 ON login (person_id)'); + $this->addSql('CREATE INDEX IDX_AA08CB10FE54D947 ON login (group_id)'); + $this->addSql('ALTER TABLE login ADD CONSTRAINT FK_AA08CB10217BBB47 FOREIGN KEY (person_id) REFERENCES midata_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE login ADD CONSTRAINT FK_AA08CB10FE54D947 FOREIGN KEY (group_id) REFERENCES midata_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE login_id_seq CASCADE'); + $this->addSql('DROP TABLE login'); + } +} diff --git a/src/Migrations/Version20240911090632.php b/src/Migrations/Version20240911090632.php new file mode 100644 index 0000000..79fa102 --- /dev/null +++ b/src/Migrations/Version20240911090632.php @@ -0,0 +1,59 @@ +addSql('CREATE SEQUENCE gamification_person_profile_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE goal_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE level_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE level_up_log_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE gamification_person_profile (id INT NOT NULL, person_id INT NOT NULL, level_id INT NOT NULL, has_used_card_layer BOOLEAN NOT NULL, has_used_datafilter BOOLEAN NOT NULL, has_used_timefilter BOOLEAN NOT NULL, has_shared_el BOOLEAN NOT NULL, access_granted_count INT NOT NULL, el_filled_out BOOLEAN NOT NULL, el_revised BOOLEAN NOT NULL, el_irrelevant BOOLEAN NOT NULL, el_improved BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5649C849217BBB47 ON gamification_person_profile (person_id)'); + $this->addSql('CREATE INDEX IDX_5649C8495FB14BA7 ON gamification_person_profile (level_id)'); + $this->addSql('CREATE TABLE goal (id INT NOT NULL, level_id INT NOT NULL, required BOOLEAN NOT NULL, de_title VARCHAR(255) NOT NULL, de_information TEXT NOT NULL, de_help TEXT DEFAULT NULL, fr_title VARCHAR(255) NOT NULL, fr_information TEXT NOT NULL, fr_help TEXT DEFAULT NULL, it_title VARCHAR(255) NOT NULL, it_information TEXT NOT NULL, it_help TEXT DEFAULT NULL, key VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_FCDCEB2E5FB14BA7 ON goal (level_id)'); + $this->addSql('CREATE TABLE level (id INT NOT NULL, type INT NOT NULL, de_title VARCHAR(255) NOT NULL, fr_title VARCHAR(255) NOT NULL, it_title VARCHAR(255) NOT NULL, key INT NOT NULL, next_key INT DEFAULT NULL, required INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE level_up_log (id INT NOT NULL, person_id INT NOT NULL, level_id INT NOT NULL, date DATE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8959129C217BBB47 ON level_up_log (person_id)'); + $this->addSql('CREATE INDEX IDX_8959129C5FB14BA7 ON level_up_log (level_id)'); + $this->addSql('ALTER TABLE gamification_person_profile ADD CONSTRAINT FK_5649C849217BBB47 FOREIGN KEY (person_id) REFERENCES midata_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE gamification_person_profile ADD CONSTRAINT FK_5649C8495FB14BA7 FOREIGN KEY (level_id) REFERENCES level (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE goal ADD CONSTRAINT FK_FCDCEB2E5FB14BA7 FOREIGN KEY (level_id) REFERENCES level (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE level_up_log ADD CONSTRAINT FK_8959129C217BBB47 FOREIGN KEY (person_id) REFERENCES midata_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE level_up_log ADD CONSTRAINT FK_8959129C5FB14BA7 FOREIGN KEY (level_id) REFERENCES level (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE gamification_person_profile DROP CONSTRAINT FK_5649C8495FB14BA7'); + $this->addSql('ALTER TABLE goal DROP CONSTRAINT FK_FCDCEB2E5FB14BA7'); + $this->addSql('ALTER TABLE level_up_log DROP CONSTRAINT FK_8959129C5FB14BA7'); + $this->addSql('DROP SEQUENCE gamification_person_profile_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE goal_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE level_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE level_up_log_id_seq CASCADE'); + $this->addSql('DROP TABLE gamification_person_profile'); + $this->addSql('DROP TABLE goal'); + $this->addSql('DROP TABLE level'); + $this->addSql('DROP TABLE level_up_log'); + } +} diff --git a/src/Migrations/Version20241030131730.php b/src/Migrations/Version20241030131730.php new file mode 100644 index 0000000..8ee7d4c --- /dev/null +++ b/src/Migrations/Version20241030131730.php @@ -0,0 +1,40 @@ +addSql('CREATE SEQUENCE gamification_quap_event_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE gamification_quap_event (id INT NOT NULL, person_id INT NOT NULL, group_id INT DEFAULT NULL, questionnaire_id INT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, local_change_index INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_EA8EDF88217BBB47 ON gamification_quap_event (person_id)'); + $this->addSql('CREATE INDEX IDX_EA8EDF88FE54D947 ON gamification_quap_event (group_id)'); + $this->addSql('CREATE INDEX IDX_EA8EDF88CE07E8FF ON gamification_quap_event (questionnaire_id)'); + $this->addSql('COMMENT ON COLUMN gamification_quap_event.date IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE gamification_quap_event ADD CONSTRAINT FK_EA8EDF88217BBB47 FOREIGN KEY (person_id) REFERENCES midata_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE gamification_quap_event ADD CONSTRAINT FK_EA8EDF88FE54D947 FOREIGN KEY (group_id) REFERENCES midata_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE gamification_quap_event ADD CONSTRAINT FK_EA8EDF88CE07E8FF FOREIGN KEY (questionnaire_id) REFERENCES hc_quap_questionnaire (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP SEQUENCE gamification_quap_event_id_seq CASCADE'); + $this->addSql('DROP TABLE gamification_quap_event'); + } +} diff --git a/src/Migrations/Version20241030153416.php b/src/Migrations/Version20241030153416.php new file mode 100644 index 0000000..003ad8a --- /dev/null +++ b/src/Migrations/Version20241030153416.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE level_up_log ADD displayed BOOLEAN DEFAULT FALSE'); + $this->addSql('ALTER TABLE level_up_log ALTER date TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE level_up_log ALTER date DROP DEFAULT'); + $this->addSql('COMMENT ON COLUMN level_up_log.date IS \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE level_up_log DROP displayed'); + $this->addSql('ALTER TABLE level_up_log ALTER date TYPE DATE'); + $this->addSql('ALTER TABLE level_up_log ALTER date DROP DEFAULT'); + $this->addSql('COMMENT ON COLUMN level_up_log.date IS NULL'); + } +} diff --git a/src/Migrations/Version20241031102154.php b/src/Migrations/Version20241031102154.php new file mode 100644 index 0000000..84895ec --- /dev/null +++ b/src/Migrations/Version20241031102154.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE gamification_person_profile ADD beta_status BOOLEAN DEFAULT \'false\' NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your need + $this->addSql('ALTER TABLE gamification_person_profile DROP beta_status'); + } +} diff --git a/src/Repository/Aggregated/AggregatedDemographicCampRepository.php b/src/Repository/Aggregated/AggregatedDemographicCampRepository.php index 2318dcd..7e7c27e 100644 --- a/src/Repository/Aggregated/AggregatedDemographicCampRepository.php +++ b/src/Repository/Aggregated/AggregatedDemographicCampRepository.php @@ -30,6 +30,7 @@ public function getAllForPeriodAndMainGroup(string $from, string $to, Group $mai ->where('dc.dataPointDate >= :from') ->andWhere('dc.dataPointDate <= :to') ->andWhere('cg.group = :mainGroup') + ->orderBy('dc.startDate') ->setParameter('mainGroup', $mainGroup) ->setParameter('from', $from) ->setParameter('to', $to) diff --git a/src/Repository/Aggregated/AggregatedGeoLocationRepository.php b/src/Repository/Aggregated/AggregatedGeoLocationRepository.php index 1017d28..555899e 100644 --- a/src/Repository/Aggregated/AggregatedGeoLocationRepository.php +++ b/src/Repository/Aggregated/AggregatedGeoLocationRepository.php @@ -45,4 +45,24 @@ public function findAllForDateAndGroupType(string $date, string $groupType, int ); return $statement->fetchAllAssociative(); } + + public function findAllMeetingPointsForDate(string $date, int $groupId) + { + $connection = $this->getEntityManager()->getConnection(); + $statement = $connection->executeQuery( + "SELECT * FROM hc_aggregated_geo_location AS location + WHERE location.data_point_date = ? + AND location.shape = 'group_meeting_point' + AND location.group_id = ?;", + [ + $date, + $groupId, + ], + [ + ParameterType::STRING, + ParameterType::INTEGER, + ] + ); + return $statement->fetchAllAssociative(); + } } diff --git a/src/Repository/Aggregated/AggregatedPersonRoleRepository.php b/src/Repository/Aggregated/AggregatedPersonRoleRepository.php new file mode 100644 index 0000000..c28a264 --- /dev/null +++ b/src/Repository/Aggregated/AggregatedPersonRoleRepository.php @@ -0,0 +1,60 @@ +createQueryBuilder('a') + ->where('a.end_at IS NULL') + ->getQuery() + ->getResult(); + } + + public function getHighestAggregatedMidataIndex(): int + { + return $this->createQueryBuilder('a') + ->select('MAX(a.midata)') + ->getQuery() + ->getResult()[0][1] ?? 0; + } + + /** + * @param Group $group + * @param $start + * @param $end + * @return AggregatedPersonRole[]|null + */ + public function findByGroupInTimeframe(Group $group, $start, $end) + { + return $this->createQueryBuilder('a') + ->where('a.group = :group_id') + ->andWhere('NOT ((a.end_at IS NOT NULL AND a.end_at < :start) OR a.start_at > :end)') + ->orderBy('a.start_at') + ->setParameter('group_id', $group->getId()) + ->setParameter('start', $start) + ->setParameter('end', $end) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Repository/Gamification/GamificationPersonProfileRepository.php b/src/Repository/Gamification/GamificationPersonProfileRepository.php new file mode 100644 index 0000000..9760951 --- /dev/null +++ b/src/Repository/Gamification/GamificationPersonProfileRepository.php @@ -0,0 +1,78 @@ + + * + * @method GamificationPersonProfile|null find($id, $lockMode = null, $lockVersion = null) + * @method GamificationPersonProfile|null findOneBy(array $criteria, array $orderBy = null) + * @method GamificationPersonProfile[] findAll() + * @method GamificationPersonProfile[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GamificationPersonProfileRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GamificationPersonProfile::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(GamificationPersonProfile $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(GamificationPersonProfile $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + // /** + // * @return PersonGoal[] Returns an array of PersonGoal objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('p.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?PersonGoal + { + return $this->createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Gamification/GamificationQuapEventRepository.php b/src/Repository/Gamification/GamificationQuapEventRepository.php new file mode 100644 index 0000000..8726a53 --- /dev/null +++ b/src/Repository/Gamification/GamificationQuapEventRepository.php @@ -0,0 +1,95 @@ + + * + * @method GamificationQuapEvent|null find($id, $lockMode = null, $lockVersion = null) + * @method GamificationQuapEvent|null findOneBy(array $criteria, array $orderBy = null) + * @method GamificationQuapEvent[] findAll() + * @method GamificationQuapEvent[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GamificationQuapEventRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GamificationQuapEvent::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(GamificationQuapEvent $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(GamificationQuapEvent $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function getUniquieIds(Person $person) + { + return $this->createQueryBuilder('e') + ->join('e.questionnaire', 'q') + ->select('e.local_change_index, q.type') + ->where('e.person = :person') + ->groupBy('e.local_change_index') + ->addGroupBy('q.id') + ->setParameter('person', $person) + ->orderBy('e.local_change_index', 'ASC') + ->getQuery() + ->getResult(); + } + + // /** + // * @return GamificationQuapEvent[] Returns an array of GamificationQuapEvent objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('g.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?GamificationQuapEvent + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Gamification/GoalRepository.php b/src/Repository/Gamification/GoalRepository.php new file mode 100644 index 0000000..bd384d1 --- /dev/null +++ b/src/Repository/Gamification/GoalRepository.php @@ -0,0 +1,78 @@ + + * + * @method Goal|null find($id, $lockMode = null, $lockVersion = null) + * @method Goal|null findOneBy(array $criteria, array $orderBy = null) + * @method Goal[] findAll() + * @method Goal[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GoalRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Goal::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(Goal $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(Goal $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + // /** + // * @return Goal[] Returns an array of Goal objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('g.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Goal + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Gamification/LevelRepository.php b/src/Repository/Gamification/LevelRepository.php new file mode 100644 index 0000000..2388b2a --- /dev/null +++ b/src/Repository/Gamification/LevelRepository.php @@ -0,0 +1,88 @@ + + * + * @method Level|null find($id, $lockMode = null, $lockVersion = null) + * @method Level|null findOneBy(array $criteria, array $orderBy = null) + * @method Level[] findAll() + * @method Level[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class LevelRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Level::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(Level $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(Level $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function findNextLevel(Level $level) + { + return $this->createQueryBuilder('l') + ->where('l.key = :key') + ->setParameter('key', $level->getNextKey()) + ->getQuery() + ->getResult(); + } + + + // /** + // * @return Level[] Returns an array of Level objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('l') + ->andWhere('l.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('l.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Level + { + return $this->createQueryBuilder('l') + ->andWhere('l.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Gamification/LevelUpLogRepository.php b/src/Repository/Gamification/LevelUpLogRepository.php new file mode 100644 index 0000000..92e1851 --- /dev/null +++ b/src/Repository/Gamification/LevelUpLogRepository.php @@ -0,0 +1,90 @@ + + * + * @method LevelUpLog|null find($id, $lockMode = null, $lockVersion = null) + * @method LevelUpLog|null findOneBy(array $criteria, array $orderBy = null) + * @method LevelUpLog[] findAll() + * @method LevelUpLog[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class LevelUpLogRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, LevelUpLog::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(LevelUpLog $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(LevelUpLog $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function retrieveLastMonth() + { + $cutoffDate = (new \DateTimeImmutable('first day of last month'))->setTime(0, 0); + return $this->createQueryBuilder('l') + ->where('l.date >= :date') + ->orderBy('l.person') + ->addOrderBy('l.level') + ->setParameter('date', $cutoffDate) + ->getQuery() + ->getResult(); + } + + // /** + // * @return LevelUpLog[] Returns an array of LevelUpLog objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('l') + ->andWhere('l.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('l.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?LevelUpLog + { + return $this->createQueryBuilder('l') + ->andWhere('l.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Gamification/LoginRepository.php b/src/Repository/Gamification/LoginRepository.php new file mode 100644 index 0000000..f3f33e7 --- /dev/null +++ b/src/Repository/Gamification/LoginRepository.php @@ -0,0 +1,104 @@ + + * + * @method Login|null find($id, $lockMode = null, $lockVersion = null) + * @method Login|null findOneBy(array $criteria, array $orderBy = null) + * @method Login[] findAll() + * @method Login[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class LoginRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Login::class); + } + + public function findAllActiveBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + { + $criteria['hashed_id'] = null; + return $this->findBy($criteria, $orderBy, $limit, $offset); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(Login $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(Login $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function pseudonymizeAllOlderThan18Months(callable $hashFunc) + { + $thresholdDate = new DateTime('-18 months'); + $entities = $this->createQueryBuilder('e') + ->where('e.date < :thresholdDate') + ->setParameter('thresholdDate', $thresholdDate) + ->getQuery() + ->getResult(); + foreach ($entities as $entity) { + $hashedId = $hashFunc($entity->getPerson()->getId()); + $entity->setPerson(null); + $entity->setHashedPersonId($hashedId); + $this->_em->persist($entity); + } + $this->_em->flush(); + return $entities; + } + + // /** + // * @return Login[] Returns an array of Login objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('l') + ->andWhere('l.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('l.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Login + { + return $this->createQueryBuilder('l') + ->andWhere('l.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/General/GroupSettingsRepository.php b/src/Repository/General/GroupSettingsRepository.php new file mode 100644 index 0000000..342f106 --- /dev/null +++ b/src/Repository/General/GroupSettingsRepository.php @@ -0,0 +1,83 @@ + + * + * @method GroupSettings|null find($id, $lockMode = null, $lockVersion = null) + * @method GroupSettings|null findOneBy(array $criteria, array $orderBy = null) + * @method GroupSettings[] findAll() + * @method GroupSettings[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GroupSettingsRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GroupSettings::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(GroupSettings $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(GroupSettings $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function flush() + { + $this->_em->flush(); + } + + // /** + // * @return GroupSettings[] Returns an array of GroupSettings objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('g.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?GroupSettings + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Midata/CensusGroupRepository.php b/src/Repository/Midata/CensusGroupRepository.php new file mode 100644 index 0000000..839331a --- /dev/null +++ b/src/Repository/Midata/CensusGroupRepository.php @@ -0,0 +1,92 @@ + + * + * @method CensusGroup|null find($id, $lockMode = null, $lockVersion = null) + * @method CensusGroup|null findOneBy(array $criteria, array $orderBy = null) + * @method CensusGroup[] findAll() + * @method CensusGroup[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class CensusGroupRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, CensusGroup::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(CensusGroup $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(CensusGroup $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function getLatestYear(): int + { + $rsm = new ResultSetMapping(); + $rsm->addScalarResult('max', 'max', 'integer'); + $query = $this->_em->createNativeQuery('SELECT MAX(year) FROM census_group;', $rsm); + $result = $query->getSingleScalarResult(); + if (is_null($result)) { + throw new \Exception("No date found in census table."); + } + return $result; + } + + // /** + // * @return CensusGroup[] Returns an array of CensusGroup objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('c.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?CensusGroup + { + return $this->createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Midata/GroupRepository.php b/src/Repository/Midata/GroupRepository.php index e2f8c77..34d014e 100644 --- a/src/Repository/Midata/GroupRepository.php +++ b/src/Repository/Midata/GroupRepository.php @@ -229,16 +229,4 @@ public function findAllDepartmentsFromCanton(int $cantonId): array ->getQuery() ->getArrayResult(); } - - public function findAllDepartmentsForFederation(int $federationId): array - { - return $this->createQueryBuilder('g') - ->join('g.groupType', 'gt') - ->where('g.parentGroup = :federationId') - ->andWhere('gt.groupType IN (:groupType)') - ->setParameter('federationId', $federationId) - ->setParameter('groupType', ['Group::Kantonalverband']) - ->getQuery() - ->getArrayResult(); - } } diff --git a/src/Repository/Midata/PersonRoleRepository.php b/src/Repository/Midata/PersonRoleRepository.php index d79eadd..71437c1 100644 --- a/src/Repository/Midata/PersonRoleRepository.php +++ b/src/Repository/Midata/PersonRoleRepository.php @@ -48,8 +48,14 @@ public function findRolesForPersonInGroup(int $groupId, int $personId) * @return array|PersonRole[] * @throws \Doctrine\DBAL\Exception */ - public function findAllByDate(string $date, array $groupIds, array $groupTypes, array $leaderRoles, array $memberRoles, array $rolePriority): array - { + public function findAllByDate( + string $date, + array $groupIds, + array $groupTypes, + array $leaderRoles, + array $memberRoles, + array $rolePriority + ): array { $connection = $this->_em->getConnection(); $statement = $connection->executeQuery( "SELECT * FROM ( @@ -184,7 +190,15 @@ public function findMemberCountForPeriodByGenderGroupTypeAndGroupIds( where midata_person.group_id in (?) and midata_role.role_type in (?) );", - [$groupIds, $gender, $date, $date, WidgetAggregator::$memberRoleTypes, $groupIds, WidgetAggregator::$leadersRoleTypes], + [ + $groupIds, + $gender, + $date, + $date, + WidgetAggregator::$memberRoleTypes, + $groupIds, + WidgetAggregator::$leadersRoleTypes + ], [ Connection::PARAM_INT_ARRAY, ParameterType::STRING, @@ -252,7 +266,9 @@ public function findPersonRoles(int $personId, string $endDate, array $groupIds) ->where('person.id = :id') ->andWhere('g.id IN (:groupIds)') ->andWhere('role.roleType IN (:roles)') - ->andWhere('(personRole.createdAt < :endDate AND (personRole.deletedAt IS NULL OR personRole.deletedAt > :endDate))') + ->andWhere( + '(personRole.createdAt < :endDate AND (personRole.deletedAt IS NULL OR personRole.deletedAt > :endDate))' + ) ->setParameter('id', $personId) ->setParameter('endDate', $endDate) ->setParameter('groupIds', $groupIds) @@ -724,4 +740,16 @@ public function findAllPersonInGroupByRole(array $groupTypes, array $roleTypes): ); return $statement->fetchAllAssociative(); } + + /** + * @return PersonRole[] + */ + public function findAllWithHigherIndex(int $highestAggregatedMidataIndex): array + { + return $this->createQueryBuilder('a') + ->andWhere('a.id > :index') + ->setParameter('index', $highestAggregatedMidataIndex) + ->getQuery() + ->getResult(); + } } diff --git a/src/Repository/Security/PermissionRepository.php b/src/Repository/Security/PermissionRepository.php index e63805e..eff4d50 100644 --- a/src/Repository/Security/PermissionRepository.php +++ b/src/Repository/Security/PermissionRepository.php @@ -152,6 +152,33 @@ public function findHighestByIdOrEmail(Group $group, int $id, string $email): ?P ->getOneOrNullResult(); } + /** + * @param Group $group + * @param int $id + * @param string $email + * @return Permission|null + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function findHighestById(Group $group, int $id): ?Permission + { + $query = $this->createQueryBuilder('permission'); + + return $query + ->where('permission.group = :group') + ->andWhere('permission.person = :person') + ->andWhere($query->expr()->orX( + $query->expr()->gt('permission.expirationDate', ':now'), + $query->expr()->isNull('permission.expirationDate') + )) + ->orderBy('permission.permissionType') + ->setMaxResults(1) + ->setParameter('group', $group->getId()) + ->setParameter('person', $id) + ->setParameter('now', new \DateTime()) + ->getQuery() + ->getOneOrNullResult(); + } + public function endAllOpenPermissions(): void { $now = new \DateTimeImmutable(); diff --git a/src/Repository/Statistics/GroupGeoLocationRepository.php b/src/Repository/Statistics/GroupGeoLocationRepository.php new file mode 100644 index 0000000..774b697 --- /dev/null +++ b/src/Repository/Statistics/GroupGeoLocationRepository.php @@ -0,0 +1,96 @@ + + * + * @method GroupGeoLocation|null find($id, $lockMode = null, $lockVersion = null) + * @method GroupGeoLocation|null findOneBy(array $criteria, array $orderBy = null) + * @method GroupGeoLocation[] findAll() + * @method GroupGeoLocation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GroupGeoLocationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GroupGeoLocation::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(GroupGeoLocation $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function deleteAll() + { + $this->_em->createQueryBuilder() + ->delete(GroupGeoLocation::class, 'g') + ->getQuery() + ->execute(); + $this->_em->flush(); + $metadata = $this->_em->getClassMetaData(GroupGeoLocation::class); + $metadata->setIdGenerator(new AssignedGenerator()); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(GroupGeoLocation $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function flush() + { + $this->_em->flush(); + } + + // /** + // * @return GroupGeoLocation[] Returns an array of GroupGeoLocation objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('g.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?GroupGeoLocation + { + return $this->createQueryBuilder('g') + ->andWhere('g.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Statistics/StatisticGroupRepository.php b/src/Repository/Statistics/StatisticGroupRepository.php new file mode 100644 index 0000000..3e99309 --- /dev/null +++ b/src/Repository/Statistics/StatisticGroupRepository.php @@ -0,0 +1,124 @@ + + * + * @method StatisticGroup|null find($id, $lockMode = null, $lockVersion = null) + * @method StatisticGroup|null findOneBy(array $criteria, array $orderBy = null) + * @method StatisticGroup[] findAll() + * @method StatisticGroup[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class StatisticGroupRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, StatisticGroup::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(StatisticGroup $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(StatisticGroup $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function deleteAll() + { + $this->_em->createQueryBuilder() + ->delete(StatisticGroup::class, 'g') + ->getQuery() + ->execute(); + $this->_em->flush(); + $metadata = $this->_em->getClassMetaData(StatisticGroup::class); + $metadata->setIdGenerator(new AssignedGenerator()); + } + + public function flush() + { + $this->_em->flush(); + } + + /** + * Finds all the children of the group that are group type 2,3 or 8. (Kanton, Region, Abteilung) + * @param int $groupId + * @return int[] + * @throws Exception + */ + public function findAllRelevantChildGroups(int $groupId): array + { + $conn = $this->_em->getConnection(); + $query = $conn->executeQuery( + "WITH RECURSIVE parent as ( + SELECT statistic_group.*, midata_group_type.group_type + FROM statistic_group + JOIN midata_group_type ON group_type_id = midata_group_type.id + WHERE statistic_group.id = (?) + UNION + SELECT child.*, midata_group_type.group_type + FROM statistic_group child, parent p, midata_group_type + Where child.parent_group_id = p.id AND midata_group_type.group_type IN ('Group::Abteilung', 'Group::Region', 'Group::Kantonalverband') AND child.group_type_id = midata_group_type.id + ) SELECT * from parent;", + [$groupId], + [ParameterType::INTEGER] + ); + return $query->fetchFirstColumn(); + } + + // /** + // * @return StatisticGroup[] Returns an array of StatisticGroup objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('s') + ->andWhere('s.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('s.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?StatisticGroup + { + return $this->createQueryBuilder('s') + ->andWhere('s.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Service/Aggregator/DemographicCampAggregator.php b/src/Service/Aggregator/DemographicCampAggregator.php index fce604c..e47f842 100644 --- a/src/Service/Aggregator/DemographicCampAggregator.php +++ b/src/Service/Aggregator/DemographicCampAggregator.php @@ -223,7 +223,9 @@ private function getRelevantPersonRole(array $personRoles, DateTimeImmutable $ev if ($personRole->getCreatedAt() > $eventDate) { continue; } + // TODO: Warning this is just to prevent the app from breaking due to unstable API, could mess up the logic if ( + !is_null($personRole->getRole()) && !in_array( $personRole->getRole()->getRoleType(), array_merge(self::$leadersRoleTypes, self::$memberRoleTypes, self::$mainGroupRoleTypes) diff --git a/src/Service/Aggregator/GeoLocationAggregator.php b/src/Service/Aggregator/GeoLocationAggregator.php index 9050aca..083b4da 100644 --- a/src/Service/Aggregator/GeoLocationAggregator.php +++ b/src/Service/Aggregator/GeoLocationAggregator.php @@ -5,13 +5,17 @@ use App\Entity\Aggregated\AggregatedGeoLocation; use App\Entity\Midata\Group; use App\Entity\Midata\Role; +use App\Entity\Statistics\GroupGeoLocation; +use App\Model\LogMessage\SimpleLogMessage; use App\Repository\Aggregated\AggregatedGeoLocationRepository; use App\Repository\Midata\GroupRepository; use App\Repository\Midata\PersonRepository; use App\Repository\Midata\PersonRoleRepository; use App\Repository\Midata\RoleRepository; +use App\Repository\Statistics\GroupGeoLocationRepository; use DateInterval; use DateTime; +use Digio\Logging\GelfLogger; use Doctrine\ORM\EntityManagerInterface; class GeoLocationAggregator extends WidgetAggregator @@ -36,13 +40,20 @@ class GeoLocationAggregator extends WidgetAggregator /** @var AggregatedGeoLocationRepository $geoLocationRepository */ private $geoLocationRepository; + private GroupGeoLocationRepository $groupGeoLocationRepository; + + protected GelfLogger $gelfLogger; + + public function __construct( EntityManagerInterface $em, GroupRepository $groupRepository, PersonRepository $personRepository, PersonRoleRepository $personRoleRepository, RoleRepository $roleRepository, - AggregatedGeoLocationRepository $geoLocationRepository + AggregatedGeoLocationRepository $geoLocationRepository, + GroupGeoLocationRepository $groupGeoLocationRepository, + GelfLogger $gelfLogger ) { parent::__construct($groupRepository); @@ -52,6 +63,8 @@ public function __construct( $this->personRoleRepository = $personRoleRepository; $this->roleRepository = $roleRepository; $this->geoLocationRepository = $geoLocationRepository; + $this->groupGeoLocationRepository = $groupGeoLocationRepository; + $this->gelfLogger = $gelfLogger; } /** @@ -92,6 +105,7 @@ public function aggregate(DateTime $startDate = null): void $this->geoLocationRepository, $mainGroup->getId() ); + if ($this->isDataExistsForDate($startPointDate->format('Y-m-d 00:00:00'), $existingData)) { continue; } @@ -109,9 +123,12 @@ public function aggregate(DateTime $startDate = null): void parent::$roleTypePriority ); + if ($startPointDate->getTimestamp() === $maxDate->getTimestamp()) { + $this->aggregateGroupMeetingPoints($mainGroup, $startPointDate); + } $this->createWidgetsFromData($personGroups, $mainGroup, $startPointDate); + $this->em->flush(); } - $this->em->flush(); $this->em->clear(); } @@ -149,6 +166,29 @@ private function createWidgetsFromData(array $data, Group $group, DateTime $date } } + private function aggregateGroupMeetingPoints(Group $group, DateTime $dateTime) + { + $geoLocations = $this->groupGeoLocationRepository->findBy(['group' => $group->getId()]); + foreach ($geoLocations as $geoLocation) { + if (!is_numeric($geoLocation->getLong()) || !is_numeric($geoLocation->getLat())) { + $this->gelfLogger->warning(new SimpleLogMessage('Geolocation ' . $geoLocation->getId() . ' from group ' . $group->getId() . ' could not be aggregated because lat or long is not numeric. (lat: ' . $geoLocation->getLat() . ' ,long: ' . $geoLocation->getLong() . ')')); + continue; + } + $widget = new AggregatedGeoLocation(); + $widget->setGroup($group); + $widget->setLabel(''); // no label + $widget->setGroupType($group->getGroupType()->getGroupType()); // no group type + $widget->setPersonType('group_meeting_point'); // isn't a person + $widget->setShape('group_meeting_point'); + $widget->setCreatedAt(new \DateTimeImmutable()); + $widget->setDataPointDate(new \DateTimeImmutable($dateTime->format('Y-m-d'))); + $widget->setLongitude($geoLocation->getLong()); + $widget->setLatitude($geoLocation->getLat()); + + $this->em->persist($widget); + } + } + /** * @param Role $role * @return string|null diff --git a/src/Service/Aggregator/RoleAggregator.php b/src/Service/Aggregator/RoleAggregator.php new file mode 100644 index 0000000..60d17d5 --- /dev/null +++ b/src/Service/Aggregator/RoleAggregator.php @@ -0,0 +1,73 @@ +em = $em; + $this->personRoleRepository = $personRoleRepository; + $this->midataPersonRoleRepository = $midataPersonRoleRepository; + } + + public function getName(): string + { + return self::NAME; + } + + public function aggregate(DateTime $startDate = null) + { + // check if People have quit their jobs, and if so aggregate it + $listOfUnfinished = $this->personRoleRepository->getUnfinished(); + foreach ($listOfUnfinished as $unfinished) { + $midataObject = $this->midataPersonRoleRepository->find($unfinished->getMidata()); + $deletedAt = $midataObject->getDeletedAt(); + $unfinished->setEndAt($deletedAt); + $this->em->persist($unfinished); + } + $this->em->flush(); + + // check if people have been employed and aggregate it + $highestAggregatedMidataIndex = $this->personRoleRepository->getHighestAggregatedMidataIndex(); + $newPersonRoles = $this->midataPersonRoleRepository->findAllWithHigherIndex($highestAggregatedMidataIndex); + foreach ($newPersonRoles as $newPersonRole) { + if (is_null($newPersonRole->getRole())) { + continue; + } + $aggregatedPersonRole = new AggregatedPersonRole(); + $aggregatedPersonRole->setMidata($newPersonRole); + $aggregatedPersonRole->setEndAt($newPersonRole->getDeletedAt()); + $aggregatedPersonRole->setGroup($newPersonRole->getGroup()); + $aggregatedPersonRole->setNickname($newPersonRole->getPerson()->getNickname()); + $aggregatedPersonRole->setPerson($newPersonRole->getPerson()); + $aggregatedPersonRole->setRole($newPersonRole->getRole()); + $aggregatedPersonRole->setStartAt($newPersonRole->getCreatedAt()); + + $this->em->persist($aggregatedPersonRole); + } + $this->em->flush(); + } +} diff --git a/src/Service/Aggregator/WidgetAggregator.php b/src/Service/Aggregator/WidgetAggregator.php index b0547c3..7ecc765 100644 --- a/src/Service/Aggregator/WidgetAggregator.php +++ b/src/Service/Aggregator/WidgetAggregator.php @@ -221,7 +221,7 @@ public function findLeaderGroupTypeForRoleType(PersonRole $personRole): string { foreach (self::$leaderRoleTypesByGroupType as $groupType => $roleTypes) { foreach ($roleTypes as $roleType) { - if ($roleType === $personRole->getRole()->getRoleType()) { + if (!is_null($personRole->getRole()) && $roleType === $personRole->getRole()->getRoleType()) { return $groupType; } } @@ -238,7 +238,7 @@ public function findGroupAndPersonTypeByPersonRolesHierarchy(array $personRoles) foreach (self::$leadersRoleTypes as $leaderRole) { /** @var PersonRole $personRole */ foreach ($personRoles as $personRole) { - if (trim($personRole->getRole()->getRoleType()) !== $leaderRole) { + if (!is_null($personRole->getRole()) && trim($personRole->getRole()->getRoleType()) !== $leaderRole) { continue; } return ['leaders', $this->findLeaderGroupTypeForRoleType($personRole)]; @@ -247,7 +247,7 @@ public function findGroupAndPersonTypeByPersonRolesHierarchy(array $personRoles) foreach (self::$memberRoleTypes as $memberRole) { /** @var PersonRole $personRole */ foreach ($personRoles as $personRole) { - if (trim($personRole->getRole()->getRoleType()) !== $memberRole) { + if (!is_null($personRole->getRole()) && trim($personRole->getRole()->getRoleType()) !== $memberRole) { continue; } return ['members', $personRole->getGroup()->getGroupType()->getGroupType()]; diff --git a/src/Service/Apps/Census/CensusFilter.php b/src/Service/Apps/Census/CensusFilter.php new file mode 100644 index 0000000..9e83ab1 --- /dev/null +++ b/src/Service/Apps/Census/CensusFilter.php @@ -0,0 +1,76 @@ +getGroups(); + $roles = $censusRequestData->getRoles(); + foreach ($censusGroups as $group) { + if (array_search($group->getId(), $groups)) { + continue; + } + $filteredGroup = clone $group; + if (array_search('leiter', $roles)) { + $filteredGroup->setLeiterMCount(0); + $filteredGroup->setLeiterFCount(0); + } + if (array_search('biber', $roles)) { + $filteredGroup->setBiberMCount(0); + $filteredGroup->setBiberFCount(0); + } + if (array_search('woelfe', $roles)) { + $filteredGroup->setWoelfeMCount(0); + $filteredGroup->setWoelfeFCount(0); + } + if (array_search('pfadis', $roles)) { + $filteredGroup->setPfadisMCount(0); + $filteredGroup->setPfadisFCount(0); + } + if (array_search('rover', $roles)) { + $filteredGroup->setRoverMCount(0); + $filteredGroup->setRoverFCount(0); + } + if (array_search('pio', $roles)) { + $filteredGroup->setPiosMCount(0); + $filteredGroup->setPiosFCount(0); + } + if (array_search('pta', $roles)) { + $filteredGroup->setPtaMCount(0); + $filteredGroup->setPtaFCount(0); + } + if ($censusRequestData->isFilterMales()) { + $filteredGroup->setLeiterMCount(0); + $filteredGroup->setBiberMCount(0); + $filteredGroup->setWoelfeMCount(0); + $filteredGroup->setPfadisMCount(0); + $filteredGroup->setRoverMCount(0); + $filteredGroup->setPiosMCount(0); + $filteredGroup->setPtaMCount(0); + } + if ($censusRequestData->isFilterFemales()) { + $filteredGroup->setLeiterFCount(0); + $filteredGroup->setBiberFCount(0); + $filteredGroup->setWoelfeFCount(0); + $filteredGroup->setPfadisFCount(0); + $filteredGroup->setRoverFCount(0); + $filteredGroup->setPiosFCount(0); + $filteredGroup->setPtaFCount(0); + } + $filteredGroups[] = $filteredGroup; + } + return $filteredGroups; + } +} diff --git a/src/Service/Apps/Quap/QuapService.php b/src/Service/Apps/Quap/QuapService.php index 3f0817b..0ef565d 100644 --- a/src/Service/Apps/Quap/QuapService.php +++ b/src/Service/Apps/Quap/QuapService.php @@ -182,14 +182,14 @@ public function getAnswersForSubdepartments(Group $group, ?\DateTimeImmutable $d { $parentGroupType = $group->getGroupType()->getGroupType(); - $subdepartments = []; + $ids = []; if ($parentGroupType === GroupType::CANTON) { $subdepartments = $this->groupRepository->findAllDepartmentsFromCanton($group->getId()); - } elseif ($parentGroupType === GroupType::FEDERATION) { - $subdepartments = $this->groupRepository->findAllDepartmentsForFederation($group->getId()); + } elseif ($parentGroupType === GroupType::REGION) { + $subdepartments = $this->groupRepository->findAllRelevantSubGroupsByParentGroupId($group->getId(), [GroupType::DEPARTMENT]); + } else { + throw new \Exception(); } - - $ids = []; foreach ($subdepartments as $group) { $ids[] = $group['id']; } diff --git a/src/Service/Census/CensusAPIService.php b/src/Service/Census/CensusAPIService.php new file mode 100644 index 0000000..115688c --- /dev/null +++ b/src/Service/Census/CensusAPIService.php @@ -0,0 +1,44 @@ +guzzleWrapper = $guzzleWrapper; + $this->url = $url; + $this->apiToken = $apiToken; + } + + + public function getCensusData(int $year): Http\CurlResponse + { + $endpoint = $this->url . '/group_health/census_evaluations.json?token=' . $this->apiToken . '&year=' . $year; + return $this->guzzleWrapper->getJson($endpoint, null, []); + } + + /** + * @return string + */ + public function getUrl(): string + { + return $this->url; + } +} diff --git a/src/Service/Census/CensusDateProvider.php b/src/Service/Census/CensusDateProvider.php new file mode 100644 index 0000000..97e3d82 --- /dev/null +++ b/src/Service/Census/CensusDateProvider.php @@ -0,0 +1,32 @@ +censusGroupRepository = $censusGroupRepository; + } + + /** + * This funciton returns the latest year we have census data for in the database. + * It was created so that census widgets are still usable in the period between the start of a new year until + * the census data is updated. + * @return int + * @throws \Exception + */ + public function getLatestYear(): int + { + return $this->censusGroupRepository->getLatestYear(); + } + + public function getRelevantDateRange(): array + { + return range($this->getLatestYear() - 5, $this->getLatestYear()); + } +} diff --git a/src/Service/DataProvider/CensusDataProvider.php b/src/Service/DataProvider/CensusDataProvider.php new file mode 100644 index 0000000..975e1e6 --- /dev/null +++ b/src/Service/DataProvider/CensusDataProvider.php @@ -0,0 +1,329 @@ +censusGroupRepository = $censusGroupRepository; + $this->statisticGroupRepository = $statisticGroupRepository; + $this->censusDateProvider = $censusDateProvider; + parent::__construct( + $groupRepository, + $groupTypeRepository, + $translator + ); + } + + public function getPreviewData(Group $group) + { + $flattenedGroups = $this->getRelevantGroups($group); + $return = [ + 'm' => [ + 'leiter' => 0, + 'biber' => 0, + 'woelfe' => 0, + 'pfadis' => 0, + 'rover' => 0, + 'pio' => 0, + 'pta' => 0 + ], + 'f' => [ + 'leiter' => 0, + 'biber' => 0, + 'woelfe' => 0, + 'pfadis' => 0, + 'rover' => 0, + 'pio' => 0, + 'pta' => 0 + ] + ]; + foreach ($flattenedGroups as $group) { + $censusGroup = $this->censusGroupRepository->findOneBy(['group_id' => $group->getId(), 'year' => $this->censusDateProvider->getLatestYear()]); + if (!is_null($censusGroup)) { + $return['m']['leiter'] += $censusGroup->getLeiterMCount(); + $return['m']['biber'] += $censusGroup->getBiberMCount(); + $return['m']['woelfe'] += $censusGroup->getWoelfeMCount(); + $return['m']['pfadis'] += $censusGroup->getPfadisMCount(); + $return['m']['rover'] += $censusGroup->getRoverMCount(); + $return['m']['pio'] += $censusGroup->getPiosMCount(); + $return['m']['pta'] += $censusGroup->getPtaMCount(); + + $return['f']['leiter'] += $censusGroup->getLeiterFCount(); + $return['f']['biber'] += $censusGroup->getBiberFCount(); + $return['f']['woelfe'] += $censusGroup->getWoelfeFCount(); + $return['f']['pfadis'] += $censusGroup->getPfadisFCount(); + $return['f']['rover'] += $censusGroup->getRoverFCount(); + $return['f']['pio'] += $censusGroup->getPiosFCount(); + $return['f']['pta'] += $censusGroup->getPtaFCount(); + } + } + return $return; + } + + + /** + * Returns a COPY of the group tree that is flattend. + * For example the structure becomes: + * + * Kanton -> Region -> Abteilung + * + * instead of: + * + * Kanton -> Region -> Region -> Abteilung + * + * @param int[] $groups + * @return StatisticGroup[] + */ + public function flattenGroupTree(array $groupIds) + { + $groups = []; + foreach ($groupIds as $groupId) { + $groups[] = $this->statisticGroupRepository->findOneBy(['id' => $groupId]); + } + $flattenedGroups = []; + foreach ($groups as $group) { + $newGroup = $this->getNewGroupWithRelevantParent($group); + if (!is_null($newGroup)) { + $flattenedGroups[] = $newGroup; + } + } + return $flattenedGroups; + } + + /** + * @param StatisticGroup $baseGroup + * @return StatisticGroup|null + */ + public function getNewGroupWithRelevantParent(StatisticGroup $baseGroup) + { + if ($baseGroup->getGroupType()->getGroupType() === GroupType::DEPARTMENT) { + $clonedGroup = clone $baseGroup; + $relevantParent = $this->findHighestRelevantRegion($clonedGroup); + $clonedGroup->setParentGroup($relevantParent); + return $clonedGroup; + } + if ($baseGroup->getGroupType()->getGroupType() === GroupType::REGION) { + if ($baseGroup->getParentGroup()->getGroupType()->getGroupType() === GroupType::REGION) { + return null; + } + } + if ($baseGroup->getGroupType()->getGroupType() === GroupType::CANTON) { + if ($baseGroup->getParentGroup()->getGroupType()->getGroupType() === GroupType::CANTON) { + return null; + } + } + return $baseGroup; + } + + public function findHighestRelevantRegion(StatisticGroup $group) + { + $parentGroup = $group->getParentGroup(); + if (is_null($parentGroup)) { + return $group; + } + if ($parentGroup->getGroupType()->getGroupType() === GroupType::REGION) { + return $this->findHighestRelevantRegion($parentGroup); + } else { + if ($group->getGroupType()->getGroupType() === GroupType::DEPARTMENT && $parentGroup->getGroupType()->getGroupType() === GroupType::CANTON) { + return $this->findHighestRelevantRegion($parentGroup); + } + return $group; + } + } + + + /** + * @param StatisticGroup[] $groups + * @return StatisticGroup[] + */ + public function sortGroups(array $groups) + { + $regions = array_filter($groups, function ($group) { + return $group->getGroupType()->getGroupType() === GroupType::REGION; + }); + $departments = array_filter($groups, function ($group) { + return $group->getGroupType()->getGroupType() === GroupType::DEPARTMENT; + }); + usort($regions, function (StatisticGroup $a, StatisticGroup $b) { + return strcmp($a->getName(), $b->getName()); + }); + usort($departments, function (StatisticGroup $a, StatisticGroup $b) { + return strcmp($a->getName(), $b->getName()); + }); + + $return = []; + foreach ($regions as $region) { + $return[] = $region; + foreach ($departments as $department) { + if ($department->getParentGroup()->getId() === $region->getId()) { + $return[] = $department; + } + } + } + + foreach ($departments as $department) { + if (!array_search($department, $return)) { + $return[] = $department; + } + } + return $return; + } + + public function getRelevantGroups(Group $group) + { + $groupIds = array_filter($this->statisticGroupRepository->findAllRelevantChildGroups($group->getId()), function ($id) use ($group) { + // We need to filter because the function also returns the group itself + return !($id === $group->getId()); + }); + return $this->flattenGroupTree($groupIds); + } + + public function getTableData(Group $group, CensusRequestData $censusRequestData) + { + $flattenedGroups = $this->getRelevantGroups($group); + $flattenedGroups = $this->sortGroups($flattenedGroups); + $dataTransferObjects = []; + $relevantYears = $this->censusDateProvider->getRelevantDateRange(); + foreach ($flattenedGroups as $flattenedGroup) { + $dataTransferObjects[] = CensusMapper::mapToCensusTable($flattenedGroup, $this->censusGroupRepository->findBy(['group_id' => $flattenedGroup->getId()]), $relevantYears, $censusRequestData); + } + return [ + 'years' => $relevantYears, + 'data' => $dataTransferObjects, + ]; + } + + public function getDevelopmentData(Group $group, CensusRequestData $censusRequestData) + { + $relevantGroups = $this->getRelevantGroups($group); + $relevantGroups = $this->filterGroups($relevantGroups, $censusRequestData); + $relevantGroups = $this->sortGroups($relevantGroups); + + $absolute = []; + $relative = []; + $relevantYears = $this->censusDateProvider->getRelevantDateRange(); + foreach ($relevantGroups as $relevantGroup) { + $data = $this->censusGroupRepository->findBy(['group_id' => $relevantGroup->getId()]); + if (!sizeof($data) == 0) { + $dto = CensusMapper::mapToLineChart($relevantGroup, $data, $relevantYears, $censusRequestData); + $absolute[] = $dto->getAbsolute()[0]; + $relative[] = $dto->getRelative()[0]; + } + } + $return = new DevelopmentWidgetDTO(); + $return->setYears($relevantYears); + $return->setAbsolute($absolute); + $return->setRelative($relative); + return $return; + } + + public function getMembersData(Group $group, CensusRequestData $censusRequestData): array + { + $relevantGroups = $this->getRelevantGroups($group); + $relevantGroups = $this->filterGroups($relevantGroups, $censusRequestData); + $relevantGroups = $this->sortGroups($relevantGroups); + + $rawResults = []; + foreach ($relevantGroups as $relevantGroup) { + $data = $this->censusGroupRepository->findBy(['group_id' => $relevantGroup->getId(), 'year' => $this->censusDateProvider->getLatestYear()]); + if (!sizeof($data) == 0) { + CensusMapper::filterCensusGroup($data[0], $censusRequestData); + $biber = $data[0]->getBiberMCount() + $data[0]->getBiberFCount(); + $woelfe = $data[0]->getWoelfeMCount() + $data[0]->getWoelfeFCount(); + $pfadi = $data[0]->getPfadisMCount() + $data[0]->getPfadisFCount(); + $pio = $data[0]->getPiosMCount() + $data[0]->getPiosFCount(); + $rover = $data[0]->getRoverMCount() + $data[0]->getRoverFCount(); + $pta = $data[0]->getPtaMCount() + $data[0]->getPtaFCount(); + $leaders = $data[0]->getLeiterMCount() + $data[0]->getLeiterFCount(); + $rawResults[0][] = new StackedBarElementDTO($biber, $data[0]->getName(), '#EEE09F'); + $rawResults[1][] = new StackedBarElementDTO($woelfe, $data[0]->getName(), '#3BB5DC'); + $rawResults[2][] = new StackedBarElementDTO($pfadi, $data[0]->getName(), '#9A7A54'); + $rawResults[4][] = new StackedBarElementDTO($rover, $data[0]->getName(), '#1DA650'); + $rawResults[3][] = new StackedBarElementDTO($pio, $data[0]->getName(), '#DD1F19'); + $rawResults[5][] = new StackedBarElementDTO($pta, $data[0]->getName(), '#d9b826'); + $rawResults[6][] = new StackedBarElementDTO($leaders, $data[0]->getName(), '#005716'); + } + } + $return = []; + foreach ($rawResults as $rawResult) { + $dto = new MembersWidgetDTO(); + $dto->setData($rawResult); + $return[] = $dto; + } + return ['data' => $return, 'year' => $this->censusDateProvider->getLatestYear()]; + } + + public function getTreemapData(Group $group, CensusRequestData $censusRequestData) + { + $relevantGroups = $this->getRelevantGroups($group); + $relevantGroups = $this->filterGroups($relevantGroups, $censusRequestData); + + $return = []; + foreach ($relevantGroups as $relevantGroup) { + $data = $this->censusGroupRepository->findBy(['group_id' => $relevantGroup->getId(), 'year' => $this->censusDateProvider->getLatestYear()]); + if (!sizeof($data) == 0) { + CensusMapper::filterCensusGroup($data[0], $censusRequestData); + $dto = new TreemapWidgetDTO(); + $dto->setName($relevantGroup->getName()); + $parentName = $relevantGroup->getParentGroup()->getName(); + $dto->setRegion($parentName); + $dto->setValue($data[0]->getCalculatedTotal()); + $dto->setColor(CensusMapper::getLightColorForId($relevantGroup->getParentGroup()->getId())); + $return[] = $dto; + } + } + return ['data' => $return, 'year' => $this->censusDateProvider->getLatestYear()]; + } + + /** + * Filter out groups based on the Frontend Table filter + * @param array $statisticGroups + * @param CensusRequestData $censusRequestData + * @return array + */ + private function filterGroups(array $statisticGroups, CensusRequestData $censusRequestData) + { + // For faster lookups we swap array index with value so that array goes from [1 => 23, 2 => 352] to [23 => null, 352 => null] + if (is_null($censusRequestData->getGroups())) { + return $statisticGroups; + } + $groupIdsToFilterOut = array_flip($censusRequestData->getGroups()); + $filteredGroups = array_filter($statisticGroups, function (StatisticGroup $group) use ($groupIdsToFilterOut) { + return !isset($groupIdsToFilterOut[$group->getId()]); + }); + // Ensure that they are sequential. + return array_values($filteredGroups); + } +} diff --git a/src/Service/DataProvider/CensusFilterDataProvider.php b/src/Service/DataProvider/CensusFilterDataProvider.php new file mode 100644 index 0000000..a495552 --- /dev/null +++ b/src/Service/DataProvider/CensusFilterDataProvider.php @@ -0,0 +1,45 @@ +groupSettingsRepository = $groupSettingsRepository; + } + + public function getFilterData(Group $group) + { + $groupSettings = $this->groupSettingsRepository->find($group->getId()); + return $this->mapGroupSettingsToCensusFilter($groupSettings); + } + + private function mapGroupSettingsToCensusFilter(GroupSettings $groupSettings): CensusFilterDTO + { + $filterData = new CensusFilterDTO(); + $filterData->setFilterFemales(is_null($groupSettings->getCensusFilterFemales()) ? true : $groupSettings->getCensusFilterFemales()); + $filterData->setFilterMales(is_null($groupSettings->getCensusFilterMales()) ? true : $groupSettings->getCensusFilterMales()); + $filterData->setRoles($groupSettings->getCensusRoles() ?? []); + $filterData->setGroups($groupSettings->getCensusGroups() ?? []); + return $filterData; + } + + public function setFilterData(Group $group, CensusRequestData $censusRequestData): CensusFilterDTO + { + $groupSettings = $this->groupSettingsRepository->find($group->getId()); + $groupSettings->setCensusGroups($censusRequestData->getGroups()); + $groupSettings->setCensusRoles($censusRequestData->getRoles()); + $groupSettings->setCensusFilterFemales($censusRequestData->isFilterFemales()); + $groupSettings->setCensusFilterMales($censusRequestData->isFilterMales()); + $this->groupSettingsRepository->flush(); + return $this->mapGroupSettingsToCensusFilter($groupSettings); + } +} diff --git a/src/Service/DataProvider/FilterDataProvider.php b/src/Service/DataProvider/FilterDataProvider.php index 6cdbc6f..9c4a72e 100644 --- a/src/Service/DataProvider/FilterDataProvider.php +++ b/src/Service/DataProvider/FilterDataProvider.php @@ -31,6 +31,12 @@ public function __construct( $this->widgetDateRepository = $widgetDateRepository; } + public function getGroupTypes(Group $group, string $locale) + { + $groupTypes = $this->groupTypeRepository->findGroupTypesForParentGroup($group->getId()); + return FilterDataMapper::createGroupTypes($groupTypes, $locale); + } + /*** * @param Group $group * @param string $locale diff --git a/src/Service/DataProvider/GeoLocationDateDataProvider.php b/src/Service/DataProvider/GeoLocationDateDataProvider.php index 26efc97..650ef1d 100644 --- a/src/Service/DataProvider/GeoLocationDateDataProvider.php +++ b/src/Service/DataProvider/GeoLocationDateDataProvider.php @@ -37,7 +37,10 @@ public function __construct( public function getData(Group $group, string $date, array $subGroupTypes, array $peopleTypes): array { $result = []; - + $groupMeetingPoints = $this->geoLocationRepository->findAllMeetingPointsForDate($date, $group->getId()); + foreach ($groupMeetingPoints as $groupMeetingPoint) { + $result[] = $this->mapGeoLocation($groupMeetingPoint, false); + } foreach ($subGroupTypes as $groupType) { $geoLocations = $this->geoLocationRepository->findAllForDateAndGroupType( $date, @@ -46,6 +49,7 @@ public function getData(Group $group, string $date, array $subGroupTypes, array $peopleTypes ); + $leaders = false; if ($peopleTypes === [ 'leaders' ]) { $leaders = true; diff --git a/src/Service/DataProvider/RoleOverviewDateRangeDataProvider.php b/src/Service/DataProvider/RoleOverviewDateRangeDataProvider.php new file mode 100644 index 0000000..fc0de24 --- /dev/null +++ b/src/Service/DataProvider/RoleOverviewDateRangeDataProvider.php @@ -0,0 +1,61 @@ +groupRepository = $groupRepository; + $this->personRoleRepository = $personRoleRepository; + parent::__construct( + $groupRepository, + $groupTypeRepository, + $translator + ); + } + + public function getData(Group $group, string $from, string $to) + { + $dto = RoleOverviewMapper::createRoleOverviewDTO($group); + + $aggregatedPersonRoles = $this->personRoleRepository->findByGroupInTimeframe($group, $from, $to); + /** @var RoleOccupationWrapper[] $roleOccupationWrappers */ + $roleOccupationWrappers = []; + foreach ($aggregatedPersonRoles as $aggregatedPersonRole) { + $wrapperExistsAt = -1; + foreach ($roleOccupationWrappers as $key => $roleOccupationWrapper) { + if ($roleOccupationWrapper->getRoleType() === $aggregatedPersonRole->getRole()->getRoleType()) { + $wrapperExistsAt = $key; + } + } + if ($wrapperExistsAt !== -1) { + $roleOccupationWrappers[$wrapperExistsAt]->addData(RoleOverviewMapper::createRoleOccupation($aggregatedPersonRole, $from, $to)); + } else { + $wrapper = RoleOverviewMapper::createRoleOccupationWrapper($aggregatedPersonRole->getRole(), $this->translator->getLocale()); + $wrapper->addData(RoleOverviewMapper::createRoleOccupation($aggregatedPersonRole, $from, $to)); + $roleOccupationWrappers[] = $wrapper; + } + } + $dto->setData($roleOccupationWrappers); + return $dto; + } +} diff --git a/src/Service/DataProvider/WidgetDataProvider.php b/src/Service/DataProvider/WidgetDataProvider.php index ffea5ab..5c56f6e 100644 --- a/src/Service/DataProvider/WidgetDataProvider.php +++ b/src/Service/DataProvider/WidgetDataProvider.php @@ -37,8 +37,8 @@ class WidgetDataProvider 'Group::Pio' => '#DD1F19', 'Group::AbteilungsRover' => '#1DA650', 'Group::Pta' => '#d9b826', - 'Group::Abteilung' => '#929292', - 'leaders' => '#929292' + 'Group::Abteilung' => '#005716', + 'leaders' => '#005716' ]; /** @var string[] */ @@ -51,6 +51,16 @@ class WidgetDataProvider 'Group::Pta' ]; + public const CENSUS_ROLES = [ + 'biber', + 'woelfe', + 'pfadis', + 'rover', + 'pio', + 'pta', + 'leiter' + ]; + /** @var string */ public const PEOPLE_TYPE_LEADERS = 'leaders'; /** @var string */ diff --git a/src/Service/Gamification/LoginService.php b/src/Service/Gamification/LoginService.php new file mode 100644 index 0000000..2040d5e --- /dev/null +++ b/src/Service/Gamification/LoginService.php @@ -0,0 +1,86 @@ +personRepository = $personRepository; + $this->loginRepository = $loginRepository; + $this->groupRepository = $groupRepository; + $this->permissionRepository = $permissionRepository; + } + + public function logByUserDTOForLogin(PbsUserDTO $userDTO): Login + { + $login = new Login(); + + $activeGroupDTO = $userDTO->getGroups()[0]; // Group 0 is the active group on Login. + $activeGroup = $this->groupRepository->find($activeGroupDTO->getId()); + $user = $this->personRepository->find($userDTO->getId()); + if ($user === null) { + throwException("user couldn't be found."); + } + if (sizeof($userDTO->getRoles()) !== 1) { + throwException("Invalid amount of roles."); + } + $role = $this->permissionRepository->findHighestById($activeGroup, $user->getId()); + $roleKey = 'ROLE_USER'; + if (!is_null($role)) { + $roleKey = $role->getPermissionType()->getKey(); + } + + $login->setPerson($user); + $login->setGroup($activeGroup); + $login->setDate(new \DateTime('now', new \DateTimeZone('Europe/Zurich'))); + $login->setIsGroupChange(false); + $login->setRole($roleKey); + + $this->loginRepository->add($login); + + return $login; + } + + public function logByPersonAndGroup(PbsUserDTO $userDTO, Group $group) + { + $login = new Login(); + $person = $this->personRepository->find($userDTO->getId()); + $role = $this->permissionRepository->findHighestById($group, $person->getId()); + $roleKey = 'ROLE_USER'; + if (!is_null($role)) { + $roleKey = $role->getPermissionType()->getKey(); + } + + $login->setPerson($person); + $login->setGroup($group); + $login->setDate(new \DateTime('now', new \DateTimeZone('Europe/Zurich'))); + $login->setIsGroupChange(true); + $login->setRole($roleKey); + + $this->loginRepository->add($login); + + return $login; + } +} diff --git a/src/Service/Gamification/PersonGamificationService.php b/src/Service/Gamification/PersonGamificationService.php new file mode 100644 index 0000000..b24cfe4 --- /dev/null +++ b/src/Service/Gamification/PersonGamificationService.php @@ -0,0 +1,335 @@ +loginRepository = $loginRepository; + $this->levelRepository = $levelRepository; + $this->personRepository = $personRepository; + $this->personGoalRepository = $personGoalRepository; + $this->em = $em; + $this->levelUpLogRepository = $levelUpLogRepository; + $this->gamificationQuapEventRepository = $gamificationQuapEventRepository; + } + + public function reset(PbsUserDTO $pbsUserDTO) + { + $person = $this->personRepository->find($pbsUserDTO->getId()); + $pgp = $this->getPersonGamification($person); + $this->personGoalRepository->remove($pgp); + $events = $this->gamificationQuapEventRepository->findBy(['person' => $person]); + foreach ($events as $event) { + $this->gamificationQuapEventRepository->remove($event); + } + } + + public function getPersonGamification(Person $person): GamificationPersonProfile + { + $gamificationProfile = $person->getGamification(); + if (is_null($gamificationProfile)) { + $gamificationProfile = new GamificationPersonProfile(); + $gamificationProfile->setLevel($this->levelRepository->findOneBy(['key' => 0])); + $gamificationProfile->setPerson($person); + $gamificationProfile->setAccessGrantedCount(0); + $gamificationProfile->setElFilledOut(false); + $gamificationProfile->setElImproved(false); + $gamificationProfile->setElIrrelevant(false); + $gamificationProfile->setElRevised(false); + $gamificationProfile->setHasSharedEl(false); + $gamificationProfile->setHasUsedCardLayer(false); + $gamificationProfile->setHasUsedDatafilter(false); + $gamificationProfile->setHasUsedTimefilter(false); + $gamificationProfile->setBetaStatus(false); + $this->personGoalRepository->add($gamificationProfile); + } + return $gamificationProfile; + } + + public function genericGoalProgress(PbsUserDTO $pbsUserDTO, string $type) + { + $person = $this->personRepository->find($pbsUserDTO->getId()); + $pgp = $this->getPersonGamification($person); + + switch ($type) { + case 'card': + $pgp->setHasUsedCardLayer(true); + break; + case 'time': + $pgp->setHasUsedTimefilter(true); + break; + case 'data': + $pgp->setHasUsedDatafilter(true); + break; + case 'shareEL': + $pgp->setHasSharedEl(true); + break; + case 'invite': + $newCount = $pgp->getAccessGrantedCount() + 1; + $pgp->setAccessGrantedCount($newCount); + break; + case 'revised': + if ($pgp->getLevel()->getKey() >= 1) { + $pgp->setElRevised(true); + } + break; + case 'improvement': + if ($pgp->getLevel()->getKey() >= 2) { + $pgp->setElImproved(true); + } + break; + case 'irrelevant': + if ($pgp->getLevel()->getKey() >= 1) { + $pgp->setElIrrelevant(true); + } + break; + case 'filledOut': + if ($this->getElFilledOut($person) === 7) { + $pgp->setElFilledOut(true); + } + break; + default: + throw new \Exception('typo in type'); + break; + } + + $this->checkLevelUp($pgp); + + $this->em->persist($pgp); + $this->em->flush(); + } + + public function checkLevelUp(GamificationPersonProfile $person) + { + $currentLevel = $person->getLevel(); + $nextLevel = $this->levelRepository->findNextLevel($currentLevel); + + if (count($nextLevel) === 0) { + return $person; + } + $nextLevel = $nextLevel[0]; + $levelUp = false; + if ($currentLevel->getKey() === 0) { + if ($person->getHasUsedCardLayer() && ($person->getHasUsedDatafilter() || $person->getHasUsedTimefilter() || $person->getHasSharedEl())) { + $levelUp = true; + } + } + if ($currentLevel->getKey() === 1) { + $completedCounter = 0; + if ($person->getElFilledOut()) { + if ($person->getAccessGrantedCount() >= 1) { + $completedCounter++; + } + if ($person->getElIrrelevant()) { + $completedCounter++; + } + if ($person->getElRevised()) { + $completedCounter++; + } + if ($completedCounter >= 2) { + $levelUp = true; + } + } + } + if ($currentLevel->getKey() === 2) { + if ($person->getElImproved() && ($this->checkLoginGoal($person) || $person->getAccessGrantedCount() >= 3)) { + $levelUp = true; + } + } + + if ($levelUp) { + $person->setLevel($nextLevel); + $log = new LevelUpLog(); + $log->setPerson($person->getPerson()); + $log->setLevel($nextLevel); + $log->setDate(new \DateTimeImmutable()); + $log->setDisplayed(false); + $this->levelUpLogRepository->add($log); + } + $this->em->persist($person); + $this->em->flush(); + + return $person; + } + + public function checkLoginGoal(GamificationPersonProfile $profile): bool + { + return count($profile->getPerson()->getLogins()) >= 4; + } + + public function getPersonGamificationDTO(PbsUserDTO $pbsUserDTO, string $locale): PersonGamificationDTO + { + $levels = $this->levelRepository->findBy(['type' => Level::USER]); + /** @var Person $person */ + $person = $this->personRepository->find($pbsUserDTO->getId()); + $personGamification = $this->getPersonGamification($person); + $personGamification = $this->checkLevelUp($personGamification); + + $personGamificationDTO = GamificationPersonProfileMapper::createFromEntity($personGamification, $locale); + + $levelUp = $this->levelUpLogRepository->findOneBy(['person' => $person, 'displayed' => false]); + if (!is_null($levelUp)) { + $levelUp->setDisplayed(true); + $this->levelUpLogRepository->add($levelUp); + $personGamificationDTO->setLevelUp(true); + } + + if (count($levels) === 0) { + throw new \Exception('no levels found?!'); + } + $levelDtos = []; + foreach ($levels as $level) { + $levelDto = GamificationLevelMapper::createFromEntity($level, $locale); + if ($personGamification->getLevel()->getNextKey() === $level->getKey()) { + $levelDto->setActive(true); + } + $goalDTOs = []; + $goals = $level->getGoals(); + foreach ($goals as $goal) { + switch ($goal->getKey()) { + case 'FIRST_LOGIN': + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, true, 0); + break; + case 'CARD_LAYERS': + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getHasUsedCardLayer(), 0); + break; + case 'DATAFILTER': + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getHasUsedDatafilter(), 0); + break; + case 'TIMEFILTER': + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getHasUsedTimefilter(), 0); + break; + case 'SHARE_WITH_PARENTS': + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getHasSharedEl(), 0); + break; + case 'EL_FILL_OUT': // TODO add check + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getElFilledOut(), $this->getElFilledOut($person)); + break; + case 'SHARE_1': + $completed = $personGamification->getAccessGrantedCount() >= 1; + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $completed, $personGamification->getAccessGrantedCount()); + break; + case 'EL_IRRELEVANT': // TODO add check + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getElIrrelevant(), 0); + break; + case 'EL_CHANGE':// TODO add check + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getElRevised(), 0); + break; + case 'EL_IMPROVE':// TODO add check + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $personGamification->getElImproved(), 0); + break; + case 'LOGIN_FOUR_A_YEAR': // TODO persist this in $pgp + $logins = $person->getLogins(); + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $this->checkLoginGoal($personGamification), count($logins)); + break; + case 'SHARE_THREE': + $completed = $personGamification->getAccessGrantedCount() >= 3; + $goalDTOs[] = GamificationGoalMapper::createFromEntity($goal, $locale, $completed, $personGamification->getAccessGrantedCount()); + break; + default: + throw new \Exception('Couldnt find goal'); + break; + } + } + if (count($goalDTOs) !== 0) { + $levelDto->setGoals(array_reverse($goalDTOs)); + $levelDtos[] = $levelDto; + } + } + + $personGamificationDTO->setLevels($levelDtos); + return $personGamificationDTO; + } + + /** + * Every questionnaire has 7 aspects which can be answered, if all 7 of them have been answered the goal + * is completed + */ + private function getElFilledOut(Person $person): int + { + $counters = ['Questionnaire::Group::Default' => 0, 'Questionnaire::Group::Canton' => 0]; + $localIdAndQuestionnaireId = $this->gamificationQuapEventRepository->getUniquieIds($person); + foreach ($localIdAndQuestionnaireId as $item) { + $counters[$item['type']]++; + } + return max($counters['Questionnaire::Group::Canton'], $counters['Questionnaire::Group::Default']); + } + + public function logEvent(array $changedIds, AggregatedQuap $aggregatedQuap, PbsUserDTO $pbsUserDTO) + { + $person = $this->personRepository->find($pbsUserDTO->getId()); + if ($this->getPersonGamification($person)->getLevel()->getKey() >= 1) { + foreach ($changedIds as $id) { + $eventLog = new GamificationQuapEvent(); + $eventLog->setQuestionnaire($aggregatedQuap->getQuestionnaire()); + $eventLog->setDate(new \DateTimeImmutable()); + $eventLog->setGroup($aggregatedQuap->getGroup()); + $eventLog->setPerson($this->personRepository->find($pbsUserDTO->getId())); + $eventLog->setLocalChangeIndex($id); + $this->gamificationQuapEventRepository->add($eventLog); + } + } + $this->genericGoalProgress($pbsUserDTO, 'filledOut'); + } + + public function getBetaAccess(PbsUserDTO $pbsUserDTO): bool + { + /** @var Person $person */ + $person = $this->personRepository->find($pbsUserDTO->getId()); + $profile = $person->getGamification(); + if ($profile->getLevel()->getKey() === 3) { + /** request logic */ + $profile->setBetaStatus(true); + $this->personGoalRepository->add($profile); + return true; + } + return false; + } +} diff --git a/src/Service/Gamification/QuapGamificationService.php b/src/Service/Gamification/QuapGamificationService.php new file mode 100644 index 0000000..3bcd51c --- /dev/null +++ b/src/Service/Gamification/QuapGamificationService.php @@ -0,0 +1,85 @@ +personGamificationService = $personGamificationService; + $this->aggregatedQuapRepository = $aggregatedQuapRepository; + $this->personRepository = $personRepository; + $this->gamificationQuapEventRepository = $gamificationQuapEventRepository; + } + + /** + * Answered State: Erfüllt = 1, Meistens Erfüllt = 2, ... , Nicht Relevant = 5 + * @param array $newAnswers + * @param Group $group + * @return string + */ + public function processQuapEvent(array $newAnswers, Group $group, PbsUserDTO $pbsUserDTO) + { + $aggregatedQuap = $this->aggregatedQuapRepository->findCurrentForGroup($group->getId()); + $oldAnswers = $aggregatedQuap->getAnswers(); + + $changedQuestionnaires = []; + $irrelevant = false; + $improvement = false; + + for ($questionnaireIndex = 0; $questionnaireIndex < count($oldAnswers); $questionnaireIndex++) { + for ($i = 0; $i < count($oldAnswers[$questionnaireIndex]); $i++) { + if ($oldAnswers[$questionnaireIndex][$i] !== $newAnswers[$questionnaireIndex][$i]) { + $changedQuestionnaires[] = $questionnaireIndex; + if ($newAnswers[$questionnaireIndex][$i] === 5) { + $irrelevant = true; + } + if ($newAnswers[$questionnaireIndex][$i] !== 4 && $newAnswers[$questionnaireIndex][$i] < $oldAnswers[$questionnaireIndex][$i]) { + $improvement = true; + } + //$textchanges .= 'Question ' . $questionnaireIndex . '.' . $i . ': ' . $oldAnswers[$questionnaireIndex][$i] . '/' . $newAnswers[$questionnaireIndex][$i] . '; '; + } + } + } + + foreach ($changedQuestionnaires as $index) { + if ($this->isQuestionnaireFullyAnswered($newAnswers[$index])) { + $this->personGamificationService->genericGoalProgress($pbsUserDTO, 'revised'); + } + } + if ($irrelevant) { + $this->personGamificationService->genericGoalProgress($pbsUserDTO, 'irrelevant'); + } + if ($improvement) { + $this->personGamificationService->genericGoalProgress($pbsUserDTO, 'improvement'); + } + + $this->personGamificationService->logEvent($changedQuestionnaires, $aggregatedQuap, $pbsUserDTO); + } + + private function isQuestionnaireFullyAnswered($questionnaire) + { + foreach ($questionnaire as $answer) { + if ($answer === 4 || $answer === 0) { + return false; + } + } + return true; + } +} diff --git a/src/Service/GroupStructureAPIService.php b/src/Service/GroupStructureAPIService.php new file mode 100644 index 0000000..6d5fec0 --- /dev/null +++ b/src/Service/GroupStructureAPIService.php @@ -0,0 +1,50 @@ +guzzleWrapper = $guzzleWrapper; + $this->url = $url; + $this->apiToken = $apiToken; + } + + + /** + * Fetch a group from the Group Structure API. + * This can return any group regardless of healthcheck opt-out or any other factor. + * @param int $groupId + * @return Http\CurlResponse + */ + public function getGroup(int $groupId): Http\CurlResponse + { + $endpoint = $this->url . '/de/groups/' . $groupId . '.json?token=' . $this->apiToken; + return $this->guzzleWrapper->getJson($endpoint, null, []); + } + + /** + * @return string + */ + public function getUrl(): string + { + return $this->url; + } +} diff --git a/src/Service/QualificationProcessor.php b/src/Service/QualificationProcessor.php index 1159063..501e329 100644 --- a/src/Service/QualificationProcessor.php +++ b/src/Service/QualificationProcessor.php @@ -194,6 +194,20 @@ private function removeUnneeded(array $qualifications): array { /** @var AggregatedLeaderOverviewQualification[] $unique */ $temp = []; + // Sort Qualifications by expiration date descending, and null goes first. + // This is so the newest qualification is sent to the frontend and old ones get filtered out. + usort($qualifications, function (AggregatedLeaderOverviewQualification $a, AggregatedLeaderOverviewQualification $b) { + if (is_null($a->getExpiresAt())) { + return -1; + } + if (is_null($b->getExpiresAt())) { + return 1; + } + if ($a->getExpiresAt()->getTimestamp() === $b->getExpiresAt()->getTimestamp()) { + return 0; + } + return ($a->getExpiresAt()->getTimestamp() < $b->getExpiresAt()->getTimestamp()) ? 1 : -1; + }); /** @var AggregatedLeaderOverviewQualification $qualification */ foreach ($qualifications as $qualification) { $exists = false; diff --git a/src/Service/Sentry.php b/src/Service/Sentry.php index 63596df..3df1860 100644 --- a/src/Service/Sentry.php +++ b/src/Service/Sentry.php @@ -2,10 +2,23 @@ namespace App\Service; +use App\DTO\Model\PbsUserDTO; +use Sentry\UserDataBag; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Security; class Sentry { + private $security; + + public function __construct(Security $security) + { + $this->security = $security; + } + /** + * Filter out acceptable Excepitons (HTTP 401) and sensitive data. + * @return callable + */ public function sentryFilter(): callable { return function (\Sentry\Event $event, ?\Sentry\EventHint $hint): ?\Sentry\Event { @@ -14,6 +27,16 @@ public function sentryFilter(): callable return null; } + $user = $this->security->getUser(); + if ($user instanceof PbsUserDTO) { + $userBag = new UserDataBag($user->getId(), null, null, null, null); + $userBag->setMetadata('Nickname', $user->getNickName()); + $event->setUser($userBag); + } elseif (!is_null($user)) { + $userBag = new UserDataBag(null, null, null, $user->getUsername(), null); + $userBag->setMetadata('Is PbsUserDTO', false); + $event->setUser($userBag); + } return $event; }; } diff --git a/symfony.lock b/symfony.lock index d165baa..1fa8573 100644 --- a/symfony.lock +++ b/symfony.lock @@ -102,6 +102,9 @@ "doctrine/sql-formatter": { "version": "1.1.0" }, + "egulias/email-validator": { + "version": "2.1.25" + }, "friendsofphp/proxy-manager-lts": { "version": "v1.0.16" }, @@ -339,6 +342,18 @@ "symfony/inflector": { "version": "v5.1.2" }, + "symfony/mailer": { + "version": "5.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.3", + "ref": "df66ee1f226c46f01e85c29c2f7acce0596ba35a" + }, + "files": [ + "config/packages/mailer.yaml" + ] + }, "symfony/maker-bundle": { "version": "1.0", "recipe": { @@ -348,6 +363,9 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/mime": { + "version": "v5.1.11" + }, "symfony/options-resolver": { "version": "v5.4.21" },