From a4aeee4961119ef523d837bdbe983cbe92dc3b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Mon, 9 Dec 2019 17:36:04 +0100 Subject: [PATCH 01/31] Migration to slim v4 and refactoring --- .env.dist | 1 - .gitignore | 4 +- .gitlab-ci.yml | 74 +- CHANGELOG.md | 24 - Dockerfile | 4 +- LICENSE | 9 +- Makefile | 44 +- README.md | 123 +- VERSION | 2 +- anis-server.yaml | 1009 ----------- app/constants.php | 18 + app/dependencies.php | 113 ++ app/middlewares.php | 14 + app/routes.php | 29 + app/settings.php | 33 + cli-config.php | 12 +- composer.json | 29 +- composer.lock | 1508 +++++++++-------- conf-dev/Dockerfile | 7 +- conf-dev/anis_init.sh | 72 - conf-dev/{dev-init.sh => dev-meta.sh} | 9 +- conf-dev/generate_encryption_key.php | 2 - conf-dev/init-postgres.sh | 3 +- conf-dev/vhost.conf | 6 +- docker-compose.yml | 45 +- documentation/api/activate-account.md | 100 -- documentation/api/category-list.md | 93 - documentation/api/category.md | 122 -- documentation/api/change-password.md | 119 -- documentation/api/database-list.md | 194 --- documentation/api/database.md | 212 --- documentation/api/dataset-list.md | 278 --- documentation/api/dataset.md | 217 --- documentation/api/family-list.md | 143 -- documentation/api/family.md | 177 -- documentation/api/file-list.md | 168 -- documentation/api/file.md | 184 -- documentation/api/group-list.md | 89 - documentation/api/group.md | 122 -- documentation/api/login.md | 109 -- documentation/api/new-password.md | 81 - documentation/api/project-list.md | 179 -- documentation/api/project.md | 182 -- documentation/api/register.md | 100 -- documentation/api/root.md | 44 - documentation/api/tables-available.md | 60 - documentation/api/user-list.md | 150 -- documentation/api/user.md | 167 -- documentation/mcd/anis_v3_mcd.png | Bin 93829 -> 0 bytes documentation/mcd/anis_v3_mcd.txt | 27 - documentation/mcd/anis_v3_settings_mcd.png | Bin 7201 -> 0 bytes documentation/mcd/anis_v3_settings_mcd.txt | 5 - public/index.php | 67 +- src/Action/AbstractAction.php | 52 + src/Action/Admin/InstanceAction.php | 110 -- src/Action/Admin/InstanceListAction.php | 100 -- src/Action/Admin/MetamodelAction.php | 87 - src/Action/Admin/OptionAction.php | 91 - src/Action/Admin/OptionListAction.php | 90 - src/Action/Admin/SelectAction.php | 90 - src/Action/Admin/SelectListAction.php | 79 - src/Action/Admin/UserAction.php | 91 - src/Action/Admin/UserListAction.php | 49 - src/Action/{Meta => }/AttributeAction.php | 117 +- src/Action/AttributeListAction.php | 55 + src/Action/DatabaseAction.php | 100 ++ src/Action/DatabaseListAction.php | 87 + src/Action/DatasetAction.php | 122 ++ src/Action/DatasetListAction.php | 178 ++ src/Action/FamilyAction.php | 103 ++ src/Action/FamilyListAction.php | 100 ++ src/Action/Login/ActivateAccountAction.php | 168 -- src/Action/Login/ChangePasswordAction.php | 173 -- src/Action/Login/NewPasswordAction.php | 165 -- src/Action/Login/RegisterAction.php | 207 --- src/Action/Login/TokenAction.php | 182 -- src/Action/Meta/AttributeListAction.php | 105 -- src/Action/Meta/DatabaseAction.php | 153 -- src/Action/Meta/DatabaseListAction.php | 137 -- src/Action/Meta/DatasetAction.php | 173 -- src/Action/Meta/DatasetListAction.php | 240 --- src/Action/Meta/FamilyAction.php | 133 -- src/Action/Meta/FamilyListAction.php | 112 -- src/Action/Meta/FileAction.php | 93 - src/Action/Meta/FileListAction.php | 105 -- src/Action/Meta/FileProxyAction.php | 71 - src/Action/Meta/GroupAction.php | 92 - src/Action/Meta/GroupListAction.php | 81 - src/Action/Meta/OutputCategoryAction.php | 160 -- src/Action/Meta/OutputCategoryListAction.php | 99 -- src/Action/Meta/ProjectAction.php | 159 -- src/Action/Meta/ProjectListAction.php | 98 -- src/Action/Meta/TableListAction.php | 90 - src/Action/OutputCategoryAction.php | 112 ++ src/Action/OutputCategoryListAction.php | 87 + src/Action/ProjectAction.php | 109 ++ src/Action/ProjectListAction.php | 89 + src/Action/Root/OpenApiAction.php | 62 - src/Action/Root/RootAction.php | 59 - src/Action/RootAction.php | 41 + src/Action/Search/ServiceAction.php | 86 - src/Action/{Search => }/SearchAction.php | 268 +-- src/Action/TableListAction.php | 86 + src/Entity/Admin/Instance.php | 336 ---- src/Entity/Admin/SettingsOption.php | 112 -- src/Entity/Admin/SettingsSelect.php | 82 - src/Entity/Admin/User.php | 131 -- src/Entity/{Metamodel => }/Attribute.php | 12 +- src/Entity/{Metamodel => }/CriteriaFamily.php | 12 +- src/Entity/{Metamodel => }/Database.php | 12 +- src/Entity/{Metamodel => }/Dataset.php | 14 +- src/Entity/{Metamodel => }/DatasetFamily.php | 14 +- .../{Metamodel => }/DatasetPrivileges.php | 12 +- src/Entity/{Metamodel => }/File.php | 12 +- src/Entity/{Metamodel => }/Group.php | 12 +- src/Entity/{Metamodel => }/OutputCategory.php | 12 +- src/Entity/{Metamodel => }/OutputFamily.php | 12 +- src/Entity/{Metamodel => }/Project.php | 12 +- src/Entity/{Metamodel => }/User.php | 12 +- src/Handlers/LogErrorHandler.php | 52 + src/Middleware/AuthorizationMiddleware.php | 137 ++ src/Middleware/ContentTypeJsonMiddleware.php | 41 + src/Middleware/CorsMiddleware.php | 34 - src/Middleware/JsonBodyParserMiddleware.php | 50 + src/Middleware/TokenMiddleware.php | 70 - src/Utils/ActionTrait.php | 84 - src/Utils/AnisErrorHandler.php | 106 -- src/Utils/DBALConnectionFactory.php | 24 +- src/Utils/MetaEntityManagerFactory.php | 98 -- src/Utils/Operator/Between.php | 10 +- src/Utils/Operator/Equal.php | 10 +- src/Utils/Operator/GreaterThan.php | 12 +- src/Utils/Operator/GreaterThanEqual.php | 12 +- src/Utils/Operator/IOperator.php | 10 +- src/Utils/Operator/IOperatorFactory.php | 10 +- src/Utils/Operator/In.php | 10 +- src/Utils/Operator/JsonPostgres.php | 10 +- src/Utils/Operator/LessThan.php | 10 +- src/Utils/Operator/LessThanEqual.php | 10 +- src/Utils/Operator/Like.php | 10 +- src/Utils/Operator/NotEqual.php | 10 +- src/Utils/Operator/NotIn.php | 10 +- src/Utils/Operator/NotLike.php | 10 +- src/Utils/Operator/Operator.php | 10 +- src/Utils/Operator/OperatorException.php | 10 +- src/Utils/Operator/OperatorFactory.php | 10 +- src/Utils/Operator/OperatorNotNull.php | 10 +- src/Utils/Operator/OperatorNull.php | 22 +- src/Utils/SearchException.php | 10 +- src/dependencies.php | 269 --- src/middleware.php | 12 - src/routes.php | 65 - src/settings.php | 53 - tests/AbstractActionAdminTestCase.php | 47 - tests/AbstractActionMetaTestCase.php | 56 - tests/Action/AttributeActionTest.php | 250 +++ tests/Action/AttributeListActionTest.php | 165 ++ tests/Action/DatabaseActionTest.php | 122 ++ tests/Action/DatabaseListActionTest.php | 117 ++ tests/Action/DatasetActionTest.php | 196 +++ tests/Action/DatasetListActionTest.php | 234 +++ tests/Action/FamilyActionTest.php | 124 ++ tests/Action/FamilyListActionTest.php | 111 ++ .../Login/ActivateAccountActionTest.php | 86 - .../Action/Login/ChangePasswordActionTest.php | 121 -- tests/Action/Login/NewPasswordActionTest.php | 72 - tests/Action/Login/RegisterActionTest.php | 98 -- tests/Action/Login/TokenActionTest.php | 120 -- tests/Action/Meta/DatabaseActionTest.php | 158 -- tests/Action/Meta/DatabaseListActionTest.php | 138 -- tests/Action/Meta/DatasetActionTest.php | 180 -- tests/Action/Meta/DatasetListActionTest.php | 189 --- tests/Action/Meta/FamilyActionTest.php | 169 -- tests/Action/Meta/FamilyListActionTest.php | 236 --- tests/Action/Meta/FileActionTest.php | 138 -- tests/Action/Meta/FileListActionTest.php | 149 -- tests/Action/Meta/GroupActionTest.php | 113 -- tests/Action/Meta/GroupListActionTest.php | 77 - .../Action/Meta/OutputCategoryActionTest.php | 116 -- .../Meta/OutputCategoryListActionTest.php | 92 - tests/Action/Meta/ProjectActionTest.php | 159 -- tests/Action/Meta/ProjectListActionTest.php | 138 -- tests/Action/Meta/TableListActionTest.php | 49 - tests/Action/OutputCategoryActionTest.php | 146 ++ tests/Action/OutputCategoryListActionTest.php | 133 ++ tests/Action/ProjectActionTest.php | 156 ++ tests/Action/ProjectListActionTest.php | 146 ++ tests/Action/Root/RootActionTest.php | 24 - tests/Action/RootActionTest.php | 36 + tests/Action/TableListActionTest.php | 120 ++ tests/EntityManagerBuilder.php | 35 + tests/Handlers/LogErrorHandlerTest.php | 48 + .../ContentTypeJsonMiddlewareTest.php | 41 + tests/admin.yaml | 15 - tests/bootstrap.php | 15 +- tests/database.yaml | 121 -- 196 files changed, 5312 insertions(+), 14143 deletions(-) delete mode 100644 .env.dist delete mode 100644 CHANGELOG.md delete mode 100644 anis-server.yaml create mode 100644 app/constants.php create mode 100644 app/dependencies.php create mode 100644 app/middlewares.php create mode 100644 app/routes.php create mode 100644 app/settings.php delete mode 100755 conf-dev/anis_init.sh rename conf-dev/{dev-init.sh => dev-meta.sh} (79%) delete mode 100644 conf-dev/generate_encryption_key.php delete mode 100644 documentation/api/activate-account.md delete mode 100644 documentation/api/category-list.md delete mode 100644 documentation/api/category.md delete mode 100644 documentation/api/change-password.md delete mode 100644 documentation/api/database-list.md delete mode 100644 documentation/api/database.md delete mode 100644 documentation/api/dataset-list.md delete mode 100644 documentation/api/dataset.md delete mode 100644 documentation/api/family-list.md delete mode 100644 documentation/api/family.md delete mode 100644 documentation/api/file-list.md delete mode 100644 documentation/api/file.md delete mode 100644 documentation/api/group-list.md delete mode 100644 documentation/api/group.md delete mode 100644 documentation/api/login.md delete mode 100644 documentation/api/new-password.md delete mode 100644 documentation/api/project-list.md delete mode 100644 documentation/api/project.md delete mode 100644 documentation/api/register.md delete mode 100644 documentation/api/root.md delete mode 100644 documentation/api/tables-available.md delete mode 100644 documentation/api/user-list.md delete mode 100644 documentation/api/user.md delete mode 100644 documentation/mcd/anis_v3_mcd.png delete mode 100644 documentation/mcd/anis_v3_mcd.txt delete mode 100644 documentation/mcd/anis_v3_settings_mcd.png delete mode 100644 documentation/mcd/anis_v3_settings_mcd.txt create mode 100644 src/Action/AbstractAction.php delete mode 100644 src/Action/Admin/InstanceAction.php delete mode 100644 src/Action/Admin/InstanceListAction.php delete mode 100644 src/Action/Admin/MetamodelAction.php delete mode 100644 src/Action/Admin/OptionAction.php delete mode 100644 src/Action/Admin/OptionListAction.php delete mode 100644 src/Action/Admin/SelectAction.php delete mode 100644 src/Action/Admin/SelectListAction.php delete mode 100644 src/Action/Admin/UserAction.php delete mode 100644 src/Action/Admin/UserListAction.php rename src/Action/{Meta => }/AttributeAction.php (50%) create mode 100644 src/Action/AttributeListAction.php create mode 100644 src/Action/DatabaseAction.php create mode 100644 src/Action/DatabaseListAction.php create mode 100644 src/Action/DatasetAction.php create mode 100644 src/Action/DatasetListAction.php create mode 100644 src/Action/FamilyAction.php create mode 100644 src/Action/FamilyListAction.php delete mode 100644 src/Action/Login/ActivateAccountAction.php delete mode 100644 src/Action/Login/ChangePasswordAction.php delete mode 100644 src/Action/Login/NewPasswordAction.php delete mode 100644 src/Action/Login/RegisterAction.php delete mode 100644 src/Action/Login/TokenAction.php delete mode 100644 src/Action/Meta/AttributeListAction.php delete mode 100644 src/Action/Meta/DatabaseAction.php delete mode 100644 src/Action/Meta/DatabaseListAction.php delete mode 100644 src/Action/Meta/DatasetAction.php delete mode 100644 src/Action/Meta/DatasetListAction.php delete mode 100644 src/Action/Meta/FamilyAction.php delete mode 100644 src/Action/Meta/FamilyListAction.php delete mode 100644 src/Action/Meta/FileAction.php delete mode 100644 src/Action/Meta/FileListAction.php delete mode 100644 src/Action/Meta/FileProxyAction.php delete mode 100644 src/Action/Meta/GroupAction.php delete mode 100644 src/Action/Meta/GroupListAction.php delete mode 100644 src/Action/Meta/OutputCategoryAction.php delete mode 100644 src/Action/Meta/OutputCategoryListAction.php delete mode 100644 src/Action/Meta/ProjectAction.php delete mode 100644 src/Action/Meta/ProjectListAction.php delete mode 100644 src/Action/Meta/TableListAction.php create mode 100644 src/Action/OutputCategoryAction.php create mode 100644 src/Action/OutputCategoryListAction.php create mode 100644 src/Action/ProjectAction.php create mode 100644 src/Action/ProjectListAction.php delete mode 100644 src/Action/Root/OpenApiAction.php delete mode 100644 src/Action/Root/RootAction.php create mode 100644 src/Action/RootAction.php delete mode 100644 src/Action/Search/ServiceAction.php rename src/Action/{Search => }/SearchAction.php (53%) create mode 100644 src/Action/TableListAction.php delete mode 100644 src/Entity/Admin/Instance.php delete mode 100644 src/Entity/Admin/SettingsOption.php delete mode 100644 src/Entity/Admin/SettingsSelect.php delete mode 100644 src/Entity/Admin/User.php rename src/Entity/{Metamodel => }/Attribute.php (98%) rename src/Entity/{Metamodel => }/CriteriaFamily.php (87%) rename src/Entity/{Metamodel => }/Database.php (93%) rename src/Entity/{Metamodel => }/Dataset.php (95%) rename src/Entity/{Metamodel => }/DatasetFamily.php (87%) rename src/Entity/{Metamodel => }/DatasetPrivileges.php (86%) rename src/Entity/{Metamodel => }/File.php (92%) rename src/Entity/{Metamodel => }/Group.php (86%) rename src/Entity/{Metamodel => }/OutputCategory.php (90%) rename src/Entity/{Metamodel => }/OutputFamily.php (88%) rename src/Entity/{Metamodel => }/Project.php (92%) rename src/Entity/{Metamodel => }/User.php (92%) create mode 100644 src/Handlers/LogErrorHandler.php create mode 100644 src/Middleware/AuthorizationMiddleware.php create mode 100644 src/Middleware/ContentTypeJsonMiddleware.php delete mode 100644 src/Middleware/CorsMiddleware.php create mode 100644 src/Middleware/JsonBodyParserMiddleware.php delete mode 100644 src/Middleware/TokenMiddleware.php delete mode 100644 src/Utils/ActionTrait.php delete mode 100644 src/Utils/AnisErrorHandler.php delete mode 100644 src/Utils/MetaEntityManagerFactory.php delete mode 100644 src/dependencies.php delete mode 100644 src/middleware.php delete mode 100644 src/routes.php delete mode 100644 src/settings.php delete mode 100644 tests/AbstractActionAdminTestCase.php delete mode 100644 tests/AbstractActionMetaTestCase.php create mode 100644 tests/Action/AttributeActionTest.php create mode 100644 tests/Action/AttributeListActionTest.php create mode 100644 tests/Action/DatabaseActionTest.php create mode 100644 tests/Action/DatabaseListActionTest.php create mode 100644 tests/Action/DatasetActionTest.php create mode 100644 tests/Action/DatasetListActionTest.php create mode 100644 tests/Action/FamilyActionTest.php create mode 100644 tests/Action/FamilyListActionTest.php delete mode 100644 tests/Action/Login/ActivateAccountActionTest.php delete mode 100644 tests/Action/Login/ChangePasswordActionTest.php delete mode 100644 tests/Action/Login/NewPasswordActionTest.php delete mode 100644 tests/Action/Login/RegisterActionTest.php delete mode 100644 tests/Action/Login/TokenActionTest.php delete mode 100644 tests/Action/Meta/DatabaseActionTest.php delete mode 100644 tests/Action/Meta/DatabaseListActionTest.php delete mode 100644 tests/Action/Meta/DatasetActionTest.php delete mode 100644 tests/Action/Meta/DatasetListActionTest.php delete mode 100644 tests/Action/Meta/FamilyActionTest.php delete mode 100644 tests/Action/Meta/FamilyListActionTest.php delete mode 100644 tests/Action/Meta/FileActionTest.php delete mode 100644 tests/Action/Meta/FileListActionTest.php delete mode 100644 tests/Action/Meta/GroupActionTest.php delete mode 100644 tests/Action/Meta/GroupListActionTest.php delete mode 100644 tests/Action/Meta/OutputCategoryActionTest.php delete mode 100644 tests/Action/Meta/OutputCategoryListActionTest.php delete mode 100644 tests/Action/Meta/ProjectActionTest.php delete mode 100644 tests/Action/Meta/ProjectListActionTest.php delete mode 100644 tests/Action/Meta/TableListActionTest.php create mode 100644 tests/Action/OutputCategoryActionTest.php create mode 100644 tests/Action/OutputCategoryListActionTest.php create mode 100644 tests/Action/ProjectActionTest.php create mode 100644 tests/Action/ProjectListActionTest.php delete mode 100644 tests/Action/Root/RootActionTest.php create mode 100644 tests/Action/RootActionTest.php create mode 100644 tests/Action/TableListActionTest.php create mode 100644 tests/EntityManagerBuilder.php create mode 100644 tests/Handlers/LogErrorHandlerTest.php create mode 100644 tests/Middleware/ContentTypeJsonMiddlewareTest.php delete mode 100644 tests/admin.yaml delete mode 100644 tests/database.yaml diff --git a/.env.dist b/.env.dist deleted file mode 100644 index 091eedb..0000000 --- a/.env.dist +++ /dev/null @@ -1 +0,0 @@ -AMQP_HOST=amqp_host \ No newline at end of file diff --git a/.gitignore b/.gitignore index 42f6b3e..b4ed884 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ nbproject/ .vscode/ .idea/ vendor/* +phpunit-coverage/ data/ build/* logs/* @@ -14,5 +15,4 @@ cache .project .settings .phpunit* -anis_v3.sqlite -.env \ No newline at end of file +.env diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 44888d0..9692fc1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,62 +1,14 @@ stages: - - metrics - - metrics-build - - metrics-deploy - - install - - test - - sonar - - build - - deploy + - install + - test + - sonar + - build variables: - VERSION: "3.0" + VERSION: "3.1" SONARQUBE_URL: https://sonarqube.lam.fr - METRICS_IMAGE: portus.lam.fr/anis/anis-server-metrics CONTAINER_IMAGE: portus.lam.fr/anis/anis-server -php-metrics: - image: jakzal/phpqa - stage: metrics - script: - - phpmetrics --report-html=var/php-metrics . - allow_failure: true - cache: - paths: - - var - policy: push - only: - refs: - - develop - -php-metrics-build: - image: docker:stable - stage: metrics-build - script: - - echo "FROM nginx" > var/Dockerfile - - echo "COPY php-metrics /usr/share/nginx/html" >> var/Dockerfile - - docker login -u fagneray -p $PORTUS_TOKEN portus.lam.fr - - docker pull $METRICS_IMAGE:latest || true - - docker build --cache-from $METRICS_IMAGE:latest -t $METRICS_IMAGE:latest var - - docker push $METRICS_IMAGE:latest - allow_failure: true - cache: - paths: - - var - policy: pull - only: - refs: - - develop - -php-metrics-deploy: - image: alpine - stage: metrics-deploy - script: - - apk add --update curl - - curl -XPOST $METRICS_WEBHOOK - only: - refs: - - develop - install: image: jakzal/phpqa stage: install @@ -102,9 +54,9 @@ build: stage: build script: - docker login -u fagneray -p $PORTUS_TOKEN portus.lam.fr - - docker pull $CONTAINER_IMAGE:latest-dev || true - - docker build --cache-from $CONTAINER_IMAGE:latest-dev -t $CONTAINER_IMAGE:latest-dev . - - docker push $CONTAINER_IMAGE:latest-dev + - docker pull $CONTAINER_IMAGE:latest || true + - docker build --cache-from $CONTAINER_IMAGE:latest -t $CONTAINER_IMAGE:latest . + - docker push $CONTAINER_IMAGE:latest cache: paths: - vendor @@ -112,13 +64,3 @@ build: only: refs: - develop - -deploy: - image: alpine - stage: deploy - script: - - apk add --update curl - - curl -XPOST $DEV_WEBHOOK - only: - refs: - - develop \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b6975a7..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [3.0.0] - yyyy-mm-dd -### Added -- Pour les nouvelles fonctionnalités. - -### Changed -- Pour les changements au sein des fonctionnalités déjà existantes. - -### Deprecated -- Pour les fonctionnalités qui seront supprimées dans la prochaine publication. - -### Removed -- Pour les anciennes fonctionnalités Deprecated qui viennent d’être supprimées. - -### Fixed -- Pour les corrections de bugs. - -### Security -- Pour encourager les utilisateurs à mettre à niveau afin d’éviter des failles de sécurité. diff --git a/Dockerfile b/Dockerfile index 15e3de7..dc688d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM php:7.3-apache # Install modules RUN apt-get update \ && apt-get install -y zlib1g zlib1g-dev libpq-dev libpq5 libzip-dev zip unzip \ - && docker-php-ext-install pgsql pdo_pgsql zip + && docker-php-ext-install pgsql pdo_pgsql zip bcmath RUN a2enmod rewrite @@ -12,4 +12,4 @@ COPY ./conf-dev/vhost.conf /etc/apache2/sites-available/000-default.conf WORKDIR /srv/app -CMD ["apache2-foreground"] \ No newline at end of file +CMD ["apache2-foreground"] diff --git a/LICENSE b/LICENSE index 7fb812e..7638578 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ - -AstroNomical Information System - Server API +AstroNomical Information System - Server - Copyright: François Agneray & Chrystel Moreau 2012 - 2019 + Copyright: CNRS - 2019 Address: Centre de donneeS Astrophysique de Marseille (CeSAM) Laboratoire d'Astrophysique de Marseille Ple de l'Etoile, site de Chteau-Gombert @@ -9,14 +8,12 @@ AstroNomical Information System - Server API 13388 Marseille cedex 13 France CNRS U.M.R 7326 -ANIS Server API is governed by the CeCILL license under French law and +Anis Server is governed by the CeCILL license under French law and abiding by the rules of distribution of free software. You can use, modify and/ or redistribute the software under the terms of the CeCILL license as circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info" and/or below. - - CeCILL FREE SOFTWARE LICENSE AGREEMENT ====================================== diff --git a/Makefile b/Makefile index beda1b5..c5f2879 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,23 @@ -UID := $(id -u) -GID := $(id -g) +UID := 1000 +GID := 1000 list: @echo "" @echo "Useful targets:" @echo "" @echo " install > install php composer dependancies" - @echo " up > build php image and start anis-server containers for dev only (php + mailer)" - @echo " start > start anis-server containers" - @echo " restart > restart anis-server containers" - @echo " stop > stop and kill running anis-server containers" - @echo " logs > display anis-server containers logs" - @echo " shell > shell into php anis-server container" + @echo " rebuild > rebuild php image and start containers for dev only" + @echo " start > start containers" + @echo " restart > restart containers" + @echo " stop > stop and kill running containers" + @echo " status > display stack containers status" + @echo " logs > display containers logs" + @echo " shell > shell into php container" @echo " phpunit > run php unit test suite" @echo " phpcs > run php code sniffer test suite" - @echo " anis-init > generate a new anis admin database and a default metamodel database (anis-server containers running needed)" - @echo " dev-init > add metadata datasets for devlopment purpose" + @echo " create-db > create a database for dev only" + @echo " dev-meta > load metamodel information for testing application" @echo " remove-pgdata > remove the metadata database" - @echo " gen-key > generate a new encryption key that you can use to encrypt data (see anis server config file)" @echo "" install: @@ -26,7 +26,7 @@ install: -v $(CURDIR):/project \ -w /project jakzal/phpqa composer install --ignore-platform-reqs -up: +rebuild: @docker-compose up --build -d start: @@ -38,6 +38,9 @@ stop: @docker-compose kill @docker-compose rm -v --force +status: + @docker-compose ps + logs: @docker-compose logs -f -t @@ -48,21 +51,18 @@ phpunit: @docker run --init -it --rm --user $(UID):$(GID) \ -v $(CURDIR):/project \ -w /project jakzal/phpqa phpdbg -qrr ./vendor/bin/phpunit --bootstrap ./tests/bootstrap.php \ - --whitelist src --coverage-text --colors=never ./tests + --whitelist src --colors --coverage-html ./phpunit-coverage ./tests phpcs: @docker run --init -it --rm --user $(UID):$(GID) \ -v $(CURDIR):/project \ - -w /project jakzal/phpqa phpcs --standard=PSR2 --extensions=php --colors src tests + -w /project jakzal/phpqa phpcs --standard=PSR12 --extensions=php --colors src tests -anis-init: - @docker-compose exec php sh ./conf-dev/anis_init.sh +create-db: + @docker-compose exec php ./vendor/bin/doctrine orm:schema-tool:create -dev-init: - @docker-compose exec php sh ./conf-dev/dev-init.sh +dev-meta: + @docker-compose exec php sh ./conf-dev/dev-meta.sh remove-pgdata: - @docker volume rm anis-server_pgdata - -gen-key: - @docker-compose exec php php ./conf-dev/generate_encryption_key.php \ No newline at end of file + @docker volume rm anis-metamodel_pgdata diff --git a/README.md b/README.md index 64ec381..8b3e7ff 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ AstroNomical Information System is a generic web tool that aims to facilitate th This software allows you to control one or more databases related to astronomical projects and allows access to datasets via URLs. -Anis is protected by the `CeCILL` licence (see LICENCE file at the software root). +Anis is protected by the CeCILL licence (see LICENCE file at the software root). ## Authors @@ -16,111 +16,36 @@ Here is the list of people involved in the development: * `Chrystel Moreau` : Laboratoire d'Astrophysique de Marseille (CNRS) * `Tifenn Guillas` : Laboratoire d'Astrophysique de Marseille (CNRS) -# Installation guide +## Functionalities -## Prerequisites +Anis Server allows : -Before to start to install the anis-server you must make sure that you have the following commands installed on your computer: +- Add project/database that will contain a set of datasets +- Add and configure a dataset that references a table or view of a business database +- The possibility to search in a referenced dataset with search criteria -1. `make` -2. `docker` -3. `docker-compose` +## Installing and starting the application -You also need an Internet connection to download packages and dependancies. +Anis Server contains a Makefile that helps the developer to install and start the application. -## List of commands +To list all operations availables just type `make` in your terminal at the root of this application. -The `Makefile` at the root of the project provides a list of commands available to manage the application in a development mode. +- To install all dependancies : `make install` +- To start/stop/restart/status all services : `make start|stop|restart|status` +- To display logs for all services : `make logs` +- To open a shell command into php container : `make shell` +- To execute tests suite : `make phpunit` +- To execute php code sniffer : `make phpcs` +- To create the metamodel database : `make create-db` +- To load metamodel information for testing : `make dev-meta` -To see the list of commands just open a terminal in the root of the project and type: +## Open API -> make +You can find an open api documentation into the `anis-server.yaml` file. -**Warning**: The `docker-compose.yml` and the` Makefile` commands must be used only for development or testing but no in production mode. +## Few examples with curl -## Dependancies installation - -To start installing dependancies, use the following command at the software root: - -> make install - -The `Composer.phar` software will be downloaded and it will automatically use the `composer.json` and `composer.lock` files to download all the dependancies and save them to the `vendor` directory at the root of the application. - -After that anis-server can work! - -## Start anis-server - -Anis server has been developed to be used with docker containers. If you open the `docker-compose.yml` file you can see all containers needed for the **development or test configuration** of anis server. - -`Makefile` provides a command to build install and run all the containers that you need for a fresh installation of anis-server software: - -> make up - -**Note:** This command use `docker-compose` to work. These operations may take a few minutes as it is necessary to download docker images - -If you want to list all anis stack containers running: - -> docker-compose ps - -And if you want to print anis stack logs: - -> make logs - -## Databases installation - -Anis server need at least two databases to work: - -1. An admin database to store users and the list of available metamodel databases. -2. A default metamodel database to store information about business databases, projects and datasets availables. - -Our `Makefile` provides a command to generate these two databases: - -> make anis-init - -## Datasets for beginning to use - -Our `Makefile` also provides a command to add a business database and configure datasets into the default metamodel database to beginning to test or develop the anis-server. To install the test datasets type: - -> make dev-init - -## Anis server is now ready to use - -Open a browser and go => [http://localhost:8080/](http://localhost:8080/) - -Few examples: - -* To list all datasets available in the default instance => [http://localhost:8080/metadata/default/dataset](http://localhost:8080/metadata/default/dataset) -* To print all data for the obs_cat dataset with column 1, 2 and 3 => [http://localhost:8080/search/default/data/obs_cat?a=1;2;3](http://localhost:8080/search/default/data/obs_cat?a=1;2;3) -* To print only 3 obs_cat data (search by id) => [http://localhost:8080/search/default/data/obs_cat?a=1;2;3&c=1::in::104600094|104600095|104600108](http://localhost:8080/search/default/data/obs_cat?a=1;2;3&c=1::in::104600094|104600095|104600108) - -# More about Anis-Server - -First of all, you will find the user manual at the root of the project: [MANUAL.md](MANUAL.md) - -## Software directories - -* `conf-dev`: Configuration files used by make commands and docker-compose to work -* `public`: Web server root (index.php) -* `src`: Source code of Anis Server -* `test`: Anis Unit Tests `phpunit` - -## Key files - -* `public/index.php`: Bootstrap file for starting application (file used by an http web server like nginx or apache) -* `src/settings.php`: Anis server configuration file -* `src/routes.php`: Anis server configured routes (list all availables URL) - -## Technologies - -You can see here just a few direct links about softwares or dependancies used by anis-team for the devlopment of anis-server: - -* `Slim` : [http://www.slimframework.com/](http://www.slimframework.com/) -* `Doctrine 2` : [https://www.doctrine-project.org/](https://www.doctrine-project.org/) -* `Swiftmailer` : [https://swiftmailer.symfony.com/](https://swiftmailer.symfony.com/) -* `Monolog` : [https://seldaek.github.io/monolog/](https://seldaek.github.io/monolog/) -* `Composer` : [https://getcomposer.org/](https://getcomposer.org/) -* `PHP-FIG` : [http://www.php-fig.org/](http://www.php-fig.org/) -* `PHP-Unit` : [http://phpunit.de/](http://phpunit.de/) -* `Docker` : [https://www.docker.com/](https://www.docker.com/) -* `GIT` : [http://git-scm.com/](http://git-scm.com/) -* `CeCILL`: [http://www.cecill.info/index.en.html](http://www.cecill.info/index.en.html) \ No newline at end of file +* To list all datasets available in the default instance => http://localhost:8082/dataset +* To print all data for the obs_cat dataset with column 1, 2 and 3 => http://localhost:8082/search/obs_cat?a=1;2;3 +* To count the number of data available for a request => http://localhost:8082/search/obs_cat?a=count +* To print only 3 obs_cat data (search by id) => http://localhost:8082/search/obs_cat?a=1;2;3&c=1::in::104600094|104600095|104600108 \ No newline at end of file diff --git a/VERSION b/VERSION index 282895a..06a4457 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.3 \ No newline at end of file +3.1 \ No newline at end of file diff --git a/anis-server.yaml b/anis-server.yaml deleted file mode 100644 index 142348f..0000000 --- a/anis-server.yaml +++ /dev/null @@ -1,1009 +0,0 @@ -openapi: 3.0.1 -info: - title: Anis Server API - description: 'AstroNomical Information System is a generic web tool that aims to facilitate the provision of data (Astrophysics), accessible from a database, to a community of scientists.' - contact: - email: anis@lam.fr - license: - name: CeCILL 2.1 - url: http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.html - version: 3.0.0 -servers: -- url: https://anis-dev.lam.fr/ -tags: -- name: root - description: Anis Server API root path -- name: login - description: Set of actions about the user (registration, token, ...) -- name: metadata - description: Set of actions about metamodel database management -- name: search - description: Access to the data -- name: settings - description: Stores data to help the administrator fill the metamodel -paths: - /: - get: - tags: - - root - summary: Ensures that the service works - responses: - 200: - description: Ensures that the service works - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - - /register: - post: - tags: - - login - summary: Add a new Anis user - description: Action to register a new Anis user. An email with a code is send by the server to activate this new account. - operationId: register - requestBody: - description: Login object that needs to add a new anis user - content: - application/json: - schema: - $ref: '#/components/schemas/Login' - required: true - responses: - 201: - description: New user correctly added - content: - application/json: - schema: - $ref: '#/components/schemas/AnisUser' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /activate-account: - get: - tags: - - login - summary: Activate a new account - description: Activate a new registered user with the code sended by the server - operationId: activate - parameters: - - name: email - in: query - description: The email adress of the account to be activated - required: true - schema: - type: string - - name: activation_key - in: query - description: The activation code sended by the server - required: true - schema: - type: string - responses: - 200: - description: Account successfully activated - content: - application/json: - schema: - $ref: '#/components/schemas/AnisUser' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /login: - post: - tags: - - login - summary: Get an Anis user token - description: Request a json web token (jwt) to the Anis Server with a couple of email and password - operationId: login - requestBody: - description: Login object that needs to get a new token - content: - application/json: - schema: - $ref: '#/components/schemas/Login' - required: true - responses: - 200: - description: Returns a new token - content: - application/json: - schema: - $ref: '#/components/schemas/Token' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /new-password: - post: - tags: - - login - summary: Ask for a new password - description: Request the generation of a new password that will be sent by email - operationId: newPassword - requestBody: - description: The email address of the account for which the new password will be generated - content: - application/json: - schema: - properties: - email: - type: string - example: user@provider.fr - required: true - responses: - 200: - description: New password generated - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /change-password: - post: - tags: - - login - summary: Anis user can change his account password - description: Ask anis server with the actual and the new password - operationId: changePassword - requestBody: - description: The email address and the actual password of the account for which the new password will be change + the new password - content: - application/json: - schema: - $ref: '#/components/schemas/ChangePassword' - required: true - responses: - 200: - description: Password changed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/database: - get: - tags: - - metadata - summary: List all available databases - description: Anis can connect to databases (PostgreSQL, MySQL, SQLite, Oracle ...). This action lists the databases already registered and where anis can connect. - operationId: getDatabaseList - responses: - 200: - description: Databases list - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Database' - post: - tags: - - metadata - summary: Add a new database - description: Anis can connect to databases (PostgreSQL, MySQL, SQLite, Oracle ...). This action add a new data source. - operationId: addDatabase - requestBody: - description: Database form object that needs to be added to the store - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseForm' - required: true - responses: - 200: - description: Database added - content: - application/json: - schema: - $ref: '#/components/schemas/Database' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/database/{id}: - get: - tags: - - metadata - summary: Find a database by ID - description: Returns a single database registered - operationId: getDatabase - parameters: - - name: id - in: path - description: ID of database to return - required: true - schema: - type: integer - responses: - 200: - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Database' - 404: - description: Database not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - put: - tags: - - metadata - summary: Updates a database with form data - operationId: editDatabase - parameters: - - name: id - in: path - description: ID of database to return - required: true - schema: - type: integer - requestBody: - description: Database form object that needs to be added to the store - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseForm' - required: true - responses: - 200: - description: Database edited - content: - application/json: - schema: - $ref: '#/components/schemas/Database' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 404: - description: Database not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - metadata - summary: Delete a registered database - operationId: deleteDatabase - parameters: - - name: id - in: path - description: ID of database to return - required: true - schema: - type: integer - responses: - 200: - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - 404: - description: Database not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/database/{id}/table: - get: - tags: - - metadata - summary: Get tables and views list - description: Get tables and views listed in the database identified by the ID parameter - operationId: tableListDatabase - parameters: - - name: id - in: path - description: ID of database to return - required: true - schema: - type: integer - responses: - 200: - description: Tables and views list - content: - application/json: - schema: - type: array - items: - type: string - 404: - description: Database not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/project: - get: - tags: - - metadata - summary: List all available projects - description: All searchable datasets are listed in one or more projects and a project is attached to a database - operationId: getProjectList - responses: - 200: - description: Project list - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Project' - post: - tags: - - metadata - summary: Add a new project - description: All searchable datasets are listed in one or more projects and a project is attached to a database. This action add a new data project. - operationId: addProject - requestBody: - description: Project form object that needs to be added to the store - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - required: true - responses: - 200: - description: Project added - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/project/{name}: - get: - tags: - - metadata - summary: Find a project by name - description: Returns a single project registered - operationId: getProject - parameters: - - name: name - in: path - description: Name of project to return - required: true - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - 404: - description: Project not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - put: - tags: - - metadata - summary: Updates a project with form data - operationId: editProject - parameters: - - name: name - in: path - description: Name of project to return - required: true - schema: - type: string - requestBody: - description: Project form object that needs to be edited to the store - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - required: true - responses: - 200: - description: Project edited - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 404: - description: Project not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - metadata - summary: Delete a registered project - operationId: deleteProject - parameters: - - name: name - in: path - description: Name of project to return - required: true - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - 404: - description: Project not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/dataset: - get: - tags: - - metadata - summary: List all available datasets - description: Get all searchable datasets - operationId: getDatasetList - responses: - 200: - description: Dataset list - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Dataset' - post: - tags: - - metadata - summary: Add a new dataset - description: Add a new dataset. This action add automatically the associated attributes (columns) - operationId: addDataset - requestBody: - description: Dataset form object that needs to be added to the store - content: - application/json: - schema: - $ref: '#/components/schemas/Dataset' - required: true - responses: - 200: - description: Dataset added - content: - application/json: - schema: - $ref: '#/components/schemas/Dataset' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/dataset/{name}: - get: - tags: - - metadata - summary: Find a dataset by name - description: Returns a single dataset registered - operationId: getDataset - parameters: - - name: name - in: path - description: Name of dataset to return - required: true - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Dataset' - 404: - description: Dataset not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - put: - tags: - - metadata - summary: Updates a dataset with form data - operationId: editDataset - parameters: - - name: name - in: path - description: Name of dataset to return - required: true - schema: - type: string - requestBody: - description: Dataset form object that needs to be edited to the store - content: - application/json: - schema: - $ref: '#/components/schemas/Dataset' - required: true - responses: - 200: - description: Dataset edited - content: - application/json: - schema: - $ref: '#/components/schemas/Dataset' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 404: - description: Dataset not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - metadata - summary: Delete a registered dataset - operationId: deleteDataset - parameters: - - name: name - in: path - description: Name of dataset to return - required: true - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - 404: - description: Dataset not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /metadata/dataset/{name}/attribute: - get: - tags: - - metadata - summary: Retrieve all attributes for a dataset - operationId: getAttributes - parameters: - - name: name - in: path - description: Name of dataset to return - required: true - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Attribute' - 404: - description: Dataset not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - put: - tags: - - metadata - summary: Updates all attributes for a dataset - operationId: editAttributes - parameters: - - name: name - in: path - description: Name of dataset to return - required: true - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Attribute' - 404: - description: Dataset not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /search/meta/{dname}: - get: - tags: - - search - summary: Retrieve all metdata of a search - parameters: - - name: dname - in: path - description: Name of dataset about the search - required: true - schema: - type: string - - name: c - in: query - description: Search criteria list separated by a semicolon - required: true - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/MetaResponse' - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 404: - description: Dataset not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /search/data/{dname}: - get: - tags: - - search - summary: Retrieve all data of a search - parameters: - - name: dname - in: path - description: Name of dataset about the search - required: true - schema: - type: string - - name: c - in: query - description: Search criteria list separated by a semicolon - required: true - schema: - type: string - - name: a - in: query - description: Search id attributes output list separated by a semicolon - required: true - schema: - type: string - - name: o - in: query - description: Display order list of the search separated by a semicolon. This parameter is mandatory for pagination (p) - required: false - schema: - type: string - - name: p - in: query - description: Pagination settings separated by a colon - required: false - schema: - type: string - responses: - 200: - description: Successful operation - content: - application/json: - schema: - type: array - items: - type: object - 400: - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 404: - description: Dataset not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - -components: - schemas: - ApiResponse: - type: object - properties: - code: - type: integer - format: int32 - type: - type: string - message: - type: string - ErrorResponse: - type: object - properties: - error: - type: string - example: Invalid request - error_description: - type: string - example: Param email needed to register a new user - Token: - type: object - properties: - email: - type: string - example: user@provider.fr - token: - type: string - Login: - type: object - properties: - email: - type: string - example: user@provider.fr - password: - type: string - example: my_password - AnisUser: - type: object - properties: - email: - type: string - example: user@provider.fr - activated: - type: boolean - example: false - adminsi: - type: boolean - example: false - superuser: - type: boolean - example: false - id_group: - type: integer - example: 1 - ChangePassword: - type: object - properties: - email: - type: string - example: user@provider.fr - password: - type: string - example: my_password - new_password: - type: string - example: my_new_password - DatabaseForm: - type: object - properties: - label: - type: string - example: Database - dbname: - type: string - example: db1 - dbtype: - type: string - example: pgsql - dbhost: - type: string - example: dbserver - dbport: - type: integer - example: 5432 - dblogin: - type: string - example: dbuser - dbpassword: - type: string - example: dbpassword - Database: - allOf: - - type: object - properties: - id: - type: string - example: 1 - - $ref: '#/components/schemas/DatabaseForm' - Project: - type: object - properties: - name: - type: string - example: my_project - label: - type: string - example: My Project - description: - type: string - example: Project description - link: - type: string - example: http://myproject-website.com - manager: - type: string - example: M. Dupont - id_database: - type: integer - example: 1 - Dataset: - type: object - properties: - name: - type: string - example: my_dataset - table_ref: - type: string - example: database_table_name - label: - type: string - example: My dataset - description: - type: string - example: My dataset description - display: - type: integer - example: 10 - count: - type: integer - example: 1500 - vo: - type: boolean - example: true - data_path: - type: string - example: /mnt/my_data - project_name: - type: string - example: my_project - id_dataset_family: - type: integer - example: 1 - Attribute: - type: object - properties: - id: - type: integer - example: 1 - name: - type: string - example: ra - table_name: - type: string - example: obs_cat - label: - type: string - example: Main_catalog - form_label: - type: string - example: Main obervations catalog - description: - type: string - example: Description for this catalog - output_display: - type: integer - criteria_display: - type: integer - search_flag: - type: string - search_type: - type: string - type: - type: string - operator: - type: string - min: - type: number - max: - type: number - placeholder_min: - type: string - placeholder_max: - type: string - uri_action: - type: string - renderer: - type: string - display_detail: - type: string - selected: - type: boolean - order_by: - type: boolean - order_display: - type: integer - detail: - type: boolean - renderer_detail: - type: string - vo_utype: - type: string - vo_ucd: - type: string - vo_unit: - type: string - vo_description: - type: string - vo_datatype: - type: string - vo_size: - type: integer - id_criteria_family: - type: integer - id_output_family: - type: integer - id_category: - type: integer - MetaResponse: - type: object - properties: - dataset-selected: - type: string - total-items: - type: integer - url: - type: string \ No newline at end of file diff --git a/app/constants.php b/app/constants.php new file mode 100644 index 0000000..75400ac --- /dev/null +++ b/app/constants.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +define('GET', 'GET'); +define('POST', 'POST'); +define('PUT', 'PUT'); +define('DELETE', 'DELETE'); +define('OPTIONS', 'OPTIONS'); +define('SETTINGS', 'settings'); diff --git a/app/dependencies.php b/app/dependencies.php new file mode 100644 index 0000000..e64cc63 --- /dev/null +++ b/app/dependencies.php @@ -0,0 +1,113 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +use Psr\Container\ContainerInterface; + +// Load settings +$container->set(SETTINGS, function () { + return include __DIR__ . '/../app/settings.php'; +}); + +// Doctrine factory +$container->set('em', function (ContainerInterface $c) { + $settings = $c->get(SETTINGS)['database']; + $dc = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration( + array('src/Entity'), + $settings['dev_mode'] + ); + $dc->setProxyDir($settings['path_proxy']); + if ($settings['dev_mode']) { + $dc->setAutoGenerateProxyClasses(true); + } else { + $dc->setAutoGenerateProxyClasses(false); + } + return \Doctrine\ORM\EntityManager::create($settings['connection_options'], $dc); +}); + +// Monolog factory +$container->set('logger', function (ContainerInterface $c) { + $loggerSettings = $c->get('settings')['logger']; + $logger = new \Monolog\Logger($loggerSettings['name']); + $logger->pushProcessor(new \Monolog\Processor\UidProcessor()); + $logger->pushHandler(new \Monolog\Handler\StreamHandler($loggerSettings['path'], $loggerSettings['level'])); + return $logger; +}); + +// Actions +$container->set('App\Action\RootAction', function () { + return new App\Action\RootAction(); +}); + +$container->set('App\Action\DatabaseListAction', function (ContainerInterface $c) { + return new App\Action\DatabaseListAction($c->get('em')); +}); + +$container->set('App\Action\DatabaseAction', function (ContainerInterface $c) { + return new App\Action\DatabaseAction($c->get('em')); +}); + +$container->set('App\Action\TableListAction', function (ContainerInterface $c) { + return new App\Action\TableListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); +}); + +$container->set('App\Action\ProjectListAction', function (ContainerInterface $c) { + return new App\Action\ProjectListAction($c->get('em')); +}); + +$container->set('App\Action\ProjectAction', function (ContainerInterface $c) { + return new App\Action\ProjectAction($c->get('em')); +}); + +$container->set('App\Action\FamilyListAction', function (ContainerInterface $c) { + return new App\Action\FamilyListAction($c->get('em')); +}); + +$container->set('App\Action\FamilyAction', function (ContainerInterface $c) { + return new App\Action\FamilyAction($c->get('em')); +}); + +$container->set('App\Action\OutputCategoryListAction', function (ContainerInterface $c) { + return new App\Action\OutputCategoryListAction($c->get('em')); +}); + +$container->set('App\Action\OutputCategoryAction', function (ContainerInterface $c) { + return new App\Action\OutputCategoryAction($c->get('em')); +}); + +$container->set('App\Action\DatasetListAction', function (ContainerInterface $c) { + return new App\Action\DatasetListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); +}); + +$container->set('App\Action\DatasetAction', function (ContainerInterface $c) { + return new App\Action\DatasetAction($c->get('em'), new App\Utils\DBALConnectionFactory()); +}); + +$container->set('App\Action\AttributeListAction', function (ContainerInterface $c) { + return new App\Action\AttributeListAction($c->get('em')); +}); + +$container->set('App\Action\AttributeAction', function (ContainerInterface $c) { + return new App\Action\AttributeAction($c->get('em')); +}); + +$container->set('App\Action\SearchAction', function (ContainerInterface $c) { + return new App\Action\SearchAction( + $c->get('em'), + new App\Utils\DBALConnectionFactory(), + new App\Utils\Operator\OperatorFactory() + ); +}); + +// Middlewares +$container->set('App\Middleware\AuthorizationMiddleware', function (ContainerInterface $c) { + return new App\Middleware\AuthorizationMiddleware($c->get('em'), $c->get(SETTINGS)['token_options']); +}); diff --git a/app/middlewares.php b/app/middlewares.php new file mode 100644 index 0000000..b029af0 --- /dev/null +++ b/app/middlewares.php @@ -0,0 +1,14 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +$app->add(new App\Middleware\JsonBodyParserMiddleware()); +$app->add(new App\Middleware\ContentTypeJsonMiddleware()); diff --git a/app/routes.php b/app/routes.php new file mode 100644 index 0000000..42c5ec3 --- /dev/null +++ b/app/routes.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +use Slim\Routing\RouteCollectorProxy; + +$app->get('/', App\Action\RootAction::class); +$app->map([OPTIONS, GET, POST], '/database', App\Action\DatabaseListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/database/{id}', App\Action\DatabaseAction::class); +$app->map([OPTIONS, GET], '/database/{id}/table', App\Action\TableListAction::class); +$app->map([OPTIONS, GET, POST], '/project', App\Action\ProjectListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/project/{name}', App\Action\ProjectAction::class); +$app->map([OPTIONS, GET, POST], '/family/{type}', App\Action\FamilyListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/family/{type}/{id}', App\Action\FamilyAction::class); +$app->map([OPTIONS, GET, POST], '/output-category', App\Action\OutputCategoryListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class); +$app->map([OPTIONS, GET, POST], '/dataset', App\Action\DatasetListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class); +$app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class); +$app->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class); +$app->get('/search/{dname}', App\Action\SearchAction::class); diff --git a/app/settings.php b/app/settings.php new file mode 100644 index 0000000..c56912f --- /dev/null +++ b/app/settings.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +return [ + 'displayErrorDetails' => getenv('DISPLAY_ERROR_DETAILS'), + 'database' => [ + 'path_proxy' => getenv('DATABASE_PATH_PROXY'), + 'dev_mode' => getenv('DATABASE_DEV_MODE'), + 'connection_options' => [ + 'driver' => getenv('DATABASE_CO_DRIVER'), + 'path' => getenv('DATABASE_CO_PATH'), + 'host' => getenv('DATABASE_CO_HOST'), + 'port' => (int) getenv('DATABASE_CO_PORT'), + 'dbname' => getenv('DATABASE_CO_DBNAME'), + 'user' => getenv('DATABASE_CO_USER'), + 'password' => getenv('DATABASE_CO_PASSWORD') + ], + ], + 'logger' => [ + 'name' => getenv('LOGGER_NAME'), + 'path' => getenv('LOGGER_PATH'), + 'level' => getenv('LOGGER_LEVEL') + ] +]; diff --git a/cli-config.php b/cli-config.php index 4039d69..1436d10 100644 --- a/cli-config.php +++ b/cli-config.php @@ -2,17 +2,17 @@ // File needed by doctrine cli require 'vendor/autoload.php'; -$settings = require './src/settings.php'; -$adminDb = $settings['settings']['admin_db']; +$settings = require './app/settings.php'; +$database = $settings['database']; -$c = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(array('src/Entity/Admin'), $adminDb['dev_mode']); -$c->setProxyDir(getcwd() . '/' . $adminDb['path_proxy']); -if ($adminDb['dev_mode']) { +$c = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(array('src/Entity'), $database['dev_mode']); +$c->setProxyDir(getcwd() . '/' . $database['path_proxy']); +if ($database['dev_mode']) { $c->setAutoGenerateProxyClasses(true); } else { $c->setAutoGenerateProxyClasses(false); } -$em = \Doctrine\ORM\EntityManager::create($adminDb['connection_options'], $c); +$em = \Doctrine\ORM\EntityManager::create($database['connection_options'], $c); $helpers = new Symfony\Component\Console\Helper\HelperSet(array( 'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()), diff --git a/composer.json b/composer.json index d0c4c18..6bf0294 100644 --- a/composer.json +++ b/composer.json @@ -1,23 +1,32 @@ { - "name": "cesamsi/anis-v3-server", - "description": "CMS used for astronomical project", + "name": "anis/server", + "type": "project", + "description": "Anis Server is used to configure information system CMS and to search into a business database", + "license": "CeCILL v2.1", "authors": [ { "name": "François Agneray", "email": "francois.agneray@lam.fr" + }, + { + "name": "Chrystel Moreau", + "email": "chrystel.moreau@lam.fr" + }, + { + "name": "Tifenn Guillas", + "email": "tifenn.guillas@lam.fr" } ], "require": { - "slim/slim": "^3.8", - "doctrine/orm": "^2.5", - "monolog/monolog": "^1.23", - "lcobucci/jwt": "^3.2", - "swiftmailer/swiftmailer": "^6.0", - "php-amqplib/php-amqplib": "^2.10" + "slim/slim": "^4.0", + "nyholm/psr7": "^1.2", + "nyholm/psr7-server": "^0.3.0", + "php-di/php-di": "^6.0", + "monolog/monolog": "^2.0", + "doctrine/orm": "^2.6" }, "require-dev": { - "phpunit/phpunit": "^7.2", - "phpunit/dbunit": "^4.0" + "phpunit/phpunit": "^7.2" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index e02f710..8a59269 100644 --- a/composer.lock +++ b/composer.lock @@ -4,51 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a2184ef90f51603d72f1a7f49ec35c4a", + "content-hash": "41b0ad136ffb7b6f8f4c93825e72e5be", "packages": [ - { - "name": "container-interop/container-interop", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/container-interop/container-interop.git", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "shasum": "" - }, - "require": { - "psr/container": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Interop\\Container\\": "src/Interop/Container/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", - "homepage": "https://github.com/container-interop/container-interop", - "time": "2017-02-14T19:40:03+00:00" - }, { "name": "doctrine/annotations", - "version": "v1.7.0", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "fa4c4e861e809d6a1103bd620cce63ed91aedfeb" + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/fa4c4e861e809d6a1103bd620cce63ed91aedfeb", - "reference": "fa4c4e861e809d6a1103bd620cce63ed91aedfeb", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/904dca4eb10715b92569fbcd79e201d5c349b6bc", + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc", "shasum": "" }, "require": { @@ -57,7 +26,7 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^7.5@dev" + "phpunit/phpunit": "^7.5" }, "type": "library", "extra": { @@ -103,20 +72,20 @@ "docblock", "parser" ], - "time": "2019-08-08T18:11:40+00:00" + "time": "2019-10-01T18:55:10+00:00" }, { "name": "doctrine/cache", - "version": "v1.8.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57" + "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57", - "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57", + "url": "https://api.github.com/repos/doctrine/cache/zipball/382e7f4db9a12dc6c19431743a2b096041bcdd62", + "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62", "shasum": "" }, "require": { @@ -127,7 +96,7 @@ }, "require-dev": { "alcaeus/mongo-php-adapter": "^1.1", - "doctrine/coding-standard": "^4.0", + "doctrine/coding-standard": "^6.0", "mongodb/mongodb": "^1.1", "phpunit/phpunit": "^7.0", "predis/predis": "~1.0" @@ -138,7 +107,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8.x-dev" + "dev-master": "1.9.x-dev" } }, "autoload": { @@ -151,6 +120,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -159,10 +132,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -172,26 +141,33 @@ "email": "schmittjoh@gmail.com" } ], - "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "https://www.doctrine-project.org", + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", "keywords": [ + "abstraction", + "apcu", "cache", - "caching" + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" ], - "time": "2018-08-21T18:01:43+00:00" + "time": "2019-11-29T15:36:20+00:00" }, { "name": "doctrine/collections", - "version": "v1.6.2", + "version": "1.6.4", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be" + "reference": "6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/c5e0bc17b1620e97c968ac409acbff28b8b850be", - "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be", + "url": "https://api.github.com/repos/doctrine/collections/zipball/6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7", + "reference": "6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7", "shasum": "" }, "require": { @@ -219,6 +195,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -227,10 +207,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -248,7 +224,7 @@ "iterators", "php" ], - "time": "2019-06-09T13:48:14+00:00" + "time": "2019-11-13T13:07:11+00:00" }, { "name": "doctrine/common", @@ -335,31 +311,30 @@ }, { "name": "doctrine/dbal", - "version": "v2.9.2", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9" + "reference": "0c9a646775ef549eb0a213a4f9bd4381d9b4d934" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9", - "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/0c9a646775ef549eb0a213a4f9bd4381d9b4d934", + "reference": "0c9a646775ef549eb0a213a4f9bd4381d9b4d934", "shasum": "" }, "require": { "doctrine/cache": "^1.0", "doctrine/event-manager": "^1.0", "ext-pdo": "*", - "php": "^7.1" + "php": "^7.2" }, "require-dev": { - "doctrine/coding-standard": "^5.0", - "jetbrains/phpstorm-stubs": "^2018.1.2", - "phpstan/phpstan": "^0.10.1", - "phpunit/phpunit": "^7.4", - "symfony/console": "^2.0.5|^3.0|^4.0", - "symfony/phpunit-bridge": "^3.4.5|^4.0.5" + "doctrine/coding-standard": "^6.0", + "jetbrains/phpstorm-stubs": "^2019.1", + "phpstan/phpstan": "^0.11.3", + "phpunit/phpunit": "^8.4.1", + "symfony/console": "^2.0.5|^3.0|^4.0|^5.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -370,7 +345,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.9.x-dev", + "dev-master": "2.10.x-dev", "dev-develop": "3.0.x-dev" } }, @@ -384,6 +359,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -392,10 +371,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -406,27 +381,38 @@ "keywords": [ "abstraction", "database", + "db2", "dbal", + "mariadb", + "mssql", "mysql", - "persistence", + "oci8", + "oracle", + "pdo", "pgsql", - "php", - "queryobject" - ], - "time": "2018-12-31T03:27:51+00:00" + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlanywhere", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "time": "2019-11-03T16:50:43+00:00" }, { "name": "doctrine/event-manager", - "version": "v1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3" + "reference": "629572819973f13486371cb611386eb17851e85c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3", - "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/629572819973f13486371cb611386eb17851e85c", + "reference": "629572819973f13486371cb611386eb17851e85c", "shasum": "" }, "require": { @@ -436,7 +422,7 @@ "doctrine/common": "<2.9@dev" }, "require-dev": { - "doctrine/coding-standard": "^4.0", + "doctrine/coding-standard": "^6.0", "phpunit/phpunit": "^7.0" }, "type": "library", @@ -455,6 +441,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -463,10 +453,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -480,27 +466,29 @@ "email": "ocramius@gmail.com" } ], - "description": "Doctrine Event Manager component", + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", "homepage": "https://www.doctrine-project.org/projects/event-manager.html", "keywords": [ "event", - "eventdispatcher", - "eventmanager" + "event dispatcher", + "event manager", + "event system", + "events" ], - "time": "2018-06-11T11:59:03+00:00" + "time": "2019-11-10T09:48:07+00:00" }, { "name": "doctrine/inflector", - "version": "v1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5527a48b7313d15261292c149e55e26eae771b0a" + "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a", - "reference": "5527a48b7313d15261292c149e55e26eae771b0a", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/ec3a55242203ffa6a4b27c58176da97ff0a7aec1", + "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1", "shasum": "" }, "require": { @@ -525,6 +513,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -533,10 +525,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -554,20 +542,20 @@ "singularize", "string" ], - "time": "2018-01-09T20:05:19+00:00" + "time": "2019-10-30T19:59:35+00:00" }, { "name": "doctrine/instantiator", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "a2c590166b2133a4633738648b6b064edae0814a" + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", - "reference": "a2c590166b2133a4633738648b6b064edae0814a", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", "shasum": "" }, "require": { @@ -610,20 +598,20 @@ "constructor", "instantiate" ], - "time": "2019-03-17T17:37:11+00:00" + "time": "2019-10-21T16:45:58+00:00" }, { "name": "doctrine/lexer", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea" + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea", - "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", "shasum": "" }, "require": { @@ -637,7 +625,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -672,38 +660,39 @@ "parser", "php" ], - "time": "2019-07-30T19:33:28+00:00" + "time": "2019-10-30T14:39:59+00:00" }, { "name": "doctrine/orm", - "version": "v2.6.3", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "434820973cadf2da2d66e7184be370084cc32ca8" + "reference": "4d763ca4c925f647b248b9fa01b5f47aa3685d62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/434820973cadf2da2d66e7184be370084cc32ca8", - "reference": "434820973cadf2da2d66e7184be370084cc32ca8", + "url": "https://api.github.com/repos/doctrine/orm/zipball/4d763ca4c925f647b248b9fa01b5f47aa3685d62", + "reference": "4d763ca4c925f647b248b9fa01b5f47aa3685d62", "shasum": "" }, "require": { - "doctrine/annotations": "~1.5", - "doctrine/cache": "~1.6", - "doctrine/collections": "^1.4", - "doctrine/common": "^2.7.1", - "doctrine/dbal": "^2.6", - "doctrine/instantiator": "~1.1", + "doctrine/annotations": "^1.8", + "doctrine/cache": "^1.9.1", + "doctrine/collections": "^1.5", + "doctrine/common": "^2.11", + "doctrine/dbal": "^2.9.3", + "doctrine/event-manager": "^1.1", + "doctrine/instantiator": "^1.3", + "doctrine/persistence": "^1.2", "ext-pdo": "*", "php": "^7.1", - "symfony/console": "~3.0|~4.0" + "symfony/console": "^3.0|^4.0|^5.0" }, "require-dev": { - "doctrine/coding-standard": "^1.0", - "phpunit/phpunit": "^6.5", - "squizlabs/php_codesniffer": "^3.2", - "symfony/yaml": "~3.4|~4.0" + "doctrine/coding-standard": "^5.0", + "phpunit/phpunit": "^7.5", + "symfony/yaml": "^3.4|^4.0|^5.0" }, "suggest": { "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" @@ -714,7 +703,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev" + "dev-master": "2.7.x-dev" } }, "autoload": { @@ -727,6 +716,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -735,10 +728,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -749,25 +738,25 @@ } ], "description": "Object-Relational-Mapper for PHP", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org/projects/orm.html", "keywords": [ "database", "orm" ], - "time": "2018-11-20T23:46:46+00:00" + "time": "2019-11-19T08:38:05+00:00" }, { "name": "doctrine/persistence", - "version": "1.1.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "3da7c9d125591ca83944f477e65ed3d7b4617c48" + "reference": "43526ae63312942e5316100bb3ed589ba1aba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/3da7c9d125591ca83944f477e65ed3d7b4617c48", - "reference": "3da7c9d125591ca83944f477e65ed3d7b4617c48", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/43526ae63312942e5316100bb3ed589ba1aba491", + "reference": "43526ae63312942e5316100bb3ed589ba1aba491", "shasum": "" }, "require": { @@ -789,7 +778,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -802,6 +791,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -810,10 +803,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -836,7 +825,7 @@ "orm", "persistence" ], - "time": "2019-04-23T08:28:24+00:00" + "time": "2019-04-23T12:39:21+00:00" }, { "name": "doctrine/reflection", @@ -914,41 +903,36 @@ "time": "2018-06-14T14:45:07+00:00" }, { - "name": "egulias/email-validator", - "version": "2.1.11", + "name": "jeremeamia/superclosure", + "version": "2.4.0", "source": { "type": "git", - "url": "https://github.com/egulias/EmailValidator.git", - "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23" + "url": "https://github.com/jeremeamia/super_closure.git", + "reference": "5707d5821b30b9a07acfb4d76949784aaa0e9ce9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23", - "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23", + "url": "https://api.github.com/repos/jeremeamia/super_closure/zipball/5707d5821b30b9a07acfb4d76949784aaa0e9ce9", + "reference": "5707d5821b30b9a07acfb4d76949784aaa0e9ce9", "shasum": "" }, "require": { - "doctrine/lexer": "^1.0.1", - "php": ">= 5.5" + "nikic/php-parser": "^1.2|^2.0|^3.0|^4.0", + "php": ">=5.4", + "symfony/polyfill-php56": "^1.0" }, "require-dev": { - "dominicsayers/isemail": "dev-master", - "phpunit/phpunit": "^4.8.35||^5.7||^6.0", - "satooshi/php-coveralls": "^1.0.1", - "symfony/phpunit-bridge": "^4.4@dev" - }, - "suggest": { - "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + "phpunit/phpunit": "^4.0|^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "2.4-dev" } }, "autoload": { "psr-4": { - "Egulias\\EmailValidator\\": "EmailValidator" + "SuperClosure\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -957,92 +941,42 @@ ], "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" - ], - "time": "2019-08-13T17:33:27+00:00" - }, - { - "name": "lcobucci/jwt", - "version": "3.3.1", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/jwt.git", - "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", - "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "ext-openssl": "*", - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "mikey179/vfsstream": "~1.5", - "phpmd/phpmd": "~2.2", - "phpunit/php-invoker": "~1.1", - "phpunit/phpunit": "^5.7 || ^7.3", - "squizlabs/php_codesniffer": "~2.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "psr-4": { - "Lcobucci\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "LuÃs Otávio Cobucci Oblonczyk", - "email": "lcobucci@gmail.com", + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia", "role": "Developer" } ], - "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "description": "Serialize Closure objects, including their context and binding", + "homepage": "https://github.com/jeremeamia/super_closure", "keywords": [ - "JWS", - "jwt" + "closure", + "function", + "lambda", + "parser", + "serializable", + "serialize", + "tokenizer" ], - "time": "2019-05-24T18:30:49+00:00" + "time": "2018-03-21T22:21:57+00:00" }, { "name": "monolog/monolog", - "version": "1.25.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf" + "reference": "f9d56fd2f5533322caccdfcddbb56aedd622ef1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf", - "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f9d56fd2f5533322caccdfcddbb56aedd622ef1c", + "reference": "f9d56fd2f5533322caccdfcddbb56aedd622ef1c", "shasum": "" }, "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" + "php": "^7.2", + "psr/log": "^1.0.1" }, "provide": { "psr/log-implementation": "1.0.0" @@ -1050,33 +984,36 @@ "require-dev": { "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "jakub-onderka/php-parallel-lint": "0.9", + "elasticsearch/elasticsearch": "^6.0", + "graylog2/gelf-php": "^1.4.2", + "jakub-onderka/php-parallel-lint": "^0.9", "php-amqplib/php-amqplib": "~2.4", "php-console/php-console": "^3.1.3", - "phpunit/phpunit": "~4.5", - "phpunit/phpunit-mock-objects": "2.3.0", + "phpspec/prophecy": "^1.6.1", + "phpunit/phpunit": "^8.3", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "php-console/php-console": "Allow sending log messages to Google Chrome", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -1102,7 +1039,7 @@ "logging", "psr-3" ], - "time": "2019-09-06T13:49:17+00:00" + "time": "2019-11-13T10:27:43+00:00" }, { "name": "nikic/fast-route", @@ -1151,109 +1088,341 @@ "time": "2018-02-13T20:26:39+00:00" }, { - "name": "php-amqplib/php-amqplib", - "version": "v2.10.0", + "name": "nikic/php-parser", + "version": "v4.3.0", "source": { "type": "git", - "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "04e5366f032906d5f716890427e425e71307d3a8" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/04e5366f032906d5f716890427e425e71307d3a8", - "reference": "04e5366f032906d5f716890427e425e71307d3a8", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9a9981c347c5c49d6dfe5cf826bb882b824080dc", + "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc", "shasum": "" }, "require": { - "ext-bcmath": "*", - "ext-sockets": "*", - "php": ">=5.6" + "ext-tokenizer": "*", + "php": ">=7.0" }, - "replace": { - "videlalvaro/php-amqplib": "self.version" + "require-dev": { + "ircmaxell/php-yacc": "0.0.5", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2019-11-08T13:50:10+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "55ff6b76573f5b242554c9775792bd59fb52e11c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/55ff6b76573f5b242554c9775792bd59fb52e11c", + "reference": "55ff6b76573f5b242554c9775792bd59fb52e11c", + "shasum": "" + }, + "require": { + "php": "^7.1", + "php-http/message-factory": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" }, "require-dev": { - "ext-curl": "*", - "nategood/httpful": "^0.2.20", - "phpdocumentor/phpdocumentor": "dev-master", - "phpunit/phpunit": "^5.7|^6.5|^7.0", - "squizlabs/php_codesniffer": "^2.5" + "http-interop/http-factory-tests": "dev-master", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^7.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10-dev" + "dev-master": "1.0-dev" } }, "autoload": { "psr-4": { - "PhpAmqpLib\\": "PhpAmqpLib/" + "Nyholm\\Psr7\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Alvaro Videla", - "role": "Original Maintainer" + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" }, { - "name": "John Kelly", - "role": "Maintainer", - "email": "johnmkelly86@gmail.com" - }, + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "time": "2019-09-05T13:24:16+00:00" + }, + { + "name": "nyholm/psr7-server", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "1b71a848fcb066fb805b7a9ab3f41ff65bffcde8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/1b71a848fcb066fb805b7a9ab3f41ff65bffcde8", + "reference": "1b71a848fcb066fb805b7a9ab3f41ff65bffcde8", + "shasum": "" + }, + "require": { + "php": "^7.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "name": "Raúl Araya", - "role": "Maintainer", - "email": "nubeiro@gmail.com" + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" }, { - "name": "Luke Bakken", - "role": "Maintainer", - "email": "luke@bakken.io" + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" } ], - "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", - "homepage": "https://github.com/php-amqplib/php-amqplib/", + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", "keywords": [ - "message", - "queue", - "rabbitmq" + "psr-17", + "psr-7" ], - "time": "2019-08-08T18:28:18+00:00" + "time": "2018-09-02T10:41:28+00:00" }, { - "name": "pimple/pimple", - "version": "v3.2.3", + "name": "php-di/invoker", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/silexphp/Pimple.git", - "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32" + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "540c27c86f663e20fe39a24cd72fa76cdb21d41a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32", - "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/540c27c86f663e20fe39a24cd72fa76cdb21d41a", + "reference": "540c27c86f663e20fe39a24cd72fa76cdb21d41a", "shasum": "" }, "require": { - "php": ">=5.3.0", + "psr/container": "~1.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "time": "2017-03-20T19:28:22+00:00" + }, + { + "name": "php-di/php-di", + "version": "6.0.10", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "a6c813bf6b0d0bdeade3ac5a920e2c2a5b1a6ce3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/a6c813bf6b0d0bdeade3ac5a920e2c2a5b1a6ce3", + "reference": "a6c813bf6b0d0bdeade3ac5a920e2c2a5b1a6ce3", + "shasum": "" + }, + "require": { + "jeremeamia/superclosure": "^2.0", + "nikic/php-parser": "^2.0|^3.0|^4.0", + "php": ">=7.0.0", + "php-di/invoker": "^2.0", + "php-di/phpdoc-reader": "^2.0.1", "psr/container": "^1.0" }, + "provide": { + "psr/container-implementation": "^1.0" + }, "require-dev": { - "symfony/phpunit-bridge": "^3.2" + "doctrine/annotations": "~1.2", + "friendsofphp/php-cs-fixer": "^2.4", + "mnapoli/phpunit-easymock": "~1.0", + "ocramius/proxy-manager": "~2.0.2", + "phpstan/phpstan": "^0.9.2", + "phpunit/phpunit": "~6.4" + }, + "suggest": { + "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", + "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" + }, + "type": "library", + "autoload": { + "psr-4": { + "DI\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "http://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "time": "2019-10-21T11:58:24+00:00" + }, + { + "name": "php-di/phpdoc-reader", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PhpDocReader.git", + "reference": "15678f7451c020226807f520efb867ad26fbbfcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/15678f7451c020226807f520efb867ad26fbbfcf", + "reference": "15678f7451c020226807f520efb867ad26fbbfcf", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpDocReader\\": "src/PhpDocReader" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", + "keywords": [ + "phpdoc", + "reflection" + ], + "time": "2019-09-26T11:24:58+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "1.0-dev" } }, "autoload": { - "psr-0": { - "Pimple": "src/" + "psr-4": { + "Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1262,17 +1431,20 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" } ], - "description": "Pimple, a simple Dependency Injection Container", - "homepage": "http://pimple.sensiolabs.org", + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", "keywords": [ - "container", - "dependency injection" + "factory", + "http", + "message", + "stream", + "uri" ], - "time": "2018-01-21T07:42:36+00:00" + "time": "2015-12-19T14:08:53+00:00" }, { "name": "psr/container", @@ -1299,7 +1471,159 @@ }, "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1312,33 +1636,38 @@ "homepage": "http://www.php-fig.org/" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Common interface for HTTP server-side request handler", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" ], - "time": "2017-02-14T16:28:37+00:00" + "time": "2018-10-30T16:46:14+00:00" }, { - "name": "psr/http-message", + "name": "psr/http-server-middleware", "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0" }, "type": "library", "extra": { @@ -1348,7 +1677,7 @@ }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "Psr\\Http\\Server\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1361,30 +1690,31 @@ "homepage": "http://www.php-fig.org/" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", + "description": "Common interface for HTTP server-side middleware", "keywords": [ "http", - "http-message", + "http-interop", + "middleware", "psr", + "psr-15", "psr-7", "request", "response" ], - "time": "2016-08-06T14:39:51+00:00" + "time": "2018-10-30T17:12:04+00:00" }, { "name": "psr/log", - "version": "1.1.0", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", "shasum": "" }, "require": { @@ -1393,7 +1723,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -1418,44 +1748,56 @@ "psr", "psr-3" ], - "time": "2018-11-20T15:27:04+00:00" + "time": "2019-11-01T11:05:21+00:00" }, { "name": "slim/slim", - "version": "3.12.2", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "200c6143f15baa477601879b64ab2326847aac0b" + "reference": "26020e9a099e69b0b12918115894f7106364dcb7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/200c6143f15baa477601879b64ab2326847aac0b", - "reference": "200c6143f15baa477601879b64ab2326847aac0b", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/26020e9a099e69b0b12918115894f7106364dcb7", + "reference": "26020e9a099e69b0b12918115894f7106364dcb7", "shasum": "" }, "require": { - "container-interop/container-interop": "^1.2", "ext-json": "*", - "ext-libxml": "*", - "ext-simplexml": "*", - "nikic/fast-route": "^1.0", - "php": ">=5.5.0", - "pimple/pimple": "^3.0", + "nikic/fast-route": "^1.3", + "php": "^7.1", "psr/container": "^1.0", - "psr/http-message": "^1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.0", - "squizlabs/php_codesniffer": "^2.5" + "ext-simplexml": "*", + "guzzlehttp/psr7": "^1.5", + "http-interop/http-factory-guzzle": "^1.0", + "nyholm/psr7": "^1.1", + "nyholm/psr7-server": "^0.3.0", + "phpspec/prophecy": "^1.8", + "phpstan/phpstan": "^0.11.5", + "phpunit/phpunit": "^7.5", + "slim/http": "^0.7", + "slim/psr7": "^0.3", + "squizlabs/php_codesniffer": "^3.4.2", + "zendframework/zend-diactoros": "^2.1" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "slim/psr7": "Slim PSR-7 implementation. See http://www.slimframework.com/docs/v4/start/installation.html for more information." }, "type": "library", "autoload": { "psr-4": { - "Slim\\": "Slim" + "Slim\\": "Slim", + "Slim\\Tests\\": "tests" } }, "notification-url": "https://packagist.org/downloads/", @@ -1478,6 +1820,11 @@ "email": "rob@akrabat.com", "homepage": "http://akrabat.com" }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + }, { "name": "Gabriel Manricks", "email": "gmanricks@me.com", @@ -1485,113 +1832,52 @@ } ], "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", - "homepage": "https://slimframework.com", + "homepage": "https://www.slimframework.com", "keywords": [ "api", "framework", "micro", "router" ], - "time": "2019-08-20T18:46:05+00:00" - }, - { - "name": "swiftmailer/swiftmailer", - "version": "v6.2.1", - "source": { - "type": "git", - "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a", - "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a", - "shasum": "" - }, - "require": { - "egulias/email-validator": "~2.0", - "php": ">=7.0.0", - "symfony/polyfill-iconv": "^1.0", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "require-dev": { - "mockery/mockery": "~0.9.1", - "symfony/phpunit-bridge": "^3.4.19|^4.1.8" - }, - "suggest": { - "ext-intl": "Needed to support internationalized email addresses", - "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.2-dev" - } - }, - "autoload": { - "files": [ - "lib/swift_required.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Corbyn" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Swiftmailer, free feature-rich PHP mailer", - "homepage": "https://swiftmailer.symfony.com", - "keywords": [ - "email", - "mail", - "mailer" - ], - "time": "2019-04-21T09:21:45+00:00" + "time": "2019-10-05T21:24:58+00:00" }, { "name": "symfony/console", - "version": "v4.3.4", + "version": "v5.0.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "de63799239b3881b8a08f8481b22348f77ed7b36" + "reference": "dae5ef273d700771168ab889d9f8a19b2d206656" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/de63799239b3881b8a08f8481b22348f77ed7b36", - "reference": "de63799239b3881b8a08f8481b22348f77ed7b36", + "url": "https://api.github.com/repos/symfony/console/zipball/dae5ef273d700771168ab889d9f8a19b2d206656", + "reference": "dae5ef273d700771168ab889d9f8a19b2d206656", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": "^7.2.5", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", - "symfony/service-contracts": "^1.1" + "symfony/service-contracts": "^1.1|^2" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3", - "symfony/process": "<3.3" + "symfony/dependency-injection": "<4.4", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" }, "provide": { "psr/log-implementation": "1.0" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "^4.3", - "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0", - "symfony/var-dumper": "^4.3" + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" }, "suggest": { "psr/log": "For using the console logger", @@ -1602,7 +1888,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1629,37 +1915,37 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2019-08-26T08:26:39+00:00" + "time": "2019-12-01T10:51:15+00:00" }, { - "name": "symfony/polyfill-iconv", - "version": "v1.12.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.13.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "685968b11e61a347c18bf25db32effa478be610f" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f", - "reference": "685968b11e61a347c18bf25db32effa478be610f", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", "shasum": "" }, "require": { "php": ">=5.3.3" }, "suggest": { - "ext-iconv": "For best performance" + "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Iconv\\": "" + "Symfony\\Polyfill\\Mbstring\\": "" }, "files": [ "bootstrap.php" @@ -1679,108 +1965,44 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Iconv extension", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "iconv", + "mbstring", "polyfill", "portable", "shim" ], - "time": "2019-08-06T08:03:45+00:00" + "time": "2019-11-27T14:18:11+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.12.0", + "name": "symfony/polyfill-php56", + "version": "v1.13.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2" + "url": "https://github.com/symfony/polyfill-php56.git", + "reference": "53dd1cdf3cb986893ccf2b96665b25b3abb384f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2", - "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/53dd1cdf3cb986893ccf2b96665b25b3abb384f4", + "reference": "53dd1cdf3cb986893ccf2b96665b25b3abb384f4", "shasum": "" }, "require": { "php": ">=5.3.3", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php72": "^1.9" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.12-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" - ], - "time": "2019-08-06T08:03:45+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.12.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", - "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" + "symfony/polyfill-util": "~1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Polyfill\\Php56\\": "" }, "files": [ "bootstrap.php" @@ -1800,29 +2022,28 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", "polyfill", "portable", "shim" ], - "time": "2019-08-06T08:03:45+00:00" + "time": "2019-11-27T13:56:44+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.12.0", + "name": "symfony/polyfill-php73", + "version": "v1.13.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "04ce3335667451138df4307d6a9b61565560199e" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", - "reference": "04ce3335667451138df4307d6a9b61565560199e", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f", + "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f", "shasum": "" }, "require": { @@ -1831,15 +2052,18 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" + "Symfony\\Polyfill\\Php73\\": "" }, "files": [ "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1856,7 +2080,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -1864,20 +2088,20 @@ "portable", "shim" ], - "time": "2019-08-06T08:03:45+00:00" + "time": "2019-11-27T16:25:15+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.12.0", + "name": "symfony/polyfill-util", + "version": "v1.13.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188" + "url": "https://github.com/symfony/polyfill-util.git", + "reference": "964a67f293b66b95883a5ed918a65354fcd2258f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188", - "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/964a67f293b66b95883a5ed918a65354fcd2258f", + "reference": "964a67f293b66b95883a5ed918a65354fcd2258f", "shasum": "" }, "require": { @@ -1886,19 +2110,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] + "Symfony\\Polyfill\\Util\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1914,32 +2132,32 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony utilities for portability of PHP codes", "homepage": "https://symfony.com", "keywords": [ + "compat", "compatibility", "polyfill", - "portable", "shim" ], - "time": "2019-08-06T08:03:45+00:00" + "time": "2019-11-27T13:56:44+00:00" }, { "name": "symfony/service-contracts", - "version": "v1.1.6", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3" + "reference": "144c5e51266b281231e947b51223ba14acf1a749" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ea7263d6b6d5f798b56a45a5b8d686725f2719a3", - "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749", + "reference": "144c5e51266b281231e947b51223ba14acf1a749", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": "^7.2.5", "psr/container": "^1.0" }, "suggest": { @@ -1948,7 +2166,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1980,7 +2198,7 @@ "interoperability", "standards" ], - "time": "2019-08-20T14:44:19+00:00" + "time": "2019-11-18T17:27:11+00:00" } ], "packages-dev": [ @@ -2286,22 +2504,22 @@ }, { "name": "phpspec/prophecy", - "version": "1.8.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76" + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, @@ -2345,60 +2563,7 @@ "spy", "stub" ], - "time": "2019-06-13T12:50:23+00:00" - }, - { - "name": "phpunit/dbunit", - "version": "4.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/dbunit.git", - "reference": "e77b469c3962b5a563f09a2a989f1c9bd38b8615" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/e77b469c3962b5a563f09a2a989f1c9bd38b8615", - "reference": "e77b469c3962b5a563f09a2a989f1c9bd38b8615", - "shasum": "" - }, - "require": { - "ext-pdo": "*", - "ext-simplexml": "*", - "php": "^7.1", - "phpunit/phpunit": "^7.0", - "symfony/yaml": "^3.0 || ^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "PHPUnit extension for database interaction testing", - "homepage": "https://github.com/sebastianbergmann/dbunit/", - "keywords": [ - "database", - "testing", - "xunit" - ], - "abandoned": true, - "time": "2018-02-07T06:47:59+00:00" + "time": "2019-10-03T11:07:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2605,16 +2770,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a" + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a", - "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", "shasum": "" }, "require": { @@ -2650,20 +2815,20 @@ "keywords": [ "tokenizer" ], - "time": "2019-07-25T05:29:42+00:00" + "time": "2019-09-17T06:23:10+00:00" }, { "name": "phpunit/phpunit", - "version": "7.5.15", + "version": "7.5.18", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d79c053d972856b8b941bb233e39dc521a5093f0" + "reference": "fcf6c4bfafaadc07785528b06385cce88935474d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d79c053d972856b8b941bb233e39dc521a5093f0", - "reference": "d79c053d972856b8b941bb233e39dc521a5093f0", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fcf6c4bfafaadc07785528b06385cce88935474d", + "reference": "fcf6c4bfafaadc07785528b06385cce88935474d", "shasum": "" }, "require": { @@ -2734,7 +2899,7 @@ "testing", "xunit" ], - "time": "2019-08-21T07:05:16+00:00" + "time": "2019-12-06T05:14:37+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2903,16 +3068,16 @@ }, { "name": "sebastian/environment", - "version": "4.2.2", + "version": "4.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", "shasum": "" }, "require": { @@ -2952,20 +3117,20 @@ "environment", "hhvm" ], - "time": "2019-05-05T09:05:15+00:00" + "time": "2019-11-20T08:46:58+00:00" }, { "name": "sebastian/exporter", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "06a9a5947f47b3029d76118eb5c22802e5869687" + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687", - "reference": "06a9a5947f47b3029d76118eb5c22802e5869687", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", "shasum": "" }, "require": { @@ -3019,7 +3184,7 @@ "export", "exporter" ], - "time": "2019-08-11T12:43:14+00:00" + "time": "2019-09-14T09:02:43+00:00" }, { "name": "sebastian/global-state", @@ -3304,16 +3469,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.12.0", + "version": "v1.13.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4" + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", "shasum": "" }, "require": { @@ -3325,7 +3490,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { @@ -3358,66 +3523,7 @@ "polyfill", "portable" ], - "time": "2019-08-06T08:03:45+00:00" - }, - { - "name": "symfony/yaml", - "version": "v4.3.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "5a0b7c32dc3ec56fd4abae8a4a71b0cf05013686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/5a0b7c32dc3ec56fd4abae8a4a71b0cf05013686", - "reference": "5a0b7c32dc3ec56fd4abae8a4a71b0cf05013686", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "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": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2019-08-20T14:27:59+00:00" + "time": "2019-11-27T13:56:44+00:00" }, { "name": "theseer/tokenizer", @@ -3452,8 +3558,8 @@ "authors": [ { "name": "Arne Blankerts", - "role": "Developer", - "email": "arne@blankerts.de" + "email": "arne@blankerts.de", + "role": "Developer" } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", @@ -3461,31 +3567,29 @@ }, { "name": "webmozart/assert", - "version": "1.5.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", + "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", "shasum": "" }, "require": { "php": "^5.3.3 || ^7.0", "symfony/polyfill-ctype": "^1.8" }, + "conflict": { + "vimeo/psalm": "<3.6.0" + }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -3507,7 +3611,7 @@ "check", "validate" ], - "time": "2019-08-24T08:43:50+00:00" + "time": "2019-11-24T13:36:37+00:00" } ], "aliases": [], diff --git a/conf-dev/Dockerfile b/conf-dev/Dockerfile index 4b38349..6563bae 100644 --- a/conf-dev/Dockerfile +++ b/conf-dev/Dockerfile @@ -9,10 +9,9 @@ RUN apt-get update \ RUN pecl install xdebug \ && rm -rf /tmp/pear +RUN touch /var/log/xdebug_remote.log && chown www-data:www-data /var/log/xdebug_remote.log + # Install mod_rewrite RUN a2enmod rewrite -# Create doctrine_proxy folder -RUN mkdir /tmp/doctrine_proxy && chmod 777 /tmp/doctrine_proxy - -CMD ["apache2-foreground"] \ No newline at end of file +CMD ["apache2-foreground"] diff --git a/conf-dev/anis_init.sh b/conf-dev/anis_init.sh deleted file mode 100755 index b29cf08..0000000 --- a/conf-dev/anis_init.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/sh -set -e - -# Create the anis admin database (only tables) -./vendor/bin/doctrine orm:schema-tool:create - -# Add the first anis superuser -curl -d '{"email":"admin@anis.fr","password":"admin"}' -H "Content-Type: application/json" -X POST http://localhost/login/register -curl -d '{"adminsi":true,"superuser":true,"activated":true}' -H "Content-Type: application/json" -X PUT http://localhost/admin/user/admin@anis.fr - -# Add settings for admin inteface -curl -d '{"name":"search_flag","label":"Search flag"}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/select -curl -d '{"name":"search_type","label":"Search Type"}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/select -curl -d '{"name":"operator","label":"Operator"}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/select -curl -d '{"name":"renderer","label":"Renderer"}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/select -curl -d '{"name":"renderer_detail","label":"Renderer detail"}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/select - -curl -d '{"label":"ID","value":"ID","display":10,"id_settings_select":1}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"RA","value":"RA","display":20,"id_settings_select":1}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"DEC","value":"DEC","display":30,"id_settings_select":1}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option - -curl -d '{"label":"Field","value":"field","display":10,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Between","value":"between","display":20,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Select","value":"select","display":30,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Select multiple","value":"select-multiple","display":40,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Datalist","value":"datalist","display":50,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Radio","value":"radio","display":60,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Checkbox","value":"checkbox","display":70,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Between date","value":"between-date","display":80,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Date","value":"date","display":90,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Time","value":"time","display":100,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Date time","value":"date-time","display":110,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"JSON","value":"json","display":120,"id_settings_select":2}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option - -curl -d '{"label":"=","value":"eq","display":10,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"≠","value":"neq","display":20,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"between","value":"bw","display":30,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":">","value":"gt","display":40,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":">=","value":"gte","display":50,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"<","value":"lt","display":60,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"<=","value":"lte","display":70,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"like","value":"lk","display":80,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"not like","value":"nlk","display":90,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"in","value":"in","display":100,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"not in","value":"nin","display":110,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"null","value":"nl","display":120,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"not null","value":"nnl","display":130,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"json","value":"js","display":140,"id_settings_select":3}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option - -curl -d '{"label":"Image","value":"img","display":10,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Thumbnail","value":"thumbnail","display":20,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Link","value":"link","display":30,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Button","value":"btn","display":40,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Detail link","value":"detail-link","display":50,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Detail button","value":"detail-btn","display":60,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"Download","value":"download","display":70,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option -curl -d '{"label":"JSON","value":"json","display":80,"id_settings_select":4}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option - -curl -d '{"label":"Image","value":"img","display":10,"id_settings_select":5}' -H "Content-Type: application/json" -X POST http://localhost/admin/settings/option - -# Generate default metamodel -curl -d '{"name":"default","label":"Default instance","dev_mode":true,"driver":"pdo_pgsql","path":"","host":"db","port":5432,"login":"anis","password":"anis","path_proxy":"/tmp/doctrine_proxy","dbname":"anis_metamodel"}' -H "Content-Type: application/json" -X POST http://localhost/admin/instance -curl -X GET http://localhost/admin/instance/default/create-database - -# Add admin group to the default metamodel -curl -d '{"label":"Admin"}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/group - -# Add all default families -curl -d '{"label":"Default dataset family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/family/dataset -curl -d '{"label":"Default criteria family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/family/criteria -curl -d '{"label":"Default output family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/family/output -curl -d '{"label":"Default output category","display":10,"id_output_family":1}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/output-category \ No newline at end of file diff --git a/conf-dev/dev-init.sh b/conf-dev/dev-meta.sh similarity index 79% rename from conf-dev/dev-init.sh rename to conf-dev/dev-meta.sh index f178091..9db6662 100644 --- a/conf-dev/dev-init.sh +++ b/conf-dev/dev-meta.sh @@ -1,8 +1,9 @@ #!/bin/sh set -e -curl -d '{"label":"Test","dbname":"anis_test","dbtype":"pdo_pgsql","dbhost":"db","dbport":5432,"dblogin":"anis","dbpassword":"anis"}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/database -curl -d '{"name":"anis_project","label":"Anis Project Test","description":"Project used for testing","link":"http://project.com","manager":"M. Durand","id_database":1}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/project +curl -d '{"label":"Test","dbname":"anis_test","dbtype":"pdo_pgsql","dbhost":"db","dbport":5432,"dblogin":"anis","dbpassword":"anis"}' -H "Content-Type: application/json" -X POST http://localhost/database +curl -d '{"name":"anis_project","label":"Anis Project Test","description":"Project used for testing","link":"http://project.com","manager":"M. Durand","id_database":1}' -H "Content-Type: application/json" -X POST http://localhost/project -curl -d '{"name":"obs_cat","table_ref":"obs_cat","label":"ObsCat dataset","description":"ObsCat","display":"10","count":"10000","vo":false,"data_path":"/mnt/mount","selectable_row":true,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/dataset -curl -d '{"name":"observations","table_ref":"observations_info","label":"Observations dataset","description":"Observations","display":"20","count":"177454","vo":false,"data_path":"/mnt/mount","selectable_row":false,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/metadata/default/dataset +curl -d '{"label":"Default dataset family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/dataset +curl -d '{"name":"obs_cat","table_ref":"obs_cat","label":"ObsCat dataset","description":"ObsCat","display":"10","count":"10000","vo":false,"data_path":"/mnt/mount","selectable_row":true,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/dataset +curl -d '{"name":"observations","table_ref":"observations_info","label":"Observations dataset","description":"Observations","display":"20","count":"177454","vo":false,"data_path":"/mnt/mount","selectable_row":false,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/dataset diff --git a/conf-dev/generate_encryption_key.php b/conf-dev/generate_encryption_key.php deleted file mode 100644 index 6ca7ab6..0000000 --- a/conf-dev/generate_encryption_key.php +++ /dev/null @@ -1,2 +0,0 @@ -<?php -echo base64_encode(openssl_random_pseudo_bytes(32)) . PHP_EOL; diff --git a/conf-dev/init-postgres.sh b/conf-dev/init-postgres.sh index c3c2083..ef67c76 100644 --- a/conf-dev/init-postgres.sh +++ b/conf-dev/init-postgres.sh @@ -3,11 +3,10 @@ set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE USER anis LOGIN PASSWORD 'anis'; - CREATE DATABASE anis_admin; CREATE DATABASE anis_metamodel; CREATE DATABASE anis_test; GRANT ALL PRIVILEGES ON DATABASE anis_metamodel TO anis; GRANT ALL PRIVILEGES ON DATABASE anis_test TO anis; EOSQL psql -v ON_ERROR_STOP=1 -f /sql/obs_cat.sql --username "anis" --dbname "anis_test" -psql -v ON_ERROR_STOP=1 -f /sql/observations_info.sql --username "anis" --dbname "anis_test" \ No newline at end of file +psql -v ON_ERROR_STOP=1 -f /sql/observations_info.sql --username "anis" --dbname "anis_test" diff --git a/conf-dev/vhost.conf b/conf-dev/vhost.conf index 5723255..723b9ec 100644 --- a/conf-dev/vhost.conf +++ b/conf-dev/vhost.conf @@ -1,7 +1,7 @@ <VirtualHost *:80> - DocumentRoot /srv/app/public + DocumentRoot /project/public - <Directory "/srv/app/public"> + <Directory "/project/public"> Require all granted RewriteEngine on RewriteRule ^.+$ index.php [L] @@ -9,4 +9,4 @@ ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined -</VirtualHost> \ No newline at end of file +</VirtualHost> diff --git a/docker-compose.yml b/docker-compose.yml index 8491513..704697d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,37 +3,28 @@ version: '3' services: php: build: conf-dev - working_dir: /srv/app + working_dir: /project environment: docker: "true" - SLIM_DISPLAY_ERROR_DETAILS: "true" - METADATA_DOCTRINE_PATH_PROXY: "/tmp/doctrine_proxy" - METADATA_DOCTRINE_DEV_MODE: "true" - METADATA_DB_DRIVER: "pdo_pgsql" - METADATA_DB_HOST: "db" - METADATA_DB_PORT: 5432 - METADATA_DB_DBNAME: "anis_admin" - METADATA_DB_USER: "anis" - METADATA_DB_PASSWORD: "anis" - MAILER_HOST: "mailer" - MAILER_PORT: 25 - LOGGER_NAME: "anis-v3-server" - LOGGER_PATH: "php://stdout" + DISPLAY_ERROR_DETAILS: 'true' + DATABASE_PATH_PROXY: "/tmp/doctrine_proxy" + DATABASE_DEV_MODE: "true" + DATABASE_CO_DRIVER: "pdo_pgsql" + DATABASE_CO_HOST: "db" + DATABASE_CO_PORT: 5432 + DATABASE_CO_DBNAME: "anis_metamodel" + DATABASE_CO_USER: "postgres" + DATABASE_CO_PASSWORD: "postgres" + LOGGER_NAME: "anis-metamodel" + LOGGER_PATH: "php://stderr" LOGGER_LEVEL: "debug" - AMQP_HOST: ${AMQP_HOST} - AMQP_PORT: 5672 - AMQP_USER: "guest" - AMQP_PASSWORD: "guest" ports: - - 8080:80 + - 8082:80 volumes: - - .:/srv/app + - .:/project - ./conf-dev/dev-php.ini:/usr/local/etc/php/conf.d/dev-php.ini - ./conf-dev/vhost.conf:/etc/apache2/sites-available/000-default.conf - mailer: - image: djfarrelly/maildev - ports: - - 1080:80 + db: image: postgres environment: @@ -43,10 +34,6 @@ services: - ./conf-dev/obs_cat.sql:/sql/obs_cat.sql - ./conf-dev/observations_info.sql:/sql/observations_info.sql - ./conf-dev/init-postgres.sh:/docker-entrypoint-initdb.d/init-postgres.sh - adminer: - image: adminer - ports: - - 8083:8080 volumes: - pgdata: + pgdata: \ No newline at end of file diff --git a/documentation/api/activate-account.md b/documentation/api/activate-account.md deleted file mode 100644 index a9907b5..0000000 --- a/documentation/api/activate-account.md +++ /dev/null @@ -1,100 +0,0 @@ -# Enregistrer un nouvel utilisateur ----- -1. Permet d'activer un nouvel utilisateur ANIS (`GET`) - -### URL - -- /activate-account - -### Method - -- `OPTIONS` -- `GET` - -### Headers - -None - -### URL Params - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :--------------------------------------------------------- | :----------: | -| email | string | Adresse email du nouvel utilisateur | [x] | -| activation_key | string | Clé d'activation qui permet de valider l'utilisateur | [x] | - -### Data Params - -None - -### Success Response - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "email": "user1@anis.fr", - "activated": true, - "adminsi": false, - "superuser": false, - "id_group": 1 - } -} -``` - -### Error Response - -1. Si le paramètre email est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param email needed to activate a new user account" -} -``` - -2. Si le paramètre activation key est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param activation_key needed to activate a new user account" -} -``` - -3. Si l'utilisateur identifié par l'email envoyé dans la requête n'éxiste pas dans la base de données - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: No account is identified with this email address" -} -``` - -4. Si la clé d'activation envoyée par l'utilisateur n'est pas valide - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid activation key", - "error_description": "HTTP 400: Bad activation key; Unable to activate account" -} -``` - -### Exemple d'utilisation - -1. Enregistrer un nouvel utilisateur - -> curl -X GET http://localhost:8989/activate-account?email=user1@anis.fr&activation_key=noioioi diff --git a/documentation/api/category-list.md b/documentation/api/category-list.md deleted file mode 100644 index 3e56ade..0000000 --- a/documentation/api/category-list.md +++ /dev/null @@ -1,93 +0,0 @@ -# Metadata: Category collection ----- -1. Permet de récupérer l'ensemble des category disponibles dans la base de données metadata (`GET`) -2. Permet également de créer une nouvelle category (`POST`) - -### URL - -- /metadata/category - -### Method - -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la category à créer | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "id": 3, - "label": "Default" - }, { - "id": 4, - "label": "Instrumental settings" - }, { - "id": 5, - "label": "Photometry" - }] -} -``` - -2. `POST` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 9, - "label": "My category" - } -} -``` - -### Error Response - -1. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new category" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des databases metadata - -> curl -X GET http://localhost:8989/metadata/category - -2. Ajouter une nouvelle database metadata - -> curl -d '{"label":"My category"}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/category diff --git a/documentation/api/category.md b/documentation/api/category.md deleted file mode 100644 index 42d3182..0000000 --- a/documentation/api/category.md +++ /dev/null @@ -1,122 +0,0 @@ -# Metadata: Category item ----- -1. Permet de récupérer la category qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement la category qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer la category qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/category/:id - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :--- | :--------------------------| -| id | int | Identifiant de la category | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la catégory à modifier | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 3, - "label": "Default category" - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 9, - "label": "My modified category" - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Category with id 1 is removed!" -} -``` - -### Error Response - -1. Si la category avec l'id passé en paramètre est inconnu - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Category with id 15 is not found" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to edit the category" -} -``` - -### Exemples d'utilisation - -1. Récuperer la category qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/category/9 - -2. Modifier la category qui correspond à l'identifiant passé en paramètre - -> curl -d '{"label":"My modified category"}' -H "Content-Type: application/json" -X PUT http://localhost:8989/metadata/category/9 - -3. Supprimer la category qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/category/9 diff --git a/documentation/api/change-password.md b/documentation/api/change-password.md deleted file mode 100644 index f1c7cf6..0000000 --- a/documentation/api/change-password.md +++ /dev/null @@ -1,119 +0,0 @@ -# Changement du mot de passe utilisateur ----- -1. Permet à l'utilisateur de changer son mot de passe de login (`POST`) - -### URL - -- /change-password - -### Method - -- `OPTIONS` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------ | :----------: | -| email | string | Adresse email de l'utilisateur | [x] | -| password | string | Mot de passe de l'utilisateur | [x] | -| new_password | string | Nouveau mot de passe de l'utilisateur | [x] | - -### Success Response - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Password changed!" -} -``` - -### Error Response - -1. Si le paramètre email est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param email needed to change your password" -} -``` - -2. Si le paramètre password est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param password needed to change your password" -} -``` - -3. Si le paramètre new_password est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param new_password needed to change your password" -} -``` - -4. Si l'utilisateur identifié par l'email envoyé dans la requête n'éxiste pas dans la base de données - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: No account is identified with this email address" -} -``` - -5. Si le compte utilisateur n'est pas encore activé - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: Account not yet activated" -} -``` - -6. Si l'utilisateur a renseigné un mauvais mot de passe - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: Bad password; unable to change the password" -} -``` - -### Exemple d'utilisation - -1. Changer son mot de passe - -> curl -d '{"email":"admin@anis.fr", "password":"5b3f576db200c", "new_password":"admin"}' -H "Content-Type: application/json" -X POST http://localhost:8989/change-password diff --git a/documentation/api/database-list.md b/documentation/api/database-list.md deleted file mode 100644 index 3569c55..0000000 --- a/documentation/api/database-list.md +++ /dev/null @@ -1,194 +0,0 @@ -# Metadata: Database collection ----- -1. Permet de récupérer l'ensemble des databases disponibles dans la base de données metadata (`GET`) -2. Permet également de créer une nouvelle database (`POST`) - -### URL - -- /metadata/database - -### Method - -- `OPTIONS` -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la database à créer | [x] | -| dname | string | Le nom de la base de données | [x] | -| dbtype | string | Le SGBD qui contient la base de données | [x] | -| dbhost | string | Le nom de la machine hôte ou est installé le SGBD | [x] | -| dbport | int | Le port d'écoute du SGBD | [x] | -| dblogin | string | Le login de connexion vers la base de données | [x] | -| dbpassword | string | Le password de connexion vers la base de données | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "id": 16, - "label": "Exodat", - "dbname": "exodat_new", - "dbtype": "pgsql", - "dbhost": "cesamsidb", - "dbport": 5432, - "dblogin": "consult", - "dbpassword": "consult" - }, { - "id": 17, - "label": "db", - "dbname": "anis_v3_database", - "dbtype": "pgsql", - "dbhost": "db", - "dbport": 5432, - "dblogin": "anis", - "dbpassword": "anis" - }, { - "id": 18, - "label": "Cosmologydb", - "dbname": "cosmologydb", - "dbtype": "pgsql", - "dbhost": "cesamsidb", - "dbport": 5432, - "dblogin": "consult", - "dbpassword": "consult" - }] -} -``` - -2. `POST` - -**Code:** 201 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "id": 16, - "label": "Exodat", - "dbname": "exodat_new", - "dbtype": "pgsql", - "dbhost": "cesamsidb", - "dbport": 5432, - "dblogin": "consult", - "dbpassword": "consult" -} -``` - -### Error Response - -1. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new database" -} -``` - -2. Si le paramètre dbname est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbname needed to add a new database" -} -``` - -3. Si le paramètre dbtype est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbtype needed to add a new database" -} -``` - -4. Si le paramètre dbhost est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbhost needed to add a new database" -} -``` - -5. Si le paramètre dbport est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbport needed to add a new database" -} -``` - -6. Si le paramètre dblogin est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dblogin needed to add a new database" -} -``` - -7. Si le paramètre dbpassword est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbpassword needed to add a new database" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des databases metadata - -> curl -X GET http://localhost:8989/metadata/database - -2. Ajouter une nouvelle database metadata - -> curl -d '{"label":"Test","dbname":"test","dbtype":"pgsql","dbhost":"cesamsidb","dbport":5432,"dblogin":"consult","dbpassword":"consult"}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/database diff --git a/documentation/api/database.md b/documentation/api/database.md deleted file mode 100644 index 7294469..0000000 --- a/documentation/api/database.md +++ /dev/null @@ -1,212 +0,0 @@ -# Metadata: Database item ----- -1. Permet de récupérer la database qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement la database qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer la database qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/database/:id - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :--- | :--------------------------| -| id | int | Identifiant de la database | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la database à créer | [x] | -| dname | string | Le nom de la base de données | [x] | -| dbtype | string | Le SGBD qui contient la base de données | [x] | -| dbhost | string | Le nom de la machine hôte ou est installé le SGBD | [x] | -| dbport | int | Le port d'écoute du SGBD | [x] | -| dblogin | string | Le login de connexion vers la base de données | [x] | -| dbpassword | string | Le password de connexion vers la base de données | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 19, - "label": "Test", - "dbname": "test", - "dbtype": "pgsql", - "dbhost": "cesamsidb", - "dbport": 5432, - "dblogin": "consult", - "dbpassword": "consult" - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 19, - "label": "Test", - "dbname": "test", - "dbtype": "pgsql", - "dbhost": "cesamsidb", - "dbport": 5432, - "dblogin": "consult", - "dbpassword": "consult" - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Database with id 1 is removed!" -} -``` - -### Error Response - -1. Si la database avec l'id passé en paramètre est inconnu - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Database with id 15 is not found" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to edit the database" -} -``` - -3. Si le paramètre dbname est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbname needed to edit the database" -} -``` - -4. Si le paramètre dbtype est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbtype needed to edit the database" -} -``` - -5. Si le paramètre dbhost est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbhost needed to edit the database" -} -``` - -6. Si le paramètre dbport est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbport needed to edit the database" -} -``` - -7. Si le paramètre dblogin est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dblogin needed to edit the database" -} -``` - -8. Si le paramètre dbpassword est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param dbpassword needed to edit the database" -} -``` - -### Exemples d'utilisation - -1. Récuperer la database qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/database/16 - -2. Modifier la database qui correspond à l'identifiant passé en paramètre - -> curl -d '{"label":"Test_modif","dbname":"test","dbtype":"pgsql","dbhost":"cesamsidb","dbport":5432,"dblogin":"consult","dbpassword":"consult"}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/database/19 - -3. Supprimer la database qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/database/19 diff --git a/documentation/api/dataset-list.md b/documentation/api/dataset-list.md deleted file mode 100644 index d05fe6d..0000000 --- a/documentation/api/dataset-list.md +++ /dev/null @@ -1,278 +0,0 @@ -# Metadata: Dataset collection ----- -1. Permet de récupérer l'ensemble des datasets disponibles dans la base de données metadata (`GET`) -2. Permet également de créer une nouveau dataset (`POST`) - -### URL - -- /metadata/dataset - -### Method - -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -| Nom du paramètre | Type | Description | Obligatoire | -| :---------------- | :----- | :----------------------------------------------------------------- | :----------: | -| name | string | Nom qui identifie le dataset | [x] | -| table_ref | string | Table ou vue de référence pour le dataset | [x] | -| label | string | Label du dataset | [x] | -| description | text | Description du dataset | [x] | -| illustration | string | Image stocké dans le data_pah et qui permet d'illustrer le dataset | [x] | -| display | int | Numéro d'ordre d'apparation du dataset dans sa famille | [x] | -| count | int | Nom d'enregistrements dans le dataset | [x] | -| vo | int | Permet d'activer les fonctionnalités VO pour le dataset | [x] | -| data_path | int | Chemin vers le stockage des fichiers associés au dataset | [x] | -| project_name | int | Nom qui identifie le projet qui contiendra le dataset | [x] | -| id_dataset_family | int | Identifiant de la famille ou sera rangé le dataset | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "name": "exo2mass_cat", - "table_ref": "v_exo2mass_joins", - "label": "Stars in the CoRoT eyes", - "description": "It is the reference catalog manufactured by exodat team used to prepare the next observations (EXO2MASS).", - "display": 10, - "count": 65865234, - "vo": true, - "data_path": "", - "project_name": "exodat", - "id_dataset_family": 4 - }, { - "name": "aspic_vuds_dr1_cosmos", - "table_ref": "aspic_vuds_dr1_cosmos", - "label": "VUDS-DR1-COSMOS", - "description": "VUDS-COSMOS Spectroscopic catalogue", - "display": 10, - "count": 384, - "vo": true, - "data_path": "\/mnt\/data-path", - "project_name": "aspic", - "id_dataset_family": 6 - }, { - "name": "corot_targets", - "table_ref": "v_corot_targets", - "label": "CoRoT Targets", - "description": "Information about targets observed by the CoRoT mission", - "display": 30, - "count": 177454, - "vo": false, - "data_path": "", - "project_name": "exodat", - "id_dataset_family": 5 - }, { - "name": "obscat", - "table_ref": "v_obs_cat", - "label": "ObsCat", - "description": "CoRoT stars observations", - "display": 30, - "count": 11170033, - "vo": true, - "data_path": "\/mnt\/data-path", - "project_name": "exodat", - "id_dataset_family": 4 - }] -} -``` - -2. `POST` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "name": "my_dataset", - "table_ref": "my_table", - "label": "Mon super dataset", - "description": "Une description compl\u00e8te du dataset", - "display": 150, - "count": 550, - "vo": false, - "data_path": "\/mnt\/mount", - "project_name": "aspic", - "id_dataset_family": 5 - } -} -``` - -### Error Response - -1. Si le nom qui identifie le projet à associer au dataset est inconnu (`POST`) - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Project with name undifined is not found" -} -``` - -2. Si l'identifiant de la famille à associer au dataset est inconnu (`POST`) - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Dataset family with id 15 is not found" -} -``` - -3. Si le paramètre name est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param name needed to add a new dataset" -} -``` - -4. Si le paramètre table_ref est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param table_ref needed to add a new dataset" -} -``` - -5. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new dataset" -} -``` - -6. Si le paramètre description est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param description needed to add a new dataset" -} -``` - -7. Si le paramètre display est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param display needed to add a new dataset" -} -``` - -8. Si le paramètre count est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param count needed to add a new dataset" -} -``` - -9. Si le paramètre vo est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param vo needed to add a new dataset" -} -``` - -10. Si le paramètre data_path est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param data_path needed to add a new dataset" -} -``` - -11. Si le paramètre project_name est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param project_name needed to add a new dataset" -} -``` - -12. Si le paramètre id_dataset_family est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param id_dataset_family needed to add a new dataset" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des datasets - -> curl -X GET http://localhost:8989/metadata/dataset - -2. Ajouter une nouveau projet - -> curl -d '{"name":"my_dataset","table_ref":"my_table","label":"Mon super dataset","description":"Une description complète du dataset","display":"150","count":"550","vo":false,"data_path":"/mnt/mount","project_name":"aspic","id_dataset_family":5}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/dataset diff --git a/documentation/api/dataset.md b/documentation/api/dataset.md deleted file mode 100644 index c9e5486..0000000 --- a/documentation/api/dataset.md +++ /dev/null @@ -1,217 +0,0 @@ -# Metadata: Dataset item ----- -1. Permet de récupérer le dataset qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement le dataset qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer le dataset qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/dataset/:name - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :--- | :--------------------------| -| name | int | Identifiant du dataset | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -| Nom du paramètre | Type | Description | Obligatoire | -| :---------------- | :----- | :----------------------------------------------------------------- | :----------: | -| label | string | Label du dataset | [x] | -| description | text | Description du dataset | [x] | -| display | int | Numéro d'ordre d'apparation du dataset dans sa famille | [x] | -| count | int | Nom d'enregistrements dans le dataset | [x] | -| vo | int | Permet d'activer les fonctionnalités VO pour le dataset | [x] | -| data_path | int | Chemin vers le stockage des fichiers associés au dataset | [x] | -| project_name | int | Nom qui identifie le projet qui contiendra le dataset | [x] | -| id_dataset_family | int | Identifiant de la famille ou sera rangé le dataset | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "name": "my_dataset", - "table_ref": "my_table", - "label": "Mon super dataset", - "description": "Une description compl\u00e8te du dataset", - "display": 150, - "count": 550, - "vo": false, - "data_path": "\/mnt\/mount", - "project_name": "aspic", - "id_dataset_family": 5 - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "name": "my_dataset", - "table_ref": "my_table", - "label": "Mon super dataset", - "description": "Une description compl\u00e8te du dataset", - "display": 150, - "count": 550, - "vo": false, - "data_path": "\/mnt\/mount", - "project_name": "aspic", - "id_dataset_family": 5 - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Dataset with name corot_targets is removed!" -} -``` - -### Error Response - -1. Si l'identifiant de la famille à associer au dataset est inconnu (`POST`) - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Dataset family with id 15 is not found" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to edit the dataset" -} -``` - -3. Si le paramètre description est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param description needed to edit the dataset" -} -``` - -4. Si le paramètre display est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param display needed to edit the dataset" -} -``` - -5. Si le paramètre count est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param count needed to edit the dataset" -} -``` - -6. Si le paramètre vo est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param vo needed to edit the dataset" -} -``` - -7. Si le paramètre data_path est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param data_path needed to edit the dataset" -} -``` - -8. Si le paramètre id_dataset_family est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param id_dataset_family needed to edit the dataset" -} -``` - -### Exemples d'utilisation - -1. Récuperer le dataset qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/dataset/corot_targets - -2. Modifier le dataset qui correspond à l'identifiant passé en paramètre - -> curl -d '{"label":"Mon super dataset","description":"Une description complète du dataset","display":"150","count":"550","vo":false,"data_path":"/mnt/mount","id_dataset_family":5}' -H "Content-Type: application/json" -X PUT http://localhost:8989/metadata/dataset/19 - -3. Supprimer le dataset qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/dataset/corot_targets diff --git a/documentation/api/family-list.md b/documentation/api/family-list.md deleted file mode 100644 index 6474ca2..0000000 --- a/documentation/api/family-list.md +++ /dev/null @@ -1,143 +0,0 @@ -# Metadata: Family collection ----- -1. Permet de récupérer l'ensemble des familles disponibles dans la base de données metadata (`GET`) -2. Permet également de créer une nouvelle famille (`POST`) - -### URL - -- /metadata/family/:type - -### Method - -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :------ | :------------------------- | -| type | string | Type de famille concerné par la requête | - -- 3 types sont possibles : - 1. `dataset` - 2. `output` - 3. `criteria` - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -- Dans le cas des types `criteria` et `output` - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la famille à créer | [x] | - -- Dans le cas du type `dataset` - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la famille à créer | [x] | -| description | text | La description de la famille de dataset à créer | [x] | -| display | int | Position de la famille dans l'ordre d'affichage | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "id": 1, - "label": "Criteria by default" - }, { - "id": 2, - "label": "Photometry" - }] -} -``` - -2. `POST` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "id": 1, - "label": "Spectroscopy" - }] -} -``` - -### Error Response - -1. Si l'argument type est inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "Type undifined is not defined" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new family" -} -``` - -3. Si le paramètre description est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param description needed to add a new family" -} -``` - -4. Si le paramètre display est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param display needed to add a new family" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des familles de criteria - -> curl -X GET http://localhost:8989/metadata/family/criteria - -2. Ajouter une nouvelle famille de criteria - -> curl -d '{"label":"New criteria Family"}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/family/criteria diff --git a/documentation/api/family.md b/documentation/api/family.md deleted file mode 100644 index fa8b522..0000000 --- a/documentation/api/family.md +++ /dev/null @@ -1,177 +0,0 @@ -# Metadata: Family item ----- -1. Permet de récupérer la famille qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement la famille qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer la famille qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/family/:type/:id - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :------ | :------------------------- | -| type | string | Type de famille concerné par la requête | -| id | int | Identifiant de la famille | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -- Dans le cas des types `criteria` et `output` - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la famille à créer | [x] | - -- Dans le cas du type `dataset` - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label de la famille à créer | [x] | -| description | text | La description de la famille de dataset à créer | [x] | -| display | int | Position de la famille dans l'ordre d'affichage | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 1, - "label": "VUDS dataset collection", - "description": "VUDS datasets description", - "display": 10, - "type": "dataset", - "datasets": [] - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 1, - "label": "Edited family", - "description": "Edited Family description", - "display": 40, - "type": "dataset", - "datasets": [] - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Dataset family with id 1 is removed!" -} -``` - -### Error Response - -1. Si l'argument type est inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "Type undifined is not defined" -} -``` - -2. Si la famille avec l'identifiant passé en paramètre est introuvable - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Dataset family with id 15 is not found" -} -``` - -3. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new family" -} -``` - -4. Si le paramètre description est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param description needed to add a new family" -} -``` - -5. Si le paramètre display est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param display needed to add a new family" -} -``` - -### Exemples d'utilisation - -1. Récuperer la famille de criteria qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/family/criteria/5 - -2. Modifier la famille de criteria qui correspond à l'identifiant passé en paramètre - -> curl -d '{"label":"My criteria family"}' -H "Content-Type: application/json" -X PUT http://localhost:8989/metadata/family/criteria/5 - -3. Supprimer la famille de criteria qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/family/criteria/5 diff --git a/documentation/api/file-list.md b/documentation/api/file-list.md deleted file mode 100644 index 47e6c43..0000000 --- a/documentation/api/file-list.md +++ /dev/null @@ -1,168 +0,0 @@ -# Metadata: File collection ----- -1. Permet de récupérer l'ensemble des fichiers disponibles pour un dataset dans la base de données metadata (`GET`) -2. Permet également de créer une nouveau fichier pour un dataset (`POST`) - -### URL - -- /metadata/dataset/:name/file - -### Method - -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :------ | :------------------------- | -| name | string | Identifiant du dataset | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :-------------------------------------------------------------- | :----------: | -| label | string | Permet de définir un label pour le fichier | [x] | -| file_loc | string | Chemin vers le fichier | [x] | -| type | string | Type de fichier référencé (txt, csv, fits...) | [x] | -| display | int | Numéro d'ordre d'apparation du fichier | [x] | -| visible | bool | Permet d'afficher ou non le fichier dans l'inteface utilisateur | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "id": 1, - "label": "My file", - "file_loc": "my_file.txt", - "type": "txt", - "display": 10, - "visible": true - }, { - "id": 2, - "label": "My fits", - "file_loc": "my_fits.fits", - "type": "fits", - "display": 20, - "visible": true - }] -} -``` - -2. `POST` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 3, - "label": "My new fits", - "file_loc": "my_new_fits.fits", - "type": "fits", - "display": 30, - "visible": true - } -} -``` - -### Error Response - -1. Si l'identifiant du dataset est inconnu (name) - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Dayasey with name undifined is not found" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new file" -} -``` - -3. Si le paramètre file_loc est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param file_loc needed to add a new file" -} -``` - -4. Si le paramètre type est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param type needed to add a new file" -} -``` - -5. Si le paramètre display est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param display needed to add a new file" -} -``` - -6. Si le paramètre visible est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param visible needed to add a new file" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des fichiers d'un dataset - -> curl -X GET http://localhost:8989/metadata/dataset/corot_targets/file - -2. Ajouter une nouveau fichier au dataset corot_targets - -> curl -d '{"label":"my_new_fits","file_loc":"my_new_fits.fits","type":"fits","display":30,"visible":true}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/dataset/corot_targets/file diff --git a/documentation/api/file.md b/documentation/api/file.md deleted file mode 100644 index 0585aae..0000000 --- a/documentation/api/file.md +++ /dev/null @@ -1,184 +0,0 @@ -# Metadata: File item ----- -1. Permet de récupérer le fichier qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement le fichier qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer le fichier qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/file/:id - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :--- | :--------------------------| -| id | int | Identifiant du fichier | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :-------------------------------------------------------------- | :----------: | -| label | string | Permet de définir un label pour le fichier | [x] | -| file_loc | string | Chemin vers le fichier | [x] | -| type | string | Type de fichier référencé (txt, csv, fits...) | [x] | -| display | int | Numéro d'ordre d'apparation du fichier | [x] | -| visible | bool | Permet d'afficher ou non le fichier dans l'inteface utilisateur | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 1, - "label": "My file", - "file_loc": "my_file.txt", - "type": "txt", - "display": 10, - "visible": true, - "dataset_name": "corot_targets" - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 1, - "label": "My edited file", - "file_loc": "my_edited_file.txt", - "type": "txt", - "display": 20, - "visible": false, - "dataset_name": "corot_targets" - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "File with id 1 is removed!" -} -``` - -### Error Response - -1. Si le fichier avec l'id passé en paramètre est inconnu - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: File with id 15 is not found" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to edit the file" -} -``` - -3. Si le paramètre file_loc est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param file_loc needed to edit the file" -} -``` - -4. Si le paramètre type est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param type needed to edit the file" -} -``` - -5. Si le paramètre display est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param display needed to edit the file" -} -``` - -6. Si le paramètre visible est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param visible needed to edit the file" -} -``` - -### Exemples d'utilisation - -1. Récuperer le fichier qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/file/16 - -2. Modifier le fichier qui correspond à l'identifiant passé en paramètre - -> curl -d '{"label":"Test_modif","file_loc":"my_edited_file","type":"txt","display":"20","visible":true}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/file/16 - -3. Supprimer la database qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/file/16 diff --git a/documentation/api/group-list.md b/documentation/api/group-list.md deleted file mode 100644 index 0852d0b..0000000 --- a/documentation/api/group-list.md +++ /dev/null @@ -1,89 +0,0 @@ -# Metadata: Group collection ----- -1. Permet de récupérer l'ensemble des groupes d'utilisateur disponibles dans la base de données metadata (`GET`) -2. Permet également de créer un nouveau groupe (`POST`) - -### URL - -- /metadata/group - -### Method - -- `OPTIONS` -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :------------------------------------------------ | :----------: | -| label | string | Le label du groupe à créer | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "id": 1, - "label": "Default" - }, { - "id": 2, - "label": "Admin" - }] -} -``` - -2. `POST` - -**Code:** 201 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "id": 3, - "label": "Student" -} -``` - -### Error Response - -1. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new group" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des groupes dans la base de metadata - -> curl -X GET http://localhost:8989/metadata/group - -2. Ajouter une nouveau groupe metadata - -> curl -d '{"label":"New group"}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/group diff --git a/documentation/api/group.md b/documentation/api/group.md deleted file mode 100644 index 27cb924..0000000 --- a/documentation/api/group.md +++ /dev/null @@ -1,122 +0,0 @@ -# Metadata: Group item ----- -1. Permet de récupérer le groupe qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement le groupe qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer le groupe qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/group/:id - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :--- | :--------------------------| -| id | int | Identifiant du groupe | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :-------------------------------------------------------------- | :----------: | -| label | string | Permet de définir un label pour le groupe | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 1, - "label": "Default" - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "id": 1, - "label": "My edited group" - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Group with id 1 is removed!" -} -``` - -### Error Response - -1. Si le groupe avec l'id passé en paramètre est inconnu - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Groupe with id 15 is not found" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to edit the group" -} -``` - -### Exemples d'utilisation - -1. Récuperer le groupe qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/group/16 - -2. Modifier le groupe qui correspond à l'identifiant passé en paramètre - -> curl -d '{"label":"Test_modif"}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/group/16 - -3. Supprimer le groupe qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/group/16 diff --git a/documentation/api/login.md b/documentation/api/login.md deleted file mode 100644 index 35883f2..0000000 --- a/documentation/api/login.md +++ /dev/null @@ -1,109 +0,0 @@ -# Récupérer un jeton d'authentification ----- -1. Permet à l'utilisateur de récupérer un jeton d'authentification avec son email et son password - -### URL - -- /login - -### Method - -- `OPTIONS` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :--------------------------------------------------------- | :----------: | -| email | string | Adresse email de l'utilisateur enregistré | [x] | -| password | string | Mot de passe de l'utilisateur | [x] | - -### Success Response - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "email": "admin@anis.fr", - "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9hbmlzLmxhbS5mciIsImF1ZCI6Imh0dHA6XC9cL2FuaXMubGFtLmZyIiwiaWF0IjoxNTMwODAyMTk4LCJuYmYiOjE1MzA4MDIxOTgsImV4cCI6MTUzMDgzODE5OCwiZW1haWwiOiJhZG1pbkBhbmlzLmZyIn0.WM4gvKY1HwmLBPRtwa87yptpipCkc8l3xqSiMxGJ_Ig" - } -} -``` - -### Error Response - -1. Si le paramètre email est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param email needed to login" -} -``` - -2. Si le paramètre password est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param password needed to login" -} -``` - -3. Si l'utilisateur identifié par l'email envoyé dans la requête n'éxiste pas dans la base de données - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: No account is identified with this email address" -} -``` - -4. Si le compte utilisateur n'est pas encore activé - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: Account not yet activated" -} -``` - -5. Si l'utilisateur a renseigné un mauvais mot de passe - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: Bad password; unable to login" -} -``` - -### Exemple d'utilisation - -1. Récupérer un jeton d'authentification - -> curl -d '{"email":"admin@anis.fr","password":"admin"}' -H "Content-Type: application/json" -X POST http://localhost:8989/login diff --git a/documentation/api/new-password.md b/documentation/api/new-password.md deleted file mode 100644 index 5dc13f2..0000000 --- a/documentation/api/new-password.md +++ /dev/null @@ -1,81 +0,0 @@ -# Demander la génération d'un nouveau mot de passe ----- -1. Permet de demander la génération d'un nouveau mot de passe qui sera envoyé par email à l'utilisateur (`POST`) - -### URL - -- /new-password - -### Method - -- `OPTIONS` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :---------------------------------- | :----------: | -| email | string | Adresse email de l'utilisateur | [x] | - -### Success Response - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Password re-generated!" -} -``` - -### Error Response - -1. Si le paramètre email est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param email needed to generate a new password" -} -``` - -2. Si l'utilisateur identifié par l'email envoyé dans la requête n'éxiste pas dans la base de données - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: No account is identified with this email address" -} -``` - -3. Si le compte utilisateur n'est pas encore activé - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: Account not yet activated" -} -``` - -### Exemple d'utilisation - -1. Demander la génération d'un nouveau mot de passe - -> curl -d '{"email":"admin@anis.fr"}' -H "Content-Type: application/json" -X POST http://localhost:8989/new-password diff --git a/documentation/api/project-list.md b/documentation/api/project-list.md deleted file mode 100644 index de1b1d7..0000000 --- a/documentation/api/project-list.md +++ /dev/null @@ -1,179 +0,0 @@ -# Metadata: Project collection ----- -1. Permet de récupérer l'ensemble des projets disponibles dans la base de données metadata (`GET`) -2. Permet également de créer une nouveau projet (`POST`) - -### URL - -- /metadata/project - -### Method - -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :--------------------------------------------------------- | :----------: | -| name | string | Identifiant du projet à créé | [x] | -| label | string | Permet de définir un label pour le projet | [x] | -| description | text | Description du projet | [x] | -| link | string | Lien vers le site du projet | [x] | -| manager | string | Nom du scientifique qui dirige le projet | [x] | -| id_database | int | Identifiant de la database contenant les données du projet | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "name": "exodat", - "label": "ExoDat", - "description": "The ExoDat Information System is a search engine for exploring and displaying data from the CoRoT\/Exoplanet channel database.", - "link": "https:\/\/corot.cnes.fr\/fr", - "manager": "M.Deleuil", - "id_database": 16 - }, { - "name": "aspic", - "label": "Aspic", - "description": "Aspic project", - "link": "http:\/\/cesam.lam.fr\/aspic", - "manager": "C. Adami", - "id_database": 18 - }] -} -``` - -2. `POST` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "name": "my_project", - "label": "Mon super projet", - "description": "Une description compl\u00e8te du projet", - "link": "http:\/\/monprojet.com", - "manager": "M. Durand", - "id_database": 19 - } -} -``` - -### Error Response - -1. Si l'identifiant de la database à associer au projet est inconnu (id_database) (`POST`) - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Database with id 15 is not found" -} -``` - -2. Si le paramètre name est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param name needed to add a new project" -} -``` - -3. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new project" -} -``` - -4. Si le paramètre description est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param description needed to add a new project" -} -``` - -5. Si le paramètre link est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param link needed to add a new project" -} -``` - -6. Si le paramètre manager est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param manager needed to add a new project" -} -``` - -7. Si le paramètre id_database est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param id_database needed to add a new project" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des projets - -> curl -X GET http://localhost:8989/metadata/project - -2. Ajouter une nouveau projet - -> curl -d '{"name":"my_project","label":"Mon super projet","description":"Une description complète du projet","link":"http://monprojet.com","manager":"M. Durand","id_database":19}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/project diff --git a/documentation/api/project.md b/documentation/api/project.md deleted file mode 100644 index fed052f..0000000 --- a/documentation/api/project.md +++ /dev/null @@ -1,182 +0,0 @@ -# Metadata: Project item ----- -1. Permet de récupérer le projet qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement le projet qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer la projet qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/project/:name - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :------ | :--------------------------| -| name | string | Nom du projet | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :--------------------------------------------------------- | :----------: | -| label | string | Permet de définir un label pour le pojet | [x] | -| description | text | Description du projet | [x] | -| link | string | Lien vers le site du projet | [x] | -| manager | string | Nom du scientifique qui dirige le projet | [x] | -| id_database | int | Identifiant de la database contenant les données du projet | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "name": "my_project", - "label": "Mon super projet", - "description": "Une description compl\u00e8te du projet", - "link": "http:\/\/monprojet.com", - "manager": "M. Durand", - "id_database": 19 - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "name": "my_project", - "label": "Mon projet tip top", - "description": "Une description complete du projet", - "link": "http:\/\/monprojet.com", - "manager": "M. Durand", - "id_database": 19 - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "Project vuds is removed!" -} -``` - -### Error Response - -1. Si l'identifiant de la database à associer au projet est inconnu (id_database) (`POST`) - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Database with id 15 is not found" -} -``` - -2. Si le paramètre label est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param label needed to add a new project" -} -``` - -3. Si le paramètre description est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param description needed to add a new project" -} -``` - -4. Si le paramètre link est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param link needed to add a new project" -} -``` - -5. Si le paramètre manager est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param manager needed to add a new project" -} -``` - -6. Si le paramètre id_database est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param id_database needed to add a new project" -} -``` - -### Exemples d'utilisation - -1. Récuperer le projet qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/project/my_project - -2. Modifier le projet qui correspond à l'identifiant passé en paramètre - -> curl -d '{"label":"Mon projet tip top","description":"Une description complete du projet","link":"http://monprojet.com","manager":"M. Durand","id_database":19}' -H "Content-Type: application/json" -X PUT http://localhost:8989/metadata/project/my_project - -3. Supprimer le projet qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/project/my_project diff --git a/documentation/api/register.md b/documentation/api/register.md deleted file mode 100644 index d6204ff..0000000 --- a/documentation/api/register.md +++ /dev/null @@ -1,100 +0,0 @@ -# Enregistrer un nouvel utilisateur ----- -1. Permet de créer un nouvel utilisateur ANIS (`GET`) - -### URL - -- /register - -### Method - -- `OPTIONS` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :--------------------------------------------------------- | :----------: | -| email | string | Adresse email du nouvel utilisateur | [x] | -| password | string | Mot de passe du nouvel utilisateur | [x] | - -### Success Response - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "email": "test@anis.fr", - "activated": false, - "adminsi": false, - "superuser": false, - "id_group": 1 - } -} -``` - -### Error Response - -1. Si le paramètre email est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param email needed to register a new user" -} -``` - -2. Si le paramètre password est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param password needed to register a new user" -} -``` - -3. Si un utilisateur avec la même adresse email éxiste déjà dans la base de données - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "error": "Invalid user", - "error_description": "HTTP 400: A user with the email address user1@anis.fr already exists" -} -``` - -4. Si l'adresse email envoyé par l'utilisateur n'est pas valide - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid email", - "error_description": "HTTP 400: Bad email adress; a well-defined email address is needed to finalize the registration" -} -``` - -### Exemple d'utilisation - -1. Enregistrer un nouvel utilisateur - -> curl -d '{"email":"user1@anis.fr","password":"test"}' -H "Content-Type: application/json" -X POST http://localhost:8989/register diff --git a/documentation/api/root.md b/documentation/api/root.md deleted file mode 100644 index e2a9396..0000000 --- a/documentation/api/root.md +++ /dev/null @@ -1,44 +0,0 @@ -# Root API - -1. Retourne la chaîne "It works!" qui permet de s'assurer que le service fonctionne. (`GET`) - -## URL - -- / - -## Method - -- `GET` - -## Headers - -None - -## URL Params - -None - -### Data Params - -1. Pour une requête GET - -None - -## Success Response - -1. `GET` - -**Code:** 200 -**Content-type:** application/json -**Response:** -```json -{ - "message": "it's works!" -} -``` - -## Example of use - -1. Tester le fonctionnement du service - -> curl -X GET http://localhost:8989/ diff --git a/documentation/api/tables-available.md b/documentation/api/tables-available.md deleted file mode 100644 index 121b43d..0000000 --- a/documentation/api/tables-available.md +++ /dev/null @@ -1,60 +0,0 @@ -# Metadata: Tables collection ----- -1. Permet de récupérer l'ensemble des tables et vues disponibles dans la database qui correspond à l'identifiant passé dans l'URL (`GET`) - -### URL - -- /metadata/database/:id/table - -### Method - -- `GET` - -### Headers - -None - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :--- | :--------------------------| -| id | int | Identifiant de la database | - -### Data Params - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -[ - "a_t_hub", - "a_t_hub_usnoa2_checked", - "cfht_cat" -] -``` - -### Error Response - -1. Si la database avec l'id passé en paramètre est inconnu - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Database with id 15 is not found" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des tables et vues disponibles dans la database qui correspond à l'identifiant passé dans l'URL - -> curl -X GET http://localhost:8989/metadata/database/16/table diff --git a/documentation/api/user-list.md b/documentation/api/user-list.md deleted file mode 100644 index 4e524ed..0000000 --- a/documentation/api/user-list.md +++ /dev/null @@ -1,150 +0,0 @@ -# Metadata: User collection ----- -1. Permet de récupérer l'ensemble des utilisateurs disponibles dans la base de données metadata (`GET`) -2. Permet également de créer un nouvel utilisateur (`POST`) - -### URL - -- /metadata/user - -### Method - -- `GET` -- `POST` - -### Headers - -- Content-type: application/json - -### URL Params - -None - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête POST - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :--------------------------------------------------------- | :----------: | -| email | string | Adresse email du nouvel utilisateur | [x] | -| adminsi | bool | Permet de définir l'utilisateur comme un admin du SI | [x] | -| superuser | bool | Permet de définir l'utilisateur comme un super utilisateur | [x] | -| id_group | int | Identifiant du groupe | [x] | - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "email": "user1@anis.fr", - "activated": false, - "adminsi": false, - "superuser": false, - "id_group": 1 - }, { - "email": "user2@anis.fr", - "activated": true, - "adminsi": false, - "superuser": false, - "id_group": 1 - }] -} -``` - -2. `POST` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": [{ - "email": "newuser@anis.fr", - "activated": false, - "adminsi": false, - "superuser": false, - "id_group": 1 - }] -} -``` - -### Error Response - -1. Si l'identifiant du groupe à associer à l'utilisateur est inconnu (id_group) (`POST`) - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Group with id 15 is not found" -} -``` - -2. Si le paramètre email est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param email needed to add a new user" -} -``` - -3. Si le paramètre adminsi est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param adminsi needed to add a new user" -} -``` - -4. Si le paramètre superuser est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param superuser needed to add a new user" -} -``` - -5. Si le paramètre id_group est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param id_group needed to add a new user" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'ensemble des projets - -> curl -X GET http://localhost:8989/metadata/user - -2. Ajouter un nouvel utilisateur - -> curl -d '{"email":"newuser@anis.fr","adminsi":true,"superuser":true,"id_group":1}' -H "Content-Type: application/json" -X POST http://localhost:8989/metadata/user diff --git a/documentation/api/user.md b/documentation/api/user.md deleted file mode 100644 index 023c65c..0000000 --- a/documentation/api/user.md +++ /dev/null @@ -1,167 +0,0 @@ -# Metadata: User item ----- -1. Permet de récupérer l'utilisateur qui correspond à l'identifiant passé dans l'URL (`GET`) -2. Permet de modifier intégralement l'utilisateur qui correspond à l'identifiant passé dans l'URL (`PUT`) -3. Permet de supprimer l'utilisateur qui correspond à l'identifiant passé dans l'URL (`DELETE`) - -### URL - -- /metadata/user/:email - -### Method - -- `GET` -- `PUT` -- `DELETE` - -### Headers - -- Content-type: application/json - -### URL Params - -| Nom du paramètre | Type | Description | -| :--------------- | :------ | :----------------------------- | -| email | string | Adresse email de l'utilisateur | - -### Data Params - -1. Pour une requête GET - -None - -2. Pour une requête PUT - -| Nom du paramètre | Type | Description | Obligatoire | -| :--------------- | :----- | :--------------------------------------------------------- | :----------: | -| adminsi | bool | Permet de définir l'utilisateur comme un admin du SI | [x] | -| superuser | bool | Permet de définir l'utilisateur comme un super utilisateur | [x] | -| activated | bool | Active ou désactive un utilisateur | [x] | -| id_group | int | Identifiant du groupe | [x] | - -3. Pour une requête DELETE - -None - -### Success Response - -1. `GET` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "email": "user1@anis.fr", - "activated": false, - "adminsi": false, - "superuser": false, - "id_group": 1 - } -} -``` - -2. `PUT` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "data": { - "email": "user1@anis.fr", - "activated": true, - "adminsi": false, - "superuser": false, - "id_group": 2 - } -} -``` - -3. `DELETE` - -**Code:** 200 <br /> -**Content-type:** application/json <br /> -**Exemple de retour:** <br /> -```json -{ - "message": "User withe mail user1@anis.fr is removed!" -} -``` - -### Error Response - -1. Si l'identifiant du groupe à associer à l'utilisateur est inconnu (id_group) (`POST`) - -**Code:** 404 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 404: Group with id 15 is not found" -} -``` - -2. Si le paramètre adminsi est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param adminsi needed to edit the user" -} -``` - -3. Si le paramètre superuser est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param superuser needed to edit the user" -} -``` - -4. Si le paramètre activated est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param activated needed to edit the user" -} -``` - -5. Si le paramètre id_group est vide ou inconnu - -**Code:** 400 <br /> -**Content-type:** application/json <br /> -**Retour:** <br /> -```json -{ - "error": "Invalid request", - "error_description": "HTTP 400: Param id_group needed to edit the user" -} -``` - -### Exemples d'utilisation - -1. Récuperer l'utilisateur qui correspond à l'identifiant passé en paramètre - -> curl -X GET http://localhost:8989/metadata/user/user1@anis.fr - -2. Modifier le projet qui correspond à l'identifiant passé en paramètre - -> curl -d '{"adminsi":false,"superuser":false,"activated":true,"id_group":1}' -H "Content-Type: application/json" -X PUT http://localhost:8989/metadata/user/user1@anis.fr - -3. Supprimer le projet qui correspond à l'identifiant passé en paramètre - -> curl -X DELETE http://localhost:8989/metadata/user/user1@anis.fr diff --git a/documentation/mcd/anis_v3_mcd.png b/documentation/mcd/anis_v3_mcd.png deleted file mode 100644 index e79b21a8c25e69ffabd22583adb62ae9dd9a483e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93829 zcmbrm1yogE*FJg>ML|-!k?w9JL^`FrySw2W5R?`W>6Vi2?rxB7knZlj8}xmD?{~lV zf5*M!9tXoAd!Mz}UTdy7pZUyZ8u(F06afwg4g>-rh>HoygFw)zAkg!`S1`bD(n0TU zfWKh$B}Iim508IR8?qyT-@w|6sW|}On0)+!5~NIU1%CO`QCwR1<q8Zu5*)HqG`lDW zL<ABSdavj@vpesitEe>7b#%~gI6@IABlt!TL6|%3i&!X8z|YN~!l`+5wUSgcbR(!2 zJB6LKwZzU;^3T5$f1-rbhf)kr4i6U;yv`Wxzj;{%tu_iz=d-v`)YGuBz;iOk6X~{| z=t1B+y5&gx^f{)$Mt_F#_>p6N`}7+La-pYBk6*y!hcXWN|LHO-oHd+JKtOj*iPbFi zizU*M5)!oM5h5bIs856aFcf?m?7P&{U;*D?|Hoke_3~AE)N7UFtz{O482yR6GBMeK zkKw{XkJIV?ynpnPfP#!l*asPf?9UTM7;C7W1J6Q`HCBgty`U*(JR_&x$=Wq(-qJNW zTLIoHWwX4w_6V}-c>ei0({`;~d5Brh$VIe#xJvUa=DLABO9Ugg55b+g;_SW4jmzFz zQp`fu&u)>8DVdN)YgTd&h0v~Ns&$@p8tm3-_4#-@{5qWre9=EK<j~eJOLy-w@h<m3 z#x5?)Upi4zkc&`h?30-WVwnuSv=R{$+n?iel~(nx$z(^WtTzfyYR^|2Ee~9(dS33X z44F=pVq0;SL!}ZGc}sR__L8Y+ei}B^?WvR8;^6!mT<fgc7o$;awk#b|?fzUkJ$_<p z!I1fQZJVK|uV>@y%l*`cU!0AacVxn!g$w=CYOO{QvrDd5hGH)_dEx7p-*7u_d1qHq z5F``SAI;(V34d`ondca#s$}WoSC!EfLw+m$DKAuXG|S3SE`XI8XPuOoc%(T4PohQH z)Zlj}G$R&#yC&L)9p?g?imjPCnnrLV7Cel5$3>?ldt{Vd`Ha`Z1Q@vQ=;*9vwkx59 z<mJg|^j?Lvg*}VBwz7!31kO1e4^#`3Ds+BEc2=YvX(WnDSo44-IT58d*mq^B#>!u& zu-i?fEfFvfoY>nr&s*PTz(Wg-eBSy*&3HCld9Xto<38O!3lSl?s<}c#M^maBnwimn zzSRnY{sc|g-IwPyJF%z1;@O|+S-Yw5)!1;jQPM*s6kk|I8PVC}#gGhSq9QCP2u_6| z@Q)csu(S^?hPB>)lIVjwd94X6>}-sY;#r-@LlH(n%M9;bqp(psgA)?<l3;9tfQW%2 zrh6R)v}(MeTcW;GZZ&kzyEWZJ0W3FdVpTYUGFT=I%zJoxY&eoH9`%M8h3mmIHZmYU z3>tO%-5VDd7x4X($t32r7LRk?1m>&+Q(YbE9O)}>tyu^e1x;7Q!t%xkXA$S5S!mCi zYe`FC_Ml)(&ywxewddRNJC)d?BjPhDer-$5qob%UNG>7ds7n#juyC-eqrxfj>heLO zTcj*>zHKj&c2<KDO~zP&!#mOp>`<q4bwt>rc6%0ELh<FzFjwV?c#p_)Ef1HqtGJ;R zLNqa<GTT<-`05EH9H!VcLRyoaIzpD^c$Cwi11$p&@4=EJ*R7)%O6t<#91jW@c*2i6 zB@&Xos@MDSrD4V^>+2$Y&IMSk`iwB?KDZQf?G%IszjOSPTeaohi>Xq=rlUIA3lCNe z(PWQqDPjRTJJ@J7t4#HZA}|y`jp$!?1{~M_gO~YxsQ-0&Rq^NG<GJU0Ly$E+{p17^ zi3iwp_~RqBg@jh;Mi?pnhd=p0jQT%Z+aO6<cB6!Zyn56QpIUV?dN2pI81bVc!l|rL zSpW9z`wxMDM58v*i!T0OxdH)?F3YU`jR?hWLM&T}8`oQ(uBh;UTk#Svo@I+8Wg?-B z7KIm<5wE`kTV<Mmb>RoN63UOn8_l~pMfN9`!$Kndk;%~=q9@7`an2zaOc9q?SHJi2 zggyuXKC#=3edk{76n`v?m;WGZN?9T}cKwgniL~(YZ0)dou;xKuGm?rw1F6NS(Yl$y z=7>|N=3~4O(GlF38f-q&li`?if{qxD_;1b#NdRokm*RN|`|=#G+1kZcZW2+`p^uH$ z%ZBQO+lzjP9v?XeDzIhWQv+^uG8TI@80gYeG$~+Y_68Q_#p~m$11{5e(ea)lL$=Pb z59xj)GlBP2;K}siG|U%G_V$YV-^tKYvCf#lIDm@-u597W25KDCM6E*m$6d50Jokqg zE?4ujz|9`*xy9TVt(SJl;$;i{bSt>vx{vTF!S7e0fyO$;AVsbbT4YLWNRQ$2VarNe zeBtE|`t9)w!<^%Vw2+AC_A0R8RN`AxL6-1%6S(Qa0<(w%W}|%SJ}40|A1<T<5>1w2 zy7X9am~cW*ao=>wYE-Id^Rpsyl;Oho`iy0u0jC{{*_;xXCY^r#XpM;pc<xvs(`rOv zw1@LsPe7jNZ)0{;WHf=M7Q5ZKO5gJDB!))DcGEiaZEd5_z$n1Cc7y^FVcgt4U%pgl z!a*oBdjIwwE+l{zYda2^Yv}S?O*8E7mJV%oO-cR0<c#M?n5+uK4{4IPka_<6MYtVl z`UpFh8rRew`a7OGwaHSzE5(bjkzJ?~On@MB{nwl*T=V(LI&ddeQPJqDv#;QtRcwVG zV_qV-tF$yQz}o%xXxKnC|AD0;W2v1XQ5o_Y@QG2@or67tEjS<Da32I{lC_hi;@M!^ z#Al)L#AV1BC@?)f&pX+OWVT_CLs84?L=5Z2&P(s!>LDf7c7;bpv=!pt1mfm<gb8Kn z{erkfob+e0>By^o^egD@yhh*In#q%_up;?Ea&&fLbQg3swdahu$J74x!1-5&N|nT3 zD@no|v^U+^!F-wJ8EDy~;z0cMM<O79mrK=>){?JU{AuKyJasP(XZ1$7g+_1+(lquo zHxu#M6@?Y6ZJ(CGrT&?+w&yXgGX1nz%v9w+i|M#E{aIzbUP>+^RA!!sN%zv*d=>{D z#`?NzfY!|gHer9>ufu#P*G{Fzon3P;1jahi7B$7Q{8Ki8JDi8`i?@1dq#-h#jbU{* z`qMXku{=ifGN}et!I~G+@c+x@6-1KC2aB#RY`NoMp!Bz!aZpk|`GUcE<-J*Cj&exU zP8L7M9a7(?gy#-j89cd`2e#ZyabJ<Nj?YfkYI!=9rw4(MV&@W?XRp<@kH0n5kS-G< zhF_p|f0kkn=~=Ubiw{U-d5o?~)U=eX%Bp@34|DvFNb8vA3~&I18F<|NxI+ys-b>Jj z$8wok5oO}M2*e|?Scx*(3VO&~Rh3p6nM7kBEO%eMc#l&t_B{%&Rh*bul=>BMXvi0? zz$aYwpRmflC7$<8N&ef2xfO?jJn>eJn#m~^F)`aP!W1!rfNQL^)*BR*h)&VJ2u+>= zuwWDvvt~Yx%0R2d+mM2vvA@FwM8fa|^I|C{uN&&V#a<liTMB1723qw+O(Hm<C(w3R zm8|+!>^{(8m(|4u`=9<^_DhD#4M=4FJC<OZZ}mo6LhRkG*D3B;XqAqJ2yk>P?qp}d zM$6^6aojgH8RRC#_hob-&Lo)ycQ+;|AB-%1u4Gsw?XLAu{t}BW@C&6!4;c`k`KtC} z+q+}v@z|$a(#H;1<4N<ctMl8s5_)<z93Gt+Zgzuj%Z)HWW>li$vIEAevG1$n=3l)k z9f8=oR6?3G-lIp1{>2%^z#H~w4M9?gnvm0n#k8&83V-e>JiCX&m|XD|;6Jv14eI8K zmK7F$A<KWRGwBZ4(~_Jtn&KaHY~R?|+pV|tawnE?t;kQ*4|LdGu42b6+YgFKP!Ldb zjHU4@zShtb{}NO6GSXz>ON;JDM&Z?vUyo5L0w%XQ1Fg(IF8mh=1fFgT+s&_+uwpM< zkdV`Zt^A?Sr*lT9<z4}uYNkl((*l*RSsyX-%^nYtV#`d8lu*$M7KO{}Pm~uB7WN-* zQ1M7kVhi&of)M`@gbjXGIF=<D=3LKc!dLl2p4%l%@9>Zh1i%67I9v2SOwYkYf1S)9 zb24J8$ZbUdD!Rg}<xHHi`!whg4#aFmnV&u)p+u0NKOgqcIWR-v@@MTAe}e7X!T>zO zDaMX`gB|`Wa&PfLq@=(PqkyDly;Bl61rLZH6`1mXtKwMbT`@bA5h)sCi@|mFN57Km zFFGkQNV7+4wOCj7tMOcrSW=gj0jmcB?P`6bBV%Zwf(?l1sOMs;#EN6y-326rR(5=J zroS`E<9btJ^p5#Lrhk3KW-p0<S8sXD?tAq2<GKDSG4Al*aSnJ2#;KHtcMm8U?0ZCD zQzR9;y!8U&*!xvixZ_~gJ)PID%-)FI3>G-P0bU|Q0yuml%^gUA9j45BtZ}BJX<Sr2 zTZK*)=w&#!4;}f>eDBP#QH6IZ2VleCTiQmYHukN=c|ARKsIAF>37@4U!$!aic6Ifg zO<6j(Im?&i&IGAlsd>>@S(IESo>s+;jMxiw6g;k1k6-tTO!|)eZHcFUXYc{*m=TdH z$&^<sZ$}gHWiRscj#`Op`p&`Qn#m5x>k6$VDh&CFktoIJD?4RAt&n`t0W!5Zh5dex zt<v4kW~F(h?=g`H5fawMFqQRq5>~=feSILtxcQ^Q^Oi||WJxhfC_R+wnmF`wnA3#^ z!zFik5!9GaY!Kc-T=mKQ&h{Pjr-e~s``Vu-f*C&QWw$#pCz|v%?_?-4BB<#piJT6K z472fC^{P~oOut9T%PC99?K3eW?zNdRqu`-P3rW3vM+b&m<_iDcAZAA^XJA1~yx+r( z2{5|a_R6*1oiF;#ahg3}%*UR96G{$d-Mmb1AwJ%874E&^%knmB3YII7C=wxQy^{v< ztW)P|76j8IxNT{*ASU`3O(80D_PO!FKIwv#rqJ!LUY%c%nR6P{?D<z%t}CBk<Ud%x zh9$YFnaC3feMIy%-Z>ls+j$v8Df1eQKdOl?zMro+a&wisaJ*0x4cx*HtBa}n<bjbv znrT2&%4Dl*6Cwg_#lxB{eY445j(hpwWC;55b+;?G1DA#jwQ2GvlqQ=wSe6{{H^0p% zDH>nzD1+OEPYqZ@;a<p${RA?dA4XDWODJi6q68^yg}FFvsN{>@!=^tK(?x(#{pf_v z41??)F3)z(6u(C$S9)Qr8kRY<na^C{22`;Lyyt5>K88C&D%WRCj?LntY=1!7yM?AL z?1J(EudZPzsu$s~Jw+d(?feX)hjNNHO*AxA#*^y^js%WW#<H?6Zs1GRy{P)Dlw>xK zt)9cwsi;T)aXdSc4daXQ*>{f%lPHr5WCQjD#VU%$COeZRq!B;AfAlPa@?hhz@7qGb z#{V)s-#$oqUfp<mFjE4|;eR=yywQ85bdg8*t4U-m>lO7qW9ES>MFJ|Xmf!<U<@b3W zs@@^4l`g7Y-p&YKewyDV$Ao3W&2%oIOTptBZor{dtuZ9g(x1Ig(~2S3bJZL4Tz_}V zy@b*NPq2_JbHiUFG5|@|=}x=L$<|CKXQ=Y#LTc(wzu+4ZJ>ko&u2!UwUwxLwjS7S- zCx?Nd<ZEh<FhXfVN7sTLA+L?Zq8%n2;sJ4prrmoeey_%5*`XQ51BN<sj#?f2nlC&_ zEf@EnB@WE~hPcqX*INrZL+Zp(zl&AE>n*aMF%T2iLTO}_{LJY_8`Er-5Y8ObL0?+~ zP678$*MJ0{fMa%H?svLBIf3s?kd%_j(@i!t*g50Ml`yVLYaHG0+|hhnyzA<C(%!Z> z?oavfHMmzBFJ3ha^mIP#856hW5niv`PW4F3l5v*S$b9;fMr@kXFe_<6(V$0h4@Cy{ zhp5kn)GRaCI@Sh!`Xu;GL0+xl{PnkQNG#{^G+I`LIK1Bm@ntm)nl8-8Nf}ra9vP6w zWCZ@7QJSWY%~M)>Oc`}qaZ1$hHVw~6C>g_5v{OX4BUp*!2@bp%u|unC?rI>g9VU9` zlz&*B?4R_)S-CX2nLp$=s@`Y}rz~WdxuQOUzUBJvI29!|CTb^_Cgc5g%G>gW<}2F; z<(BxLTYl#&HgB)~kcW-w&~tVnVIfH<{bnCu@yce*4%=g^1M{}xC~Wqi*^u>aYmXK~ zCrDC46T+ZT6CagQ08K<n8p~r{lQ!Y?9b?uJQD(TTNLCK{55`raz>I@?bxc7_eTu|J z24>JTqM^=oxWH0@%3BPUpwl_zav%Ul1=;hw)q4-$M6B)8;Ej%kw!G%_Lq@1I9Lmwu zrJgaq=ciiRP(>V4V}HRzb*bD01m%$;8WDI5C6g<1lkmo4Y`$8MuzGXvoud$w0~jE5 zgi&7@9G$EeRfXlAVTBI(r-0pBbVxK*=sUH~H*yiOGfix1DPq=#DY7um&rjR8O|BGf zjt-9}xHyFY7yXo_t|lIxo~QkcHf&vTT(;$;q%F0(w1rD4&=?Zlo1Nf1v(R$Ay4-%A z2o=1j_2E7EtqmzSBq1SiDg8zvTt{fchh*;EeLK9SB%f+<i|Owm3x8$+wAuN(7h7yV zNd!xpp`hldv;6T2=Ic9zVsiT>@lA>JS}n_&G>3<_6r7X+;0E`RI+ZX5zRId`9g5%Z zSPpGux20Y>nR+rW+&<^4-LrLcWV*V!=~nSvZq?1tfrr=%_Yn8^Tm5<_p&1V{J961b zp57(HI6C|z;QRBO@7n9Ei<}6Z%oNZqes-RuRGe6zWeQ87%~tJY$$DafZ>~UmeA<QB zbD(8^$2LRe`-c_PXh^wsh4Isl1}HKR(Y{F^_G{DQ&+B5tkycG$9f#!%c0b?gUS|Mj z07EoBoMXD5?x~tJ6s@%vvjnGvnGgJp8EYTr210*#n=(ftC62FrVYN8Q4xSUA$IgxY zn5QtwP75|+15oX4tpk6I7VpoIYXh|!ArTHcUgZL86fRuWhm-&@5=@lQrF^hBD^>_= z=vHcT%6p7pBvBRz8j7#R9Nb-%EAx{vqxr&|Q18J}6E><R_@7^}2f<0I)|GCnvt*L3 zapK90jFyV=#byZv1BWi+bq~es$uj+;&NBD@X$2iJGOE2qD;>hTXj;Qwttz{_?)UZg zt)tluULRJ>j!5dAX3L!G5817b-VK@9mrh&0duWHZt|#_6;t0{(3(91xg#13>us>01 zqwMC9ld?bgGUR5ef447+&HRHQh4%K7DEWPY4TqHrUD)1aMEO|y>E`GW(yZgobaH#m z0pmr)1M)QODN=<ofLCKr4)fl*ADnoy9w-C_OQN7~DQ%}?MBpG0&J#WeSR~CTE1B*L zSwd=`fAO;&kghr&=sYvY@cMu@UC07NNviH1zVxmm1bFgNt&NlWTF3@J!?zfh5X=pR zRWW;J2Li9~pPSL;JBoJ}9C351jfY<M?l+eTeBkVTCdX}p6aO_RC@_G@9rusAoUCR< zC-Wvk0=jcP4*3Nr_E@`skY}q%8Zi#s4>lC5io*ErUcQ;-L9`)EVHs(B%Un}4TsZ{l z*EXS_FqUP|cRzC(r{}aOx(kA1w^8jH=a~DFGM?g`#64=$OvDau%<bG0(R8A-P@y9O zU?a<MeWDBn?xa<wM$x&eyZhz`2_c;4Ku*P~3qWR1RvYv<vo2E<)WDP!q=iO`M%oF* z0P}(4qUri0(cV6q;>#=BJFA#szzD8tKGSL(a@~M=LfZCI4JIcvJx*SMjHM(D1q5Zm zx7HkMYim+*ZZ84esCQsR6crm^+dwZr7tX5V9I)S;{C-W0rmFb1m!kmT#%QVdPRpY* z&vEyEqGc@wEeWm0U5}Bp1CX=Tf`FR%(bjxb49DILLUUW+O?}wPa0sh}q@?LWZiR)K zV;{TcSw}5SRd9g0$WSA+>hbXG9P|Cc=yhVRM4y|bJh6|D@_zLNL+#WPDK^%>WP3az zDY=P08R(<nQRd?E#)h1p7`%C8n3g4|*r-fcX}!<`&0^g#xh(ChOkhn;0`N5tjh9RV z@%~JEM@>uX#@B7qu{RfZ#>?e)<KV!eLPlk!&z1KVXOBG4V2M?Y*YY?izSv4ACI<2d zsds>Jmr1HPcWh^S4hJawa@AvZsxra4!72S9WyuO5b@VsN?({L)i)m5WK0YsKU$e;@ z{r==+Od<H-2Y!?jZYsOuqw}<OWyPJ!$&k9ZmtBjkYX*h<1w%daZT*Ru2Sv_zfQz2! zU?AS`10SQBr2_eE3Hme^XFi8Wx=$s3{AiX}xy9YV=bs{9vHcRJ3*_HYM-Gd*sd0%S zn&?-}!opSRBNW@N+LEiqbz$*3ze-Nt2QaefG<oTW-HQKt`@n$W-mX9sW+!u8NIO#C zS~gwX_NlvkBun=Y1MyNRr&QKT*B;JnGGXPd{Yaw$Q@j+GH2I99sf>huExn(+*U@RJ z*PJf#td`%f>cd-c7~hBhcFf_lhw#_r5un54Q|@F%v{}u*AWLCa_enl@de(fZyO(6e zn|D=?Z-csDya%#x1VwG_xA#8EA)Tl?+rEMTt!+UqG|o!>9mN2q7$CoTdO1b>=?k#D z_K@uz;EG{XZ5{INUuUIMtTRZBqj%<>ovmW%A{-Wst)L)Vl990WomGW}#q5+!?|vyL zR>c(Ljgun(G2`+!Us&Ut%N{Asl|a(#S?o~qPwDE&!<B7r{1ysIg7JPU`%CYSeeTM4 zTyMYZWI*2p27N6YJy!{Bh`4!hhJLF@4fho2R`E*yKu>k$a2pcFDg2)e$Vvy*gQs72 zq~!FN2pcPcDrlP0RGPl7uA%8x&E_=(P6!N~!R69~yimfNv2P%NymsUIJhg|YA{Y@F z<<Iu}6rxr!yT`ffY|CxOnfN^>3dqhhi>}Tvle021{OTxVixh4;`**j*^Q?IT_s2r# zPSK9Tj6Z%)_z+m|%ERsIsJ+dyb9DA)inl=iTj^r<!0Oc3BXM(c0l1ug!i4=Rjb<!d z-@dh1B}}jwbt)9HKX@P{(NeO0_h}O;wm*6n@8`eMCiTR^PGkNX6#vTcfLFuj+MOv$ zscz}Y2EVI8R%$h}i=ChE?(fdZ&%i@MV(F-q|Do=;vbFZrzYd!t(kDwU4Iny8JzLyG zG`Wz0Ty53Y41%?-7>Qufm=0tNFuo=7dbFH!rg^y@<izo8E&6B1^SJ}0A)AsTEv%I` zvjdke3;Fj-G)@QKMSGrB?i?^E7wL|TL)MC_?(n^L^V1y<rn3k4hJukiI5<9$B%tC= zYuXfYV_j4Q*54o0gfuNt*VlX1+(+}!NWZ>mn*8-pdpv`CrGM7zP6+AD5ZbSQ;BF=U z<iz|GoNwKLBc!p9)Ap%~-(Y3tpjg#w^Nq(3#%bU*MKfF`0$^N1xF#vQnCOcR3KHkl z<z0m#;Xuo(@eie0V|e^&B;S^10AhV3Bg|hpGGkbA#MvCH7)X&I2Q!5H3%CKVt8Hr0 z<G-VNJ7`W{XN>H%T&L*HTP1_l_QPe8(aVY$tgO=+g1f%G#+g*uaOGUsP_wPChv@Iy zU#<v)K&X^`Zm)NIe!iaUZ#;2@<Gx-|MN@Hv1i!kMX@NUtk(-P77-g6N@+iBN$lf|L zE8h}rs4VSH9S4O=9y2U4=HQOyNZSIw#}CMTL&Mx3YnnKzA8r(4A7<VhI|OZxHArXH z(y)zN=HzzvyXYL`mjWX`rEn1^zrq&xmJ44Nc7f_oPhS>3-Dby`q%;wmQvlp0=9L2! zCB~*79TY+uu}RNw$s7Hnla|RL|F82i)!T(*ie;I#I4s1(CP9B5Oq6%lUf#1$0K{tv z?|6$!MHPe2$>jp?pg#|M&DZlY(^yi8&A1ScC!>>_5nvrM!!Q8ONMv<2o({1t$@fNn zs_~I0(Gu#6pheu1?67uG%w>Y6*lvER(AEg_16dN4xEaJ%GvP0{01_VTg3LYWl&v)< zqh&3|bbq=^sWGqo9vyvjbYtOmy0CGz0nMQ2!imLQN@!b;*iZ4g>32ln!}_fBEV>v9 z7u^$ifyGzL5BR7Tf~(IMSElP|?vf54U|S9Z<DWBwN2d5KC-a3B_93V1)ECph$pMIE z%IYVMM=*nHNf=;X(uNr+B1D!~^<-Gc$&Ncr7LO0QHdr>6CNFIlBzB!eqh`qn-?jGl zFr3U5`J)N_twV?ZA&7K)`)7N^`i#3O=OLzLHb78osQ+#UsY?U`0FCJ%Hc5yGEJiC5 z37g&{lz=#(^`ZcL(I@M1rMn<uUFQK$r!GpZ`}fALCr3y2yzB|{lyr1O<@bGOi=!nL zxWIXoEB-^?1dPpB^N5sOOp*d*T*0SDk`^c^W$b&>@r6KiX*D<wblc(&&>rjIKt;)( zecF;e8KMuvp&ikDiQ8H1+%bYP%YS%*TXU7G2UG%TqOBGVxsasm?-JiZg2!X($hErG z5%^2Z<}bL@h%tWxsfy;tj~HTN$d_%YfU)065A|Z&3ryOIQ_NBS(gGwlg_HF3^t+wD z|M5d(q)=hQyo%{RVc0#1!$JPBoz;s18EqSc;=^C4kkKBr8qo9}q7?a^HRa<A4BqoE z{XtaU)nr#p&bo>s`R`!h^;b6X%%#X55mgGUXIO-aRxm_2^%)g0WF|yBupME$r7-Cw zB^^Bg`$<)_InW||o(m`&(+4kV3@rJYsor{Fu~R1(^3K*&;se|uVbiR%n;Tbw!4Il# zhSkvaFgphpCQnBQdJEcP8fG%{{{r4<zmON&Ui)=VA{Fb&3p&wO@4p_Z3}-AQMD&m3 zK*a=VnRHdM_3;_dX7xv{&j1swHny2<+F(S@4h2#WeMYQHqZ>Z!yRImp=0;OYY_Fbk zppb#nLDm5RlApThE7zNk`ZT#tvlgB6{a@Hpz9q%w2n2~Wv?FbO=`n0KWPZa>8l6(P z`;3T<%lGz{_zit9Fzj%tG^v)s>`7a+j*KNwIfb<~IXsLnuuP9D`+{L;#R)Gc*s9P- zxRUr?gO~X{`#+H$O$^|_bb^a^N;-!8>*t<<0JvTZbE#a5NlY{)p)HfPW5b^p_Uz9l zWrmY4ReQLGd+Ih>sEzpC2?>i%n%(4eE_AOW?1;(A$^lHWEKhWl##YJr;xq5@ML}(u zx6XBK6?_ylNEXe3t8D^n1hBsMvrqX-S>Niq$XxSV+G||`;m<F2=kHh+t<{IT-;!ur zX+xIG-lW7Ut)0J5OM}N_{}dIBNf}FgoNe_VdC;*&?I}0d*GZ_1Ts40oU!wxse}1sV zi^_8G5$4h7I_vyNsL&Nrxb_^OE5#5x@2rT69z5t*Y^vcHY;+2n)&?b5xQtBjGd+g* z#;qZ16&%MM+fi%;>Gk<iJ{S@Fn-gWcfewo;%8C*j`%Lfz;M=lJQ&!s@KPFmt7lhGC z@@11VqyB0=Ah*kL*7+cEYV<$j31u^|#ed5rS0{LgFOhD&@MIfrF^tP<v~O&=@o#qm zyQ<H#_~1-=g@nE;B4@}+O6Od6eE}R~4Zw`P3Vt==p)Q2Ois0}j=OCfWbhT!3G0%k< z>hRoxq2w$t6jQ2ShG2yba08(!K&2&-gof`vTsDTTeO38x))t7XZ$vtd)@rR*H!wyn zmJ2oCl~Wuv9KizYa=A6;Ks672*ZhjB;K_8UwB^mZiKTGIbN}be?z_n99YQtdY(!}k zvrP}7`Wmo+ld>NOjj1*E%Q2fh9i!#*v>+25b@+GgvY^Rs3<ABlVvqJLj=pOmGjQf` zI0`4wO#D?gVh3SJWiwjG=f3*utr{EWPhSAjfCITKuwWOf%sOu%dBk!$;boF#zT-Hd z`-Yfde}>Qo3xB@BKLlxymgWf{oPqZKbx5@Dwn+|DRHm-a5EsB!*kSz*sAX4|8oXP& zIH=hRAc5t`gA#*T>)NyZk1Lhrk%4Xu3|R->`<m}qqkTP)POeX0Z}F32fPhl325`kK z@6}^L=hH2*_RZwuVI667ph>ldqC?4<2O_63vQUQHFNkwQndQYGJ`iVDfLLx_eMi6S zk62W4_j}0;sO%5}JbyHkT)FjZ;$b)PyLSVQ`wP)yZCizv^Bzvk%NwD?rl|mx&~MD~ zHH*-pAdK)$@UUU4Df4jF2M76Y7K`j;R7u~y*~5^w>$BpRj#pw^F_<sS(9^9^Zskme z;IX^{O1n_+m9LTZ2hR^&QaB6YOo4h@Vhm7cvB${7<7*#f$(GaB7HRDs2DZSmVD?f? zwjbYyy-trS2P<mYGON6N?(Cw{25daK!T&K_Ct%o=l%Xp8N2~s%J0W3_#7${Zd10-U zq(on_x)%-EvwI()%#>(wrKqTQcxv$AL|3Mwh)giPnlYLBO#ScvE!dZpxgJOKprK>@ zYH-o9uGwiEt$oN3!HWhLe}z@Jmv9w&1(KEWh^m#EQXZ{T&O-t!Yrxt8sM;gnJG?k` zB~aM=1%nL|`A~hqOtSw2UFu^QW8)=Y<y@~V*>N2R6MBdH9o4(aH<EK6+D+dz*fcGF zlkX=m78HaAJ-X1+vSS&y*#KibSJ&z?yFGTG2z3LIT(8nWtgW=dL6^|A1<_#IWRz#% z`L~I?t7G)M9Cch4-ZncQQxO-JuwcAFLCN_FAfG`-|F)K3*@5L>_q?pIHodEm!H%}k zlX5}|YVMHzhMAMye7EP%uTW9Y)i=1dEoLSRnE}NF&86wRN&ej`je5J#aD@p^zco1s z9tOHfsyb-xHJ`~~Tamn+H3cbcz>-S)&T4R?!M~*%-Pq|roQO@PAgC!ud$~rcxB30< z`$nzAKkk2J)wU`jj3+SAXMKp7*r*RGgwqr^P?`6D0CTQ!yEP?FZ)5oBy2(p7atYK) zR^io8_uowvssl`U(o2*60_F%)c*I@Fx2&un!eEZ3JA3bjx=5^(NlC{~!h@gw8vy`q zUIgV}FE6hFa8i!}5TRQ|C*Nq;<Af0_bdGn8Nq<IX%v7guxZH99Yy^wG=6w5%cFwkI zO%<A!yNB2$7O02}v<o|awd&hLEh66%z_F<>_6{e9@)P|sp$eTv%{CtYghECHZjVQi z>W}B}K-Tv+yr<if)dz%|_=}$xHeAIbyFXL@XDl1}CzfsMyHuM4dLc+YltR>^#Q>;K zodt+!fZ)bPVU<;quRO)Xm^Y;1$0wSih6ueFot)GFu?C<>@tAP3qES7?ueX5Zu}ygA z^gc|)bJfU%KH}#H2U?yTN{1|2K+PgF=l%9}Eo&AH4JC&oujzArqTsHxzlEBNGN5`` zlwi{Z$9UumyhnXppU{Ycp8++agrpQZl=$3EYlGrQN*e9bLK*b`Buqf6h`2AQcT+j< zxZ@;Oz@`hxi(uw~vx=jr`2A#V*PqLC6BIEJ&$3lQ!I&`M^w65Rg)E!__n7Wbgy;>| zS;RK7qwwc37<3|#FxSEgg#H;~9FTAb1AItKO58x!nFZ`CIK6X+B4jk2+~xqW0vVUk zxG(b1S&gS{L1HWEdNRb>oAZ-8`lHtsu5KPIPV12pq(ZCtT*VBZTW^U0N~#XBFa+pl z$`7XG0ewo@W93<<Wd>bRt;P@vaGN)T@zRcc6L3KRiEO!0PsA%uj+I8^`hdzkLi6H5 zl3+GL$>UX}$EzFhK;#)~Yg`1cedc1Z<;K&5>@3!}ZrZ*8(Dh^TDmE}OD(`w+lJh+( znt64kujV$siJB5P@sE-w^XQbAzPYVPs_wo_i=oCj?j^o|z6Y>PuI8PrBMJ(p*rQ)> z2(Br}Bi9`;QF4N1F$zsOmP6qlYg!Sn!`haJ?DnP3_wGOcDz?q)kM;G>&VEMn*IyZ* zXkT&6Hr~Mnr$ptHrF7yTXQT!MD7IL;P`)C@M3zRNkq-`A^a!q?iPm>^*=P~n_{I7o z?h}w4jr}*6ImT9gTxOIobv`gO6%+)E2y1vc^|m}H@R{y^qwZXz`bNVHOHR6Y=gz~J zK<|!zK>|XoTSFm)2+crF;V?UI5D;K4_+Mdb@`1Ns^6llkb#Gr^9P<ex!1rF7E>JU% zejO$zf5XpDSnt)~WH0<!1mkR6D2{${&=}aF`+L79?2Ut*D6ou!K_70y81;_NK>Z$O zOgsFug00<Pkl&Y=c0(UHmR5E;CW>4DDADI?xSy0k*rUI*p7f4!McI>X!O&!F$(Y%m zld7S2tr3GE+Ut}HFh9V0@DO=kUSxu7X=!3gzcI&)d;uy7ZY*{vNd~j_H|=@i+~J+L z475ZU;vkfPcu+B9NRM0w*B=0{|DFCbl}%EOIC)fwD6VYm@gMg(I6zfz4F;YD)v3M* zOp_ce#vw&cLLM1$`L+a*P{dehU1;MMU%DBv;%uTWz9^dxP2cJQN=N`X;{b4J&UHzt zx-=6Y$%;N`-@BtjM+!EsNG}h>(aI06^Y0rCv+xZTZTx|1`sWti|09Tgx0uEYBnNto zSYqN5UCM?Azl#^JLSbf{O=S|!hq-fzH{JpyuHw>RnlFG?^r(CLu<v4au)$YYk84i- zA9#9laklX)PCDg50H~{oq({`x2?oIh1G=#I|IMJqi+onDnFH8QK-6-6AWlw0;;(F# zu(Y<<bI;oY@S2%M{YQF9ZZ$7dp*@-6WNu-w|HY}LjCY-JG87>L3M-NHmI_+Rb{z_( z$5Q{*f$F*3x(oz4p&-2R$QRq=WD)RmTsJAgf9nyI*BYSi|0Wno0zI;30C@%k`I9A$ zJk`2m+{e^^X02d`ePwd*rhBm0XtTQ!NLvnpyo0NpKkDAM4qErDDm+FZV1D1042?_8 zO%+;(^c{>0DWY;7!{@6?iK1+xMG@WNI+^@GvRWDaGIS=FYhObXqmeWP%zgJmJ$RS~ zX`$8K+2VB`xYw3w=+VW_YgLY0PPfJz{|f?ux;ejfq<Q54jNQA2xg%SS6=P<_I=oaR zDYP4~<d6FPuWZeN!Lee_cb*^(IVS1!R#1TH#Pi_s1gAXqm14rCBK^OfI%<fQnH#>W zE)WoN*pUg;Iqy!2qjMqU0D<;F>CtoNi;D5N0+jNwl=~@czi}DhTsDNNlJ~?DZxH`O z&LZ?*bT2Te<>et^i#n3$!+28G<mwexoxdXPIOFZ|d)Gj3rJvckxMUk8h=Jbm^1ebm z>Y82N!^tva#i@2`#lmQ)`?6($dQ^7JY&^%PGSu_HjDs*|KZ%7=e}|1b3HI;U{s0Km zo-ES$<N_=g@j7;NYjw?pz-08A?fG*UrX8K4H+VBbfR|^XW-~I1>NAm&SRK46av+1n z3<~IusHGIb!46$ShFVCupiY=5Y8mYdreaA^Bz~k80RNofyrWFF5&4-=mpQKbAYvM* zS)>EbGaz8hXFo3d^~}X0cmSf~j_BPq&+?gmUg@K_&|m}h&_V%GfLMBJGHi|R-h&kf zL;oFxjK6QwoFgvun=dqvzW*^fHIGh?Zk_8QS5Q}PP1TFB8=p!5%oF$k(Uz@mXlSM9 zUvYtN4tzoS6c+%q78~g8m2g`x$;rsf`c*0oe<5=N#07Dm$8YAm90#WLrETeVO&6ij z>^cLi*_**#NJ9u4k=UObD9)t1Ul`n5Fce#z#HiAOr4sJS-#lslfu~kz91rv4Yu_^& zm+?drBzzd{HU{Dx@ByMs{3BOF*l@-T2sU9O3Df3<lm40QjZ;@x{p}noD5P$M@N|p+ z<NL?i033{b+4m1GUDs2P6y&rAyO|8I?bg`-84|y5P=C-MvSb==3zt6}*Sb#O-``_N zD0o5}JS>sUm`+QDF9}+7e`V7D-4>YhAh=|YTa<4|`xL~?@h?r@`ApAiJ-y-MLwSJ| z2a*g4N`YJwSO_31nDB>&6$$_Qa#`4dds65QUmLL7Ic3SD8RD^@9{1#N!;MoQ*RYb9 z^}&GBb-~|c<cCtoKiBvzr~4r~o`~K&<a`2?1P$o_-mcju7mlH!IG;AXjcvOs=EH9h z|GcG9R`$`DK_nF&o!+lv6iu(Yfq$-{udXp|M7-?o>&dhTd)#K<r)%A6>siQR1`5i` zy`v9_BI?_E@l`=f3;qv*J%;kStNAmPR#BgRdAE0d{RmX(2cgcS#L|}?xm}NX;&!Lc zSl*`TNhMvOC2MN~qRqeC*KoESY&l;KOQ`c&!GzZrV3PiMuRe_Wq3yL`#)Nsd!xAnb zR*S4BBL@ULInx}tlYj>-f;zmo$pXZ{CjXcrc)lhnCfN%QSnN>9!%Lug0%$XV&y#6? z^9(RCkxBnipXi5@j&0YW^N+FcL??C3Z6V1ks)}7K*v&Ejg*;ED4WvJ^GARHafGIGT zaTqG^V#IN(kyt6SUy|U{n&Y&PUp3qa=cI|iMkO>jxgN-NT6D<n2a3IGNMXW48Fq5R z+-A1B=;*gMN221~)iLBtC{oE+qyz-Xa3P5o1d!i$*0nd}#OWls?M#i@7i@GhmB075 zTvE<AfF2X5hi31S^|_pYrJBqipNTiVg14h!I<_2-Q#MkJyO1<WR11L90uNS(ybxK% z>*kG?G*#Wio_2X`GN`zu>;_nfM0fIx_iC5txR#n1RQm)3Qprp)#aSqDL9EnqX4tf@ z4sYYkM9r{A@pSTD5bD@WURU%@6z}aEP*tL%qP=PVxg#>noJA(Ns3GxQ8|p+SHmqkr zsCDzX`{|CHm4|ml-(;!L3Px{V@1x>(ch4Zu97R|U{I+jFr`Kl@QP<gp!rBz?2=-{4 zAe|cMv}xYQubXP};NMXx+&I6n+32!?{5-GeuZ3WIU?A)Tw8V1ZF|r3x4S|F+Kn3<P zd@Mszy~3j73{pn`G&KCa{FQ~fUz;Y6)YCbRE54A@UtQN(6HVs<!G=kX=<E7A(n0og zWZZQ;E5C*Dd%pA??w*e%%#^MRMnf}G1c3I5j%UJDb+VMJDaZpd-}fjmhnObjNdt`t zF8uZ!d>Dj{H|8`44c7z;DT$rvkcLk>;M+B%58=5MDG~847dnuk2A2J{$>k3~!-z=a z(L+j`XMB~}V0m_yT)0j*pvrx38yRjMCKuH-ca14w*-^eTm#mF>k$Y4VJJ9tIb$fd^ zB;~qE56Cp!^d?S7_av7ARWb_Jn{LR%nNH(uc~o`UT^_G-72mvSoh4mI<(v9?UOP^_ z-AikpsyT-QdU`+=D$$mh@&59t&c>DZ2peQw<3P-N(?LTyvibj|6-Rk;VbwUJK0oC2 zJL@jG^RrLTK)Fy{R+?f(jGUy9z$al^O5h{t4yLQbVfL#6*sFr~;ynorfKfl_iV+RW zmS;HKO!$d@URm0DpG}Y?Ce0_ma(|Z(E)Ov489b<hKAas~JUl;pTUz5zDJ+CT4LEr& z7u^G!+nYdaPCVyMO&8RdkXniN5h7Il9E1<4K)&v4p{6UQo5{XtbDunrVv|MG1OoK7 zKtF=;3*qtovfA^4*(&49Sjj8L;cUEt?jK$;=OH*eL5cZzyXSDpN%#2{gy`t#tfA)a zy+uWp8~ZyZcD(F$2_>pD4_eCV$~#Q@H)p>S5Eg&YG;;8J-jK}SI8T25V=(4%TW0UB zrqCoLg933`{qnlzb5q6A*ESAB^}z2ljXqz!DD)%dVF{My(;Q5S>_WxBhH0M5anQ0t zcwo7+v)c0j@FCFaLq$!izf}L(v*g$TI3PaRF%*%;(z)Nq6kS4UbN6Zz=`o#ln!ma5 zlk>9q&ht1k9pem)kPwfAo5e;Zvb=C}>rc7|xp8%E{;Hu_Ie`4^W#A_c6*fE(o134% zy7Eua(rI?to7z*?v83tP-IuYABm#5+4^hAZOuGLO%R8B;*pVh>+=+AIAp-El?!vYU z&}0%!e@_h;;L|##0th{T_hBAo=JY(Yjq3WDH<Ns`wQS7(*yBZ5f6pgJ_u~+0%+ea@ zjf-<9UrnTBpjnw#L$V`XV~iL`IB8CIuytUeVI291{r|6P$m(X~8*_AH-S^$}>1sgI zQL04)6N(iAsF^xpIgWZnX<WC{vH>w7@*50^)=fX)JXHzhtZ#t1&o*b13UsX3;x!6~ zg`E)B3^xw<`wqT&u?9;PVu>`h+{w%~$|euw<;FD#WuV+0IB+M)e11Pjo;}*ql<XQ( zpkqZ!MCms@)FAlL^yf2@(4e62NnABu_HOG-*>U$pTduAX1-ffB^y<#!h5jsZ?T16V z4jrO2&7hdOwbIf1na0IiUMrpozyUa@e}hFYK;?1)YF>l2RXQGlz~JqDv;Jl*De?eX zqX1jla$eaFkk_7Wt8WMi4r<D013p|H7s26MnR!PS;qg}K=h(Tq*wb#y_zKI+)PO`1 zH!in&=8Gum$<P#T0t@f6e7=;@5oY6g{?`x9AnT@s+t`q|G%Uen2efb5s9pq*3$|`b zYJOPET40LuPjQ=K+ZjJb;%{kdOVU$8%&TnvoquC8?DCQS{I)(pUxNzhNMX6I9gdxo zI$JPNQY`-nZ~<bzCwo~8t_PtjI-2|SCd~MCXCFTb4ZgMTj>?ghv0@zRnr}-|V;JDJ zS>AEvj!WT80lK}a&yqF7`D|m|80y7sO_8M&#x;4;v!40(y!=+}(CZ2Ux^qn0OmMO6 zr^D4#!h$e@y5myyWbBo97ixTClEIJR8KG)X`1aQl04)+H<s;=O`x$h*f2ILb1N?`~ zMsm`XU7u`r`C@g89ag9a(9L)ErTqx#6*O5p`)QgMpeW3XYTx<4*w%aiKWsDrRTIF= zGA1j|k^p^!AtczqIFZ57*H;=q{~r%ck*M%%;SB2SxNsI~4xV5N!Z<*S05$J%u@+so zukcZ5_0wa{l#&2Y7`AkMJ%e`h!OMW$L0I^+MN&brJ=y6t96UiEw;aXl)MIxI#lBU} z0iVT*$8f8Kb<G$5WN-kma2*WI2jyM6KdkY^WW^oO>2@cw6X;1JU}PTWJ!Q$h&c_~= z9=BEg)oh4Y;X!Q+lRKi5_MKFjx#R%xzq@DPakv72X1%hNI{e$a79J?m8QMhNrrpeo zj)v~Re&fk=ttfM@TW#G#$brpN`MRWfdwIJlYcaUP9q0#KR##c{nh*ir6+khGN<fty zGbO$w?assW&BFJdhr~QH0^!w9Kmn|Ji;$yib)L+!)&&(K?{i+Kmavec0*IcFC+PDD zM#))U0l>G8HagTv%&Ms97E2D%*E4Sz1qE3;ZZ8BvwY{vae0=}(N7LRjNeY_ii91|u zH&LrI$Hw5NDNFW<D-NK1Av{QvLq&t`2SZA1zskjXKTWOA33Rc|F>TqH>;`*&_=OdW z5v1A8x-KMBCwwqy08p`je8SDPQ<LlsF|j-awL0a<CD1CFsI22v5-of%(5B03fAPI5 zzzh-rZ2AZ&2e67nL_oHmJ;1<!*Ni$!*sLwrLbq~YFtw&aO=-U{!kGPKO%QvXRB05b zE<1>}C9al>{SV4gF7T;)8AvQ<fC`k+;As!qY)}9$qaTnZB>)fki^eN|5CaK(cp{*% z*c^-Eq2TocS~hf7Ke2fLs&djyrJDV}iv3SOckBkjyf@vMvYgzYLmA=vVH&g$yTxEe zS8}r@I}M}#d)&X3^5tjg#9tnf3w<JKYJK8dh@w3UOYz#;M{}B1ZEYCup53qhRY;BZ zKB|I(u&;uf2lm2XKLL4V1!Q1tBLz$ZxHPk}<d&{3m6nF)qSroCjri!#QHD)QDC5FU zWC6`{jt7K{XAK##u3;Ogq!ehek^qF*BAsbofI{gL$&bJQpzpV1Xng;Fl9bOsf1z#$ zq)RJpiPi-C@!gxY|BdiMC;j`R&Nl0+$Wn`!qUU>G__<Jv|L?+m<2w@?K;`|Pgl1{s zYbon%&Kxun|HvwBb-|))8TiDR1b``b1t_ee4<6K<#6O1gfeb42gZO*Cx++g$yZ8H4 zdtF<O_J#-6BZ&CF)30ov9bR~3|H=`~|CGP;=~I4s`{~!NdC8{Q2YNCpe2s`upEh@Z zoid$G;{w!%G>rcqQO+8%3EYqzR5~(RDQn_}J^8iqo4tz-Jps4o`w1r~f%h`(JRJF1 zUQIr(8qjElM1L8bmB8tNPlC7TSdZ_WsKUy&>z4bdy@!b14Ni9XlVjNQN=l%aYGKkF zk#+3O)Gs|u)g3hFZr?nt4AqrJCP)juIVVsfg?jc@;FZEoYNcw`_<gw|_ie6e#adpa zc~_N^dehX5zSUJOrBX@1ABuno2CPR|c=+>Y(|sW~HzddcaT1*kZoKyr_WPXmgZ>UD zgI9?|8Y|0qdE>shK`htf1OL(j#7pvTHk~#EY-;8^l>Cehm&wj4iGbcpsp=8m{uVhN zea5P_M9wr^y}2#u0i$QfT*3nUX9Q#&nY;6Ls?c&m8~RDX!X18&)>k*q@>+5;>jw&2 z&e4%CovCa?Uxx<wC25Aj5L_=wO%zCM$QIG*Pj{IKwZo4I?COuTe{`hB5Y6@=$CI)$ zGU^LL-dkO5xqE&_5YW*fykwzistFoux>ntp1ezlI-OjZUn(3va;e|K!<(S?&Xa4@3 zjH|7_pI~ZjPHJ-?b1h<4Bl-BmaY_EBv@Y5E2`mJem)YcBemInHa!Whd6NMrmB=+!6 z>>mKSHNVbI7<>?L`|AjX8rOIQpjoQu;aUrP-%jY}aW6VHF-Aj_6zoKgQI}1FikOjK z{X0mD1!}?d#7ZWKFSJFsxu++v83bwdN6=H8dj${EMiy2xwZiWj9{&3`O{2Ag1FDhm zi%b9Wk@vr<H%C?kRd7~&9|(ar{?~GyqmJh+TksjL?^Pi)c~VA2DXp$y7E>GHcb&*E z?iBK<6FO{IvAiN=&p7jl>~S2Jq~mvzprC%5G0jKR55qO8ic(_PDhlAe@wXu`&?4z5 zdE>!L$_%%jURR4v^clYm-@cvKxG&XX#$igZu{0g4Z0kk)VfR+Q=<m8`0_!fOC4Gj# zpY37G;ptV+<$T-D;eIf)P5pOe{yqmUs!)nEeEmz2i*HnR_R9)Pr`}t~Y3k?#sTV{< z%_*XUwdW(z(zP{`E6dE=78bE=R*4_huWy?89Hvv6-nl>_@quyEczuhmZ{1`EH%e0G zr9TEdAT(W^HTpXeU~(|w5%8u~1TMt}lAsMr>mqH$5%L~WlBaKdNy#2E1-3d#+sFvL zun==1RYpd_usbz+U7b9gUfT??j{r}FjRY)uunU<P6}GGT{P>eep`(uU@a5BD#5Y!# zWWu}-4&u^K7b!~CE8ur<uxr^~bNf(az^vg2Vx@0vogNRpP_eY_)_i>&V`kgKmJ_IV zQ|x~tbzMq!9fARjR8v!3qu>>as#G>0;^4rdG|raD88Os5RsoOy?l8BnR|`qS1?g$Q zf*I7VwnUh#c7J5mVmyF!tP`Sy7NIb~RGifpoUa!(1*M&MQJA1>R`$gUXv!3o#}1D? zsPGn2{5yJDt20nSBqx}EoD2Qiz&qMzW424nRJ{7X$=<gCKfYyUsP|xSdshcD^M1n6 zv{RY6(wj$%XLt>NzS1?s>s{whvz2o2d%lWq?Mj8PP2Y^`Y~65QZ?NnF8fF1(z+|Cf zP2wi<jy)mNWkFN0sPS$6;mnGnvf{}EAqMKNftZ#hFSeh;j`S0c#??KrfsHJ7RPsS( zj&H+1O!I`TaDyqv0AVaN6$cXsQwIazMUU=M4i+VYeztY%N0}qi>*u<+V{+9V)z28s zMBeqqWJJ6pNp8IzMMF>2J0HCOisgq#Wz5d{tTk#kic(W3T4u;kzGDhKCG}&72<pIq z^xk!&NII-V)jl`q=^@{0j4*L04HeYnJ`bA$nP|{<?##sA0w3l&@6DyVoVGHoz}7Sz z4leH19>c>#Z3E!CeivKg{37PY+|i_^CeH{(%$g2?n99nm;hRlfAZ!lbe8Jre==_{c zLq)$9c_ANn*Ut;+CqRcgCYZWq)9Qo@%Ie~(L#Y$bTDSM6)eWm_xlT@l7rS&BO_772 zT%@DAx<Y@8A3WAbVUs5oyhtq)){oz8^nfvCR2C8hmwomYAOtyY5G+fHUy+50YedO7 zDdDq?SLCZTMg>&A2LbEUKDp>`Bsuc4$Rf37S_JStZlbT*V*j=9VzYKfr^MAdHw9V* za}XkWbuz(~AE=?h@$op?4`=W8n|R~84wA%sA$&F4D-Syl^FzKaGHIo^tqj)*lSNB4 zTw$K+cdvG^g&gN}!4D&X_s7@%cs!biQ7}#@Brr#vG!MU{QXZmyBSXk#@SXNCLC{D8 zjgiNt653Z+&%j5KlUKS+aOM^}Y_WU&eGZu6klPvI-}Mw53W^^`vJQsPouW^C_|iKf zwrXRb;qcL9jC#zdC5OtlZd@;~QMoBKeJnH`bM20Xnr;J+kIQbi2np?(8NyV+M`(Jy zH;ks2Q5QXi%w?x-4=c%{1P`4D5IY?ojf42^0vBn^J8)-GAST%D_6`+9Dph+cL6@v; zJ$KSwFzJbevQ@tU?Ij|qk_x!wZbNn!TSa0Fl`3`*q~Ks0=;hAJ!axQb%p^TcRyOuh zH!teN#)}(QwFYd~cZrwnuo(qv;PDOVrjxo1+6TYVG849sLU=+Zyo8%9>WWeUTE*4X zjE0WHGW3NU4;5PTSnrJA4O+PpqlE?+tvK4fJv(zc-JUHvGvU2C-Dp2rQSs6iQ$^Aj zr8qu%d0-wcmhgD?wUcj}-d(gm9P%?Xwk<(Z&`*PH5A1+t)u=y=<hb0_$ul*tE#8(j z5=J;^Jfmv5o%YmDuJcBq!iF3BI`s0%;pk(|<@3R(aN^50c_*C56Exlng`@=4#e_YZ zsNQs3`B7PDG_5ol%qKKIKbMo2)41+%P<!xxqDZ?XP9d9BEJHfcM*s47ho1~Ai1zpp z1ccCn*&z<5#G7Lz%5ECty<RI&fi%3AC$HO5MOWP2*u14opWeaOHn{PBrjVGJg#FH# zIL-HqJ}daHFA310hu?p|W@d*C4i5e_UcLE!rS)u1=gQo|LLH*4r4SmNa8m&pe!V?7 z1>56!3OO+K7E1{Diu)-$1{Wxk15QGf)>sBpD^|jxF)*#p|8=Bsdt{jvgMbBU8#W7t zZe=WknLg#Y7vC?Fa?WS--t#YFrN~=hM3Kilh}=9}<iEMB)+Vg8va-rgS{lq1uv8HH zb#{Di62y|e^gkmv{2eaCM*pn##mefg)IvlXJkEsu%F+>uKkUx*No_J@Bmd@V8fl-5 zNNiwfbzK4VLlTnpdCk-pA8Xzl=h{cmgZ$FS&MwVHk(T%WV(cr!vg(?)Q4|D`4v`X2 zK#=a12I=l@>6QlR5JaTAySuxjLAtxU`&$>@Z$8g^9N!QAz{R!qUVF{VIp>@+%TGvJ z;g@8`mQHT(XRy4PIVmY?=>{c>%j+R^q10n`nul?k52#4g79_44T+b(;cL}(v_iCQT ztIw)*`7kEqjGGfCl*0Y&F*WGBe8=TvLCEcTE+DZ^V7Sv+!9jn(To8J-gVU5zSQtW@ z=(a>Lc<~ZJ@AlW$N7d43^Olx3xYncD0VxBp%qR404|;^Em8M$4rW{AZR&0~0I=s_Y z$#MMvRkU)KPcRbf5WMEsZh^Bm9eiuuF6=YW(b<u2yaM&ry#uE1%3=1xi}m&_wCMP< z!C`rOz2ySmY4@l8+reM)By9G7Fp}K691aZUa`l<b+_`QnDPhtAR1HWfRbi+H)tkLS z7X*z47ZDvBUB=FsXm>}c5xiRLJ4k$cs)AdV_15K%up6^Yl7%erh@II8X3GaR^TiNv zO$g8W1r0b4vk}Vyj2tjA9YN=ExiA}%Q2+~Ex46FE8JgsLR5}wI7so2E)a;#ub<KRc zUm&VzdUSraHYX{w%jT6+^}S<e!bv^U^(cqg@lwb^O@?K*`(*7}ZMJ==YcTFdaWe%I zZGKK6DTh<DV(e=w^?F}Lw?>anUy`TaZlj%?zkS<F-SBEbL8-2;wZ7ft8AxRPejr{S zpt>rEZHlF-i*fan{(aWfFtmd<Q;hPVI|1lZ$dRf=cCM!z)_^}QIw*?RbJS1G$-xfa z>Kq@dR-@Gp<>fV;?D&e@#c_|f<nVBZ8>7m6<i#pUYc*Z)=Gln0tv+wz??G;llqKoi z-#t1f*NpELtm}ADY64SRQMr_=<7Isew>aOTQ&T(4a!`E~S5}s8>pIA={OXR6N&0f- zryva#E2am4t%f$!_HW+2IXbo8a9(Kg!Ef-&A<J7H?J!{}e|Xj}-j5Kp+vPwIkJt>H zad<Rt?|HB*Fp0yypim7uapU>$6Ks0zXU-1zDUFqh^U%BypaIM%^45eg0z<x@^I;*n zpCoghDKdJvjmpujCx<Tbqe@3Wn$K6KI{LNoXGFTN>c{gSHs=)Nq&YN6?bo_bzrtc5 zvR)z56$-1dLt~9ml^635gxi|!luR5bjQ{xe)z$>A8{iz=Ug<7(A{gGzhT+aS#5WB- z_<M^`cC=o(J!!c1J!~}z1u?u|)m=&K6SF2q{(ic}e2;s-^J^qY=7C?obeXVArc7`5 zBp)(`iryyPjUmy*iLc^O)~~uE5wNtx8y?gxQy+C9Ch47zE<kH7B}IV>Gt*DQcKVa~ zp4|aX+YSnV9+>XA9*A3YVUD}wEo^Ru2`!pRqCL#Hyibjkm)0+bdDr_Iusp6n4kEP9 zl`4ohtc7f>D{Ynbq`QA0JtgHyP)Dj%I^X7?khQp#Zs*eadP@qla}T$bn5VxQ&;1e6 zmt2$A-1_f;Kl9{s8g@VQ(I+b!rv`1QBZmUeq>}Mq)4*}NQz#HfN+hxf^yHMyGEu-` zVUhM3pt)ZAX&g7gh3nA9i7TpYkrIA@5AX5s(R)YuA+y50r2>5wLsnW^{FqXgCPrnR zna+S_YQ|pkbeb0A-e5%2o!==DcabnOPdeV^6)f_29@3dz1@2^ntf#Qwa0FZp2`Q<e zAgP32e_%#>M*d==AAuqnQ529E`cxHRq#d<VpSeAmFVNI1(EM~!{3&x#OmC`*SxUww zk`TqUX0J}<SA>Gj*-^V`qctA|_0CwFrQ=r(CHMK!-Ue`Uj1Qdp5lXHcR5i;>D;62m z$si+faF`e`qQs<U#5xm44y(k#rjJGI3i**x6O$4)49^cTlZ}XIh6qNA$WlVP=Vkbp zAfXpY@plOr+vbrM=zq_hm!I7-u>e<X-{cXd+ltLf62G0RGVgB4`k}WH8H+pkc;s_z zEST5xFwi{aj1*7#fw<hTYP@Lhf*OiOZSA^zY$p78Bv;gYmx}U3*LK>v5>Wz=+$<T_ zjS&j#Or_NVF}KXy5kiK3k-}b&l(tR$H-e7F*fq*q@cgTE|CPoLjbxjG$X75!y^kfO zIq}J+>^m^o;}A?4mr`$CKX{`rUphO+;0#G$8iaf9YkOY8UYFV)sci4OF)}q>?ryoo znJzlZqtDLGPpQof`K6C_jRp19sn+INZc0B1Ya|UPQGr<|$|AESCm_x5xTk+sNN!dN z&ooRzYw?G6b&aFc5BK=gR+r~Q0I1rCMl)b|yvm`#@a<lc$QFb~y0awNQOG#Sn!qgC zU11>7fvGg-UZ75{&;JSWc$&AXcEGuOo1S*s-f3D~c!+AHb~|Q!xYKmc%&CbHp<kwb zjlgl6#!g0^x2!nW*yH{%k<@d0+qo_z)PZPlpZBmjw(-Dg6xuv~{~StWROn>NZ|AGq z0Krr(?FGd{qMza5YPYA0#^sg^=7a8g`h&O;rwBgtSwn)tHEz)nl_qz`{inyFSZ-+( z_1Hy(=K#~8&=SFT%myPoC&J@(C0VUa;IxUIx>x|nebpVyH8?6NR)v-AET)>yd5ePP zpy!I?ysWNi<Zb`(q-atA^#xwfY-tcr+x6no*7^6qfWrBbdW!QH7HUd;)`uJJB2;Yl z*c$4_%<!^ucJ-JaJL@}G&4=BuW8dIRmAVyxVS`t`I)X?yf<E5Da=S)A+F55)mR(dN z`*vg>2+3!E9QBM&h9yX-Y{O@|>>xe|Q}i$<zUX{ClJ}utq^wt2X`xpAH|d);(5Qc3 z5t2AwT)`TcA!v&F1_}`82NENmrMepPT&MfYKZs#3N({T~Q+0$W(2W&q=?IzMF>yU{ zKtc+^ofQIb1fD-VJnrI#6xS_K*3hmoVCgXXN&V?{sEBYNp~=pEFV}hdQR;Z6Pm|LM zDaUpAf!0}L@t5(dPA~48HNIa71zG^IMY$$4YjL~X&O+DUUP}ffA$QbSxPIp2Bc@IB zEp?xe25|ie*sUa_x&*|6uSPwsp`>YvpLF-kX43FO$Qevh(q*)8C}Gt)UNBZst)(LN zwZ`se@t1>j^POJ9R(yx2)@?=%G~|R&gF5;^GWS)5bCdIJ@NAze)b4`aGY1F7RwtOP zPplr@)sZ?x&NG-kgnJUB@vFYy;}{}-=_dg*XIyYTq!lxGMT`<gU8jK77NnkIymWQ* zp|I*mYduokR4rUwUjPH>sg6s@a|anwgTE7WuG`IEM<DZzkIx0aF33X-NnL0r{HoHx zs;<`EbIVcdcP>RXVh-Y~xzYm<AKczp(C=fqO2e(6x&Ga2YB>Q*5y7}<1CmKiNgEnh z?tjKyd489^bl2xnm9g)cz+%SdC`zyXB{?suKh8IL*<s<<eNx2b8yX6`fn|n^Kb6#a zD~pcttJrWuJ5>mAn8z9!z}#+>5-3y2h1^e&Wpjf=8#s$sYEaYmSM#}sw^fI1m=dsE zI1avihaJo-$m?1*C44&s>oHg3$092;Upr??i-u3{?|x_Zfzt_gWo0A7YxNV6uF7?n z>EMVQsj+5k5|L)_<?08Q!_cOm5sf~9$8`1i^Y8+zY+?;&ALkplhet2~g0H@ui%8GV zTQs(7N)K4-7+|L4u&G54pW<Ke<cww@ZnV1r`-5@p%v%VlI{j$TQJd2X!0o;RO%?yp zBxXWwPtBSuE)uubGfeBpbFf)EP^{7Z(P6KH1<&`pGcq$_n3*YAA?Yz%+4Us~7g1z< zaDhuBmydDPT!s{XAz$!%j>Ct)tlK<=x%>TCmrB$96g7VR@(NJ3i?pVrOt^giA30kT z$9PxQ%b*UiOaCWqEQp<Lqe1u3ewJDXRQ4NsC^#U&f82*F;aOqfB@yMnBFKS|TUztU zRz46<80T#9cf9KC=uq}#N%o(V#+|GSitjT6_cynh2zNEDBN*B-+WWht4VKky^~cjN zDoNMp>1Efcjh-8cm(Z{d1ctw>PDL}*0z*t<?jsXQGR``kQ2=*i9Mg{}_&pA)`C9HQ z9vNKC_$71NQF3^hkK+9)eWKIR&DR?zayi(sJV|$#+oxodto;FLLg@Yw_-b*x8X0%V zlu?*kQo`dY9CdDY1An_rdZK)KZYU?Op~Q45cnyEMwC3P=w8iY`xc{x_6r+23#9E)< z^*KLo5>P7AW$Fh^?jlg4P>_?fBcASY`{Os<NGU7xRE}3hQb~Z1rKbtRs}eB@b$A1W z>>T#Pws3<#Fq2JWBD3)+(a3OXnl5cjKV&hsx5=LqnMvfa_Ap0AGx%I!H44~YWBwjU z1YlV)MK~(G@u|9^@Fs%K+k|g%aqQ532)ctIxb}LP^lVnCypW}+adf;vMxhVfHA_0V zx|TDTy~6V$Qjffb_ZJol3dZY5#A996Gnb_$G=rFRrm&cK%yv1|8aF&&dbpJ4ld!@M zZ0pPKb>cAoE>3&*SB?UO<50Yn&6Ydh;Jk8*JcK7Ort^@Vc=E_aMp>;6J$VdkC-iZY z?W82o#awm~Pf@O5{wRLpZ_MeA4udkC$dv#&&2GnK52b7ZQ<17TpO=#a(EZr{@~=J- zZ8w*F4f!+QK_m3$a2<6-1csj3jT-P?{<c33vtNIdkiea9@(V1Lfn2AZD>#YcmB*{t zZOFV)?fCe3Y>tnpas7hk3cgRl^Xm|xl8CkFF@=N8S4#gL9*CUNz2ciJh)7L>T7NL5 zaiLkuI;s@#cLWpJ?2s@ONUlZ3l%WT*ykq=;7B)a-aQb@wuGhtyZp8S@msUNx59l>p z5Vus@z8aY$v7<<>2u7h1;k~2DX6GSHkyDhnwp_sfdtdya!Cl)cqHgoFhy=4)pTLFL zgys5-psXu$!y#{7n=ZzmAnDoWHmEEK3k7mi@$a6C%`p>#X9^LEcX<f~IGvQGAZm2* z;KH!4={Qggu@pn_xVoq4vmh@6`3B%Y=7wv;<l5YyeUx#3KUb{u5#hT(%v)w+bx(lU zO9%pbWP$fDlhJ-$zib<@zJt@bkl-*GTCzf_TdCCD|9L|!fT{pU=WM+<tG`0o8yF5v zP9{S<;UuhNUc_eD(PV7gmCM{05Il)Sp4>i#hPdSE*f-~U^BIpH3vEx&fdUaz?4Qd_ zg7^64@j2N+_=e95CGdHEg*#iN`uH08X;Ey<11A5mkPwiobV`W9Xz}Hmw}UR#tryUK zzF;ya4_G=0EUXX6fC@g>ZA}LY9ygwG+<|rW_!bjDJG!qxD3q3SW+qzzcvRw)hU+22 z=C@x=a(ia;6CvY#@d(K)^>bw<I+=VR>)QB^L>yYn^*6i^WV$Iz2ftXa_#}Ikp;4v> zWIgE&cu_Hf(7-P&*Z+KYeAa<_SJ$mE2dGz?E-i@(tPq6je|LCzN@O4hVN+l2D0~3z zn_-B^c+Q15_O*X+gdQ$jC6g^9Bfr_8?D5a`n2v=wVBpIjhA+4dJG*B`aC;ZGN9s2h z#C_>OAZH9aAGcW4;%LZ^CnsOk<4Jtnxk6t3MtZxKJy7Wx9~HsyUn0CiWn3;IL=RlM zHb7A8SdfUB0b6xk0l0MM@$IvZqLx5RN8#cJ{wWT_FYt!wm@MQV|GW2*&fpyoLUFR& zWakygzlD5ERA&SoYP@J%4l+*ee0^z4;Ngjvv8j=kqIu-w9cXrn33v8P#mv=A{fZ}7 z_~&~d-fdP)+zu7ogW>G#TmT*;CNQ|^H^9|`iB%UmM1>hR_UDz9@O+IZWkf$he*EBw z7#tlhb{nJHkGCU$@#0DG7tZ<>lk*`a<SmIoEpN5(fg3dF&sq`d`5=MI{#mIA1ja^8 z0QL}3Q1F71+}~yey_{+SvzCBN(^v>dIuD~G-|Li(+S&1f7c!U79vqxt+qlsV|M#V( z>ZnBegNY1HaFBwLQEU23PSSFQ1Yk!u@B}D8->$Q;pCG5G_;+i)CwmwC#LG`wQ&IGK z!}xJ;;nmdC&|7xX0LEgnhEw*@lDGqUC>2S^vo>LbAj|dll_LB#Sj6uV@&4gu^v?FH zmbshhe{WN|<Oph2&$JD)2_RdzqrX34^3n>t&ceU1ge+gt;ugA2TEi(a%@2|6@k69X zv)?CiWE6DD7%7lZ!kEst>5G24a}an$xt*g5Jnw+dcp1dTJoZ6Iv>miFlnAw@r5+*e zc5cZE?9RpH0WVY7R&>9kSn#}M%*~#)r|SL6-p;7O4dMBFA_F~9+OQgNZHQao28hR7 zMD_{fl^Kin_fsmm3$=$fw~eZMzJO{Y)-6_wXQ_5a!qCrv=QsB^p09U2LHDvPZ#Hkq zzdR)lgg=fsE{IY3i0Lx|=TdG&2n8rtLLWl)@t$y4_T2_qF7^K9CTfzkQFME~-M7Wg zZ@JvKTuL|5LUy@Z=i?hpHR`Yx9TZ2H)7{q$=h|vdMg}!n#}SemtA6#HfQ}m4M8zb> z2n}c_pr`NfmB%?eKU(&x;(n`dOl~MTK&Q0{72Fxw#5~|?OGS((w7k2F&^&U%y)xON zdx|rAP<k`Wna?hdA@5RcNt;A<?M=B;snrI?o^Z0Cve+NYYkFP5_ZiUN*ssZ`DSaAj zyxSVCE8-2?A>a=D#3n$Cg5H;$(l%A)Ou7{}PV07z;6p^DuJK9gtt7|8v;D+<J=4#I z!`}q@T=$gCn>Ot2Ef)HrB{@DB4N4`ieQAqwx1S$|7K*;xMWT%nk8(|lKUH(PoJ9gT z+3j7ZyW!pMvv+epZTs))X5E0+<NheieJIbJI`OU_6o%xE&L8T(yH`Ph!KWiUd+C6F zDJ5q2+QU}GW+znk?6w<aAl+g>C6$ELy8BW9BHmMERyx2*B7vmG3XZr$>Ekc-q{gKQ zY<gn^gCK0L7R#x<m8|bTrh~`qz)Ug_Ej&<J<o=){&4bcGDoJ}cGfJ!a@$&KtncE71 zo!(*7!+u+5c~*Q9TU9lyEkF$k7@&l%Z7cu$)dKTm6?+s&S-*Yb!wB@ZHCB!*o2)mW zkHClk8XWT9<k^M!;KE0xPU?1bV_kJ;CIs#T<h{L3=Uh>dzefK2=|0fBbln*Q1)DAW zadp^ov>p5rFY^{cbSz9uqX!Kb1pHhGP@KfVQdC@CMw_a(e~%-pU^Q{K02qxC{RCF| zR>K48wUR6XQZj}Y9*P@{)3tM2x7(}Vdhhb}y_Rwh<TX?Tjf#+=eh>UwHm;~Nx3RX* zEGa2DyNS*H5}%Mzc$qlgsGz2%{`EM6wCL-5ezvUc$>DsJYm^!Ks@&)wJDJ83BRTIR z%(qxenDlF?$^wO>6jWtHJ9^+A%-x`N`{($Kdwe47LGJ>qtjqe(4Yhs43Ej5bSAXOt zq*T@0YS(uBgM*Cb#v6oaVC@Mw6c^*QE%xX08VxOt9B!YwC?5^OZ|`gawkw|e<9hor z8e4XPnyo1=R1NkvGvS1!B1yU|_8WW=Fyb>S5PmAWE<szG7#%FNNJ;~*KQ4C94s~Nl zRT4EJ=QlX`t?^pd*3$sU<Yj_~odvNb6E^Vvsc*`5Q1I#FxgWL%<rF7qIGk7lWYJWs zTusIcT&fR=fxUsm<SrsEoBAU0VsOlBUj9F{0B#<GT$e~qj{D4@hJuQWE_`fOn^RSW zgG&1pf;w<Hny2XBUtV8Ppx2Oi^#hLvvn9rLJ;BSRQvSm`pr_Am`}y+`PXlor*ybAw zCNhMrQabu5if6FMX!PoYYJLlS&c3^5s$X|OVOL+EhAMXbU4n$pF$37Qs>~VE>goVR z@bgcLtoldhDNkXmKE}x#^dU<BmNp`z%jK7qW=f9`YFgy9n-*%fZy<Rq73V+zQoj)q z<C?G`vh!($1Rv6S&$$;{!{HEYaxj(ih)uGj;a04y?7-LxbudNYZ$L)H!$E33|0jrL z#jC5mj-kQ#5I0c-P8TK29@PrOYx}<dKJmRoEzOt+Cn@9(yB1U0tn6$^A141olN$1~ zm>&bqbU2t2Rg3%RSc!Uu5|}8%CGZ0L)9RK#6r`dGJ3zsTgU$C;+!zFd_g9fK7=dbG zg`kA54?32_V&R*8<qA$fr@)9ts$hKF_OE|Dv}S;;ffmghk%e;Uzn9L7_BY#NE@3AP z@}+K+dJN#<N$ya5ZN$b#m)|IjO)wxpk^S)eBujDO{?EijiJ@ZQV;FB5SnzKk6!`l! zqK_DKq=G*m0!5hU2vL}skq~!h#P0)i|HeLk$X$fzyw?Hhj!9Zl(v9&%G#pt)^2e2- z(K?jk;A|lK*qybh=lS>RQNy#I6cgG-Jc9W0F6sb5;-!s9{e{F5<;TdW>E(-<<!tmi z!*K9NT!#KOfBH%TkOy3Ky`-+&D?^$%PI_+)FS4tl*Vk>+;mzd?xo(XW+TdBduyO_s ze|nqvV}`8ooDh+c$eaO$e@^yFSw&S4?!YIlXlWh4hz<r&;$(lM1~_RX1+YP2&bBy@ zKi|o5=}6VX+c!9GCM$>m_5_hnv(fDdLMSeetS+%CQ@%9h^HhoJa&k(oJ56p;N!&+5 z^N9~A6^v)_!nlH~-2i6T{9P;OsAw1ST-jWJ@q{;qW=&TTxZV5y2cu*DoEVXq+#Vfj zYD<Mk0qo6$pdgDaR;$fsjpGnJ0G~#d0B<HOtKN3`ffx+^M!+2jy7zJ>e{J>J_4-x& z_DXNePuW!h_SDDcq7Dj2OG6`tDyFrocnTn<;y<kJ@X$4V6O;wGR+|<LGHa^7eAezi z8+oHs64-*y9z38>0U)#`d(`~Iin=LWWgGy2a0MQQ4%`=52AezBSH$niN!K#;R}H?* znV@Vb0jWkYOUz`9E0o92WtsE+AqFw4k0`Xnv@v443k~71+W`JUK=gaDFEuF6uG<h4 zjd#QIh2d}r$*|O1p3U9K45gP#f|$r4`5TJg^7_8S15X<ur*yi`0X10A7x-y+xLQQ^ za&fNdXT_NfIr;CSW0@5)6l2Z*q4x11LZqa#N58S(_!(CdA+6g*@raCiCkB)JvstW5 zD<L&r*(OF=fFkRZ8%|GfoNiYwzDpbFT%_s>9o@5Age5e-itC4rOD0P-me6UUI*lKC z1i6*tX_A^CefdDm*;Yafi{TFiSUdZV_F_*H6E$#Izj~|yKB!ER8-)BlAl*sN3&f=j zgpk>sx-T1U5P{WEpy_l)6i>Bgx4(m8O8*Pw%s!^t1t=9{l7ZEE5}X79qF(?z`Dl-- zm}tW#(Uh2o`!js{jx~=4vv7@w%db?#1=8gbb#+$Icsu>8Ea;_|udl9&xk1;t?c+M% zzkeqVqvZ-{0Wd?LydryY*rQfEC=kvHIY2~~CPvJ3;hYuB@_)2h%AkO1Y03_M1GfJD zet2LN{;m*y2L(+?Ru(O8Jd+G!yG(yL%VFof@nz?eZ5(=X!lws~jubB=cZ$af0<fO1 z+7P_uFbf=UdVjyOHCI4S$*RF#R%K?${2g!2&b@K87^QpGzhz1b2E`lg5O8GZLDfp< zv|6}RJWzjlCMhgQk-v$Dv*uvfVrkX%NpfkbZ|H*P(&0VFM+wZ%_usYDtn(4S*+zFy zhJi3_VLehc>^?;Ee8gw#w>OJ-w6yNwX{q({BnrrRK`xrEj?1mm9ba>C49Z?oYF~Zv zVs2Ebp2OT6t!&^gu1&aqzV#-XcioYF^M)#)tqN$>Y}V(yWKZx(Tt^Y_uc9QYPn>)D zZsyQHye>6cL4*SI(?NaBGXhOXjjwxF2HbYcaii9gbt}b)9=2+<Rv<s!iveZ~))JZq zP#K}XlEX4XCW%HQP=D&sJkD>sM%of^(~#b&+^i=2>Qn;`40s)Z?Af8SZaX|RzK50- z(mL0Mq3LdMjqoCEp;-eA^;+E3A>?p+`<zroBah~RLEyPzG_B23fYk`NDRfB%!{aI> zVL!3;75k&Gu(I2%E^+}hL-wQamOYvXStyLf_)T8qeVn+7%;kI=ce_pr89Iuw?EZdX zfu`z5gD<&XRGS6?mHyqNG$%j1)rbKcoFZ^0Fw>BaAH^{-y6rZ6fw{ed14YKE%A>1G z+;q3Lpi;#Q;yv!TIJjzXM^6CZ(lCfpgv{&-?W<exz!Ceq?u@#><#f7uf3T@tnPov} z-p2y0126R-g7uvK0vQ(-s<|jh-J{>2ZfWB9{HT4bKPF4_mJ1|$OP{k)Fz5s0gDJt+ z%F_<}7{`XQ{IUwqgev9|kWB!Q;LrG&u2m&6@+Nfi{w!bohbyPKZ*_jpmv;`@O5Yp* znP3F2HZ8l@yFSeaUWKhPnx%=~Q~^f(`}fCf4vmh6rs^aCzC2@hR~ck~ctrVz$ir4% zS9hW>uX=X~tamM+06tT5rvhM&e=m2zD7VdSPS)b(b(T?;?Z0oR!w|;OFLq5ZDI`jm zJh;DMrK>Vuo)>cOrgRvPdArb@Z%irMF|S#|qb(dI|D#Eakj3N25H@_utJ{-3?h?}; z9rK%ki|U({E_7k6bPBX(OJHH}&DZk17``At;{<|JB|HG6$M6k=L+PT?(c?%|pty4E zVzIVtdQ2RNz5Ik0x7Q`+hlwAvZa|psCIMXmpaMMphS)R1{h@9YaFR@pX9D;VSX%8b zy{LA;&WDDEfT;j~BK+sdd~z#xJ6(sjo#%9OEGn_#Mpj*3WV~|luUo=Ev!c8Z-<?g& z;a<#%Mz+EY=-DuD*C@!50Azt#2k^C!|IpOzYQ(E~Txnh5Xcp|Uj@FQTTgaY7M%j`& zNsO12ZiyHa0Mda^_g0LlXgBDrrGRg-AIJ1b0avE|3YfLdkj$Ez0Kk+0XjPM=;j%4$ zcT8<9@yFk0uGntD8{EGT-TkawRyjJL6aa*yfc8ab8yyX5{f35~@KNfmy1z1tKXFhX z-uJPtsBiTvxYJF3ZqR;U>tA*L+Y-0ZzpLgH46^@=7%y5Zv#8z;XmUVxr*-MhQ7Qgo zu;`nX>eb1-{n5Xrc2V<jw=X8!n{(8^>h_a-d;T0dwB_y1K>p~m^O`B$01P<q*t7}@ zjzjNFt>Px;F7kKta<X1XTWIZ>I<B~o#Ely^(o?cUP#qcn8D0~Svk(hJ2fs@cpDlNb zk)Y{zNj`lm&a4yEElvjt+FzX<54+JEj&JiqQRtmsLwUBi-Cowfe!V(NLggk5bXd6l z<?GuyzOnXK+ed$~Mj-V=v2Qv;Q$1?IpzLTMP86^xt3DI>hUc;lWvrBb4eW@THccS} zlzIcR&W`IHLS$4NZ6STG1v7;e7VKq6G=}@G9E=CrBg}h?N$ywP>`hU9^$VOl;&zlX zO#7FeH>4qy<m<O`q-Swbw1mr_NFZQa$CX^uMY;$QDfqiyfhOcvX~WIg-Y-64(q{z? zRGJfT+oA8{7Xm#XL=MP0bM6MZXip?%<t%;wYJQIXQw>;nbq^=<KZ@O(wHIc~HLfRT z7ny?|G}o_o1uTuA2B<Jw(_m#RQ#WxAl2|IcC#Atr1oj8rnT?GSp0+3;oj{~6S0uPT zC1?)wg6*C@W(CNJ95_d`7*+ccD{&^Eg8+Hz@7~<(!d$bt{k@f`>x$4KtTp`zO~>ye zWf%-hOvf{t_XTH$z_UE_v#VRbsjI82bTcc7&Z*gKD5|i~PzV)G+yP~oP3M<qXRj8% zKD3uODmQG+xFh=^?&>>8Fbxy0U;s`A*tGcw10-dt{j~;ccQup7Woz0^#Ogy*ebnz= z!+k=QcTgUh+;vcK{o~isr6Zp~g{|UV#{BI3_V<p{%~v4W=^LeXBLL~U?D?6~`JPin z$9=$xKOpOYE@3LMM!tg|D+Oe$?}HP51W)N`8`{GdN+ig<B|84@_*jHi-npo-J5!3M zeD>@bs+j!(+3@pkYDbCbT-)5d?d+UF{r=+u{`Obbzjy5attPSQp00eN%wi&s@>~)( zC)#L>IV?QohNKoA5CT--NP{522&Y?o)?KMZH-FgY1j$ZgSo&5PnCvJQd254FHOmN5 zeu2HQenCyy{;1yh1Vr)R7TW!J>W_YNgL?ov9w-nY!99SpSny+ANky@WmLig<lS@(p zJl=9=&L<vLTrlPv4l!Q*vjCgyTF43n7Jw4|>Aw6rMY{n#(2IVuaez?*h5c898GTa> zbJXGwsr!whAX~?Kw!T;<L=PbGm3lp*kFiQE`vURpJ6m5~BU1PKy&?#|aKUlRXP&?+ z>dC?)Zu650z}9yP1l;g1k6EiNLLWUbwDd+M+nPZxj=f?sSZ{p@Q7s<dXDB*P3~JNR zFi7%bL^{mBMJZw)rc-;(otI;ri?6?uzuQ75(pjc?BMvAzQE@v)^ZotDP^ueEV+WH& z=@{B?Y*O>^!0B057Q~B!Qm1uxv<hUqvdAM_HO^n@v<{I0uii5dFD4iwB1eWOHX5HU zx`jXO<pvESt12tLz7Ap^C}X_hZ%^FuEs#$Dh#m|mfFxm5b~TS_F!XQ<P`5}dd>g+8 z%;U0DXNY9y@@o73-hcAA%*umYcZV+#((6wVx<YZ~-h!Qq>latYPD|XSQ#LhOY@Y#= zj-s`-nwQ<&ZhL}ekItS)B(Bj<*bp5X1NM^yV7noagJy+P^0^k@Ho3YR25{fd1sCFa z#sNwgXheu4F8sJ|_nzn&t7YyN5+FJgS(kB}fZ~?w$K8dxxZlUt&zF<S8S-r*^KMJf zNRPIbABZXK-h27_PyqKnt>9K0)<;<fc1s6$bJUE%`(cMqN;~IX&Y@+8{qF&gLqnne z!M>m9-J|#P-@;2?jJl2G?cWeSFCx5uU@xXUR8ocvQ5cXdy`SSb@I1hqc%;LLjw}8x zlzgLFwei25Eu~Gj+5d$dc`^KuQAX*;jO!x(<-y7u)%u-kH7BS1nHc+d<QNNCQc5=S z7eLzzMwAG?=hrHUW>vSXw7a>?Nwd7L?3iow0RoPmsY)!5fArIxeQ?o~1}?Yx!3nR$ z&BwWX$Po$2_itU%REwu#G5n!N03|pAD8ZHH#yKE$1tr=Xusa$f!MaUWlf?ZFBnKUG zYx?qXit5$NTmQ}RLH$ZBNG>S}Tev^AD42<Rg9C@SlPT3f=<R!!s%q&F_)^mPvYG4p z6>h2&WP^na2Z-FR6kGwN+q&Ga$WyOc@QP{(K|hGgu?BfrQjOo@4{#98-R?J0g+L05 zf_M3bQYKHd&(2LL>D1=p9j`kMbZK6rXR@RpqFANhZlGDyRpvxnegeK*9nkFTB}ckX zz^;R&x;P4|ilH4-yq}Vhpz3UptTI<eB1}dUG5~-P0)fdS(&9alIWp8-+tZ`73+<_& z3=mz8d8M?R67Rh9StN27irs!=Ks+cf?f8RYU2Xl52>qqY+?x?F1$TW8X|u?S$eIs0 zT@*a#=!K?oWCCsr-qxpZ@JCInwEqaDlQU}*4>zBP#ZMpiMt~_jDN_zW0Q3eTlzxK~ z`{Kp1?)DxWyaH}#arPrpzXV8KbPyP_EE!_EmTVW**0O1(f!pb#1B=Zzdedb;4gxlQ zj?_R5qZuyLLD6I~O?oeRw`L|X?k7m~cF0r1_1+kyw#S!)iNv(Z7i4Ux;x?%Ae_)7L zNXp|UM`RQqQ@FT~bms4pb0i?fDyMUd4po_1kd~P#2&t;g@()`bLG4Z^O32EFKZTgR z(l_4L*9^8SXJ>YLE{z(0Kjg{NVvF=Dtu{B%%h{d+8JKAZsg;bV&@L#=!}Ebd@JImE zdi6;H6xATs{Rsq8K(PM?#V>S>^+$Frj%;}rwFIzq=#L0?WqJFvnoF;BaNKzM-<vcN z{`!onuy7TRQ>-UP=Q$`fwI2mbLh8y~`m9W<W#AQ4usLVObJyQe4Gvxr$Bi2}OuE%S zQbI*@tL8w^_<@|vc=Q6f=1tjDD@cX_)Xn&NuZF_>=-?3mFgCadQ3EbTvD``=(-qeH z_!x&<d$--_IgvG9>N2RmnGFIB3}olxd$oK5M-xD$7bt*QTTN+c8HVl4#v_SFaq-&i z59*4mHF3obpyUa`&bS@`Y^sr49ot6MUm%TeYYy_2xr54j94|_kX#K^7K4^Z5Zo3H$ zFExGnaFDgoqD51xH%u;pOW8g;lklO61R0zCg;9*oum7Raqyw6#cMEL3uX1AKe#z51 zsl8hv=_uXY8|)sWNL@%yoW0nrvb3&LyKpPGwIv3S1#m<`?jF=t{hw10Jc`HnU@!0u zm*PYrVUx(<t-~xQ9AtXxzd^1_c&0Tux3`eOIg+DLXtDbQqOZVrDZotd<B9@mna@|K z*wBz_hN(cebrl-~ZYrybHhSO!4TZmI3X<?ccrAyfTbw0YXh*$)Dpt4jA5{ww=_UcM zlP&b(p(h&X$Kn#pd>Q`K-he=BVD=`vC~w1E!^Lk!2>$ZCK^9o#I(3nIbLDq-;b_~t zCewf1YV%xM#Oa{J;l64T1m0?wl)KFS<5~Z|TU6|t*D?bG@dQ9(WI8yVs?++!C^o!X zKR6EP=gP+*>6Mx+j|MFU#@M?BI+*Yt{-pa9{!RoE=NX@MwZ9t7*9gAjN#gNr@D50d zE_V%E^lyXRpq){0-*}V-`5bO~qQ{ffrriCjfIY!%j_ZE*rsh8(?Ng~}_t?~L^nKB+ zPs%!iLz5y((9(MPr?b2$nAQ3FYTVFfY!|$Vha%v@dS7!=1vp^Vw=~YNv&JL|T@2xa zg)hvm&qvJ*?Ru|>=W<(su37H%GivOPV(_HYsT=}3kLovRF@t=ruW{mJ6aSB9m_)4e z!HFQyzybAnPyv+eyDi+Ws>b*JLfFxkh&c11A;=I|nZI(w$Jz}9paO$#?WSE35mj%= zWX!u9fDQ#%xrQjO3ebV_42+CbnCRfl3ME%6^p`LQX0=HF>g1oAGO6z9{{Bn>w}bsh z<&#nar)m&_BC}zl+tE#w8Y_zNOQ82U%tkSts!jyLz;BHiI<1AFSwzS+_3b^QP)bp? zKi3Tk8iDy@{(H6Lko}C`bgk3J0nDdQU7;ouoc-zFuSIc?n8~w^$=g9V9D^$bqGc6x z<O6(6|Dq<O-pF+6|EA>3t@1Aa-VKo<<l<B|jDpud@tWh~H}jUc4^$jIh4H39V*{!y zYLUZ6i%V1VSD;11kx-vYLjlQPp4n$8agSM??7x_VqoWg&cOQQwBR`1ZqtWUhTht#T zeaYQ{3&n44&!KD@0uvAZErd<>qA(*Ow}(*HpoR`O`K8_6RH@xxE%rxek|;Ipeh=5| zqL>YB(QrK+N2f2<_ZPTLrdgmM<boGZZT`1gefX9q-risI8wb{F2p&9$Vb70F-J1k$ zXN{igWC4}yDtRZVB<NB4aqswMY)l@43s*@3xF(vZm__s0rszQssR{Hj=RrUW)sZx3 zQf1C3Hl8kkR43#ejo+VWs@hJONpjdAjRz?hp_Y)M&{{#txl=QhheIpe(vP5U^io82 z3N)sw{Dud?Vq`y`E0N=2TNj=eSE9+&>!{qxrndUWw|Wj<CnIco=X7HUq6r%7-LGnM zvn<69SYU9)-<zU&9n2r%53S|AZtxfpXj$v3X~?xXpl~MgAIss)B6_>RP}X$7Y#^Kl zkUQadlr!f2u>mJOuNq*W(WX5j>IhMB@$Zk}EgI$(5-H2O;1T+)sS-{;PycR3m-8i+ z`?7Zh>I)~2z*S>Xdl2g?bC#21i`j?<*abi@GLKI+F`51WRO0icUK5Rc_^ZGh-*MUv zl5Dt&Xk8Laq#jWikhJ^V|3{7#f@DoyK(R`YI3|u+T3sHLmD1Z77%Rl8g0VRfkR+>$ z?${PiuZWoW{s6lNfl-QD|0XR%TcGf7fhKJ-BD`b$%tW|x&EAJ-#0LW-^01|KEuNf1 z^x#R#SI3%_b*m+;liz`FxZJ3H+HOvYS(IN#F?_S?DS^t@xMqzTtsEU1^3RH5U%0ua zJl(_-6HvxTqS9zfxg!Nlum(zc+>JL{y_~8LjILw-afbeUcxb4Zo|@}7XyZCuT&Y5t zrjG_an1-NXcz4dx`+C~R3F*aJNF6B<At@afjXjp0Z{2m7;-Z~qltBHYZuRPY)^DX! zBMj0IOaL1PeyO5?1k|?2dE<YP!q;)>E;bF<r(Ipt9wR?T<5`@%@m;UU(ILRX_;z&* ztT+*+zE2=r7*&4FR42RpeRMT#8#cGYJJ<`nje`?zB5fJb63~J7b)z55Przw?S;Q%! zFTiMVr%n50&gH1MHoVc1^Z{@b92|2%Y8f)6*AA$TD(<^D{DyB|&lR~rzkHa(>4>q2 zA5y#)Xn9mWin`SGlcc-ZI(SliI0z2mvBv44UygA+(1Ud6EfLJ|fF76Ld*}bFYhV+m z`6$h&FUFY~3Rp-xpiB6_<EFN}U5JK=`$!_~@*=|%O5{7d6NZz}`OQDgMq2?*w<V;^ zBZ({fmPlRhUz-Lj2gmn(uChLmv$O@rpwX!IU0y0<v(v`T=DS7*3%+SIT3ii~1+I5# z!=ht*rKblii;UGS5~Ms!%gf$Vrm2z-L!$G{+A<#v%HSY%{kzY?fk2O@Lx&Af6C2LV z?_6K}|27kt$NEQ018GY3f_DSxA_&7-TwU$kY!Ap+F8Njd3uE@=a0Ol<{UtedQR~b^ z;PBU_D5oZ<gYhEK{}yDD7$14#??4<cmK4)n>TZ#oe)*A)=-PV5A)Jh&093OArc7&> zZ=96pifT2%so@2h&Q3M58uqRi4iQoEpzWY~HPA1#DPsBC&+&t~Id#>hN~OL(a${fK zc8jCo;{^f6^I?8YPF?}X!ZRLIkW3&lgWMy~%156yBY_$SxXqvv70&G3YFqJJU_UN* z2~a>R@(%%huU|w2(Fu3cF3Bm59pAI}iYQn5R9|ZUic$cQ1lx|2z$b0;7dxgR*eRTK z6>er{=k<rPd3^%KPR=qKDgv^yHYPmMDl#EsU3{>r(>{*^OW!h5=D38Zm|%(cY^h!E zOxA$e<R^T7!<-DDcY)5jYy`t!7z=%x{zXuDK*AI(6xjzV{l`{=|AW*<Px`ntner4= z>_CmWb*ksvlwPv4(%XB_#*gatk=L~_Z*byRtZ5#6&rR;O5pu~*0E0!WKLuhROojQ$ zsqMyTPH8U}b-EBUM}c`d)q<FfESeC~T6kPdXm)i9+vJFOs_vk;-S634nqB`wF@Kj= zaN(K9E$r_4eA}N>R|F}L!>BrqTsyJp{zD7!x1RT(I%I%2wRY^m86A9O2YOq}jP=JS z&l#8)P>@%<{>-33;XK?op%27Ufim~7gBB7=zdr~^hlip2iz^p0S7T)sI%U|e@uwar zF2vQu<#x5!o317QGjyh@RJ|7`=<hnnDqT&=t6nRF3;yh1y}EF6xEtegS~OPwgN}eY z5B8rPgmg|oCNFDRG&zFei~$17UH=UJYg3gW#Z}*b={V43a*h3uT+53}U4iGGu5NPh zzxvRd5U2IMy$7UT*lP{&Ev(5&6E<D1Xl}Aw{&-pfO@77S2&35q<tx-V^k=d=^6as; z-YstFCZA<m@JOl;f0s;z`2>0jAW}kWG)QQB%d8vM3Smhnbym<UFAf*GTOJ1I6JYt3 z_d*HW2RpeC>fSE;F8_Q=6h~z4QKM&IFr^bY))1?IVGva5Qp9amW^Na>J>4%uF)Bt! zDJ~#R@1SN0r}jzuhbx{!jJBJ*RN6Z-@)xZeXA?9w>l}t$gEqb!^n;EABFm<qOP}$- z7|vg>zbQwsLanOj^5il4_)(ZEhTlrt^BMn7I6@Wb?>^6=VT7I*n+2&QAoIQ;H2>w} z^PcnO85kBR>>rhf_EYS|@b6cFZ=N(4#WCqHyngo<d<dM|fP)L)OejL~E(STZVrP7X zsh-V_S5vOL<qimGypeflv)xjb7<=09IRV28=l>3kx7-|s@1dHl#46z#-wP|z>!Ui# zTlBjA+Y6I2%pZ}lR(y6$f_SNB);1QQepy%<@88FZU8_GO>hVS{nEA0eBTX(3*rA|h za1O0M@^bDv*Mmq}k`c2(0&yo|dy5Du`)7tYkL1|r+WHElA|&2MXSN{&muD|KjcAH> zP2#~iSoOZ#Ucg~5`2LKSL6Z(erfj~w<x(9r7*m3h5P7-tRRDfZifi2zW>sRCE~)S* zZzE{yvX=qswVpOv^h0cu);G~Fo_;e?*mb=>usCr)5EC-7c-f4!VBR{?`ZH{W$p6lX z_<9)?v(qJ1r9h{B((~EbL4SM<tvAx1&~g|$c`wD*?oLPbO`24W+=RNJD;>S{mHjYV zDg80{y-z~}C6hf?xZi{U<zkqz?$P#UOD7}yWX;}Nd3l)f^Gc4xNj%%lU8}KkjW2iG zd+i5opOuYSsR|_k{}T{e#N1>$$@k^ABYq6ifSUbSDGU1N=MHmN?_rg3#`7ehA{EBt zMYu%VG!P{Q3Q^G^%hT}f8j0VeXhrN;L`GI(xI|PDE)cmu$*v-RT1R0S>rX<L;Vj>m z&oD2!K?66t5C?mC-Li8RH$%KGTvqh)AC`JHr$0`6V#kh|O@~lWL0lpFz1N)lq4<_# z=W00vwbPB4EArmn7uwzVQW7K?#{zwyiBi8&Q#eyGhBvT>9?r&pm^3!)?xo}{kkPPl zp5usjT+is4oK`M#=+bVywPh@-XN*WLo|&vRfL&XQP*PRZVNkF!(nJjf)s)}mc5lA> z&fG$?78}C)o$nh=cGIH%!u;&Z3OD>(xkiO=GC2jxGy<c<$LMTi&_G&E?ei(#Qnnm< zt?dcX+iX&BP<>sB6EV?Es>}!JoL~I>FjhvAI%!p$l#JnEc$V)hexOWFvU8CHI0~<1 zpp-o0XYNb+c<v~05J3BsfZu?$bN}8ege{-(d6*0t9wl$?H{WC9`&`P|*x7EJ!jY4n z0if_<a$03&Zde_Wjz>$l-xbYo*+G;><(Tz{_b2~OTD?oMWw&e31tzn_mJhPhd5oy& zgep0yyqs)8>~c}JpAR-ui77ZQe77q`r(+0A_RKzz|6z8U3HoelIg%nr-Z#iv$M#3L zvY`qmtrN*$@1p5vT!bXGJ3;3>mT_ZJYRcaC1-oB+aY)J3R6IFyU`Qpeu?Q<v3$#<& zNtBnjz$GAFX|L6_zeXCVUf+j_;c~qWZIOEapr}e#Lr2I@e6%(0g_uy6Jb`7SRdgFj z#BY*mGbAG?^=(+m%&qWk)r$n({+<dSpXa{0K`)gfBjo-#HKis#G=d}UD)4u3sWhHf z&$CgR)D49ETrO1^+<G{^$lz_eyWF5aIvI&`&q`f>YGvZUnBCz1yPtf~Pd_gDa#Q&J zV2{*stssYmZ5rTDs7<t?@4@A!bfY#`cfxd@DGjSoXpAC-K@{@|wNhXX>rtA}ySEVM zbS=PtU$V|0k6?$I%6PIngjg`(E52hdW5!^8E%yfQNaXZY&8zGyfrrbIQ7CX~nB~VM zxq=Imipo_zO|FR3@$WZe!%-lf^UO}qUHYBUG}$-U-A5=2IWS<7hv2ekb4^Kd#H$%j z<)75VvqpprD}9fscpi!Q<`21m*Yr)+ao}f$$Tw<pxKCIjEd%RkiB5LDe1FrFjPG(c z^@h#%ym@#^1Cd7id;`4%RxevN=zUwfw-LV=v4!DPsw6AOg?z5ASXfv^q`ZxMetmu} zn;B}jHi6^FNCo4ZfVV<A7oyk|8&`A?`?{wp$lc)L{26Y7-RbE`&e?sp^qq2r3tEY$ z*<uJBA6(EOY_f%=q|W>`m`?Qh_Ngm26H(u7;!Z(XVI}wV>Q@DqnnW|Jo1tyfBcyE& z!>QlDU%EVq<2tfF&;CR1QtR+``zKC*(Vuy4M+aL{nsm~G0afp`GU&v4^J?eXKjdf( z$;{4`q?QZBX{(E<6{oM@*194@S<5Z3!tvc@o{l{IAm*-Nh|bFpShdl&-kgXh=Cu4l zaKZI}@=lo~1ov6<aguuk9>sM2X?X>+KD&)UC(YT^4K@3)dCr@%hUbnQeeP%U>-87t z3JQ_pBN~!L!`3tl-vCQ3Im_-_T8hf0R*uVjagO>c@_Z!_VnmEAwFcRv)K=UgG~UR| z<e+Xk!?c%SAv{PT`mVh|G(%=-qR3!+nePUyy68K|s_WhH!X}Sx3A5Yyj*^Cs)~GrF zpSPSzPCkT2?{1t>#Cf;uRgvjnCV@qscRA7bIzHZ(<0#Zwzbk<4`7@tl=NoEBm3c{b zKhmR|hOSS<pQL!RiJ(-w|Fe112|j)T(@NwWF68%d%zYvZl-TRGJHS*8x4j%vN{Sax z4#st~2^EV~P{Bwsezezg_2BjgG?`@N<=;O+0YSAe+$$VSz4l7RZs}=&AJXSfkl1>* zv%4bdH^8-d*0H-##`ly65?i4j7P&M1=c*LH5rXh&*7jqG_{pW+C=8r0h~QSx*DzyZ z>@Jo?x1UHMf97u=nvu>K!=0{*YpquU(bH`9Z^#|i*}3@}g$ty2EOwa!EAv!C6OHT; z^$ZOxx$g4+A_VRN3895#n%unax0QC`&-neB%t3TxVcCL;zPTl3cKPXQ>?%H|K^Pmy zo6=4#mFXX$$Lr5zU}@Vgl~sT<0p5;LT5#tVsBhO7of0Oa%iDR9vJ=$p74Qg!*xR;R z3si7|FNt=5A^*~JNr4_>Wec-EN9Z99KinGU`$oi1A+4s=x{VRXYIln}*7?N=)^@E? zNbm_`9v@N&;`(CayWHH|uU7kL^-LyDLd9@;6ZeFMu%e1-)}8-+!2sCcUADW$X@`Ek z3Z~HUQJK+4286)fuH?<dyzz~(VXw2iDzv5@GX0FT>S{Jxd!2V&jq{48?goNX3sfpX z_JUjqt_{UiRUMuCI>(mAcQ_NfS{s#Ka`M<i(=*4-x(-7rWw2l?BXgA#1PIAwe=JHb zE5cgY59Q5}QLk*cp1$n$<xJ0v-v~2a>C;!F4B|_Xvz&01woZnle@K=>|NK6{4~>k1 zqODW#r#d7ougQ^PK>P9^pX44))><V%U=u1h&0&!HIuS8pHs<bnK`Ku^o>Sd<Qsy|o zrZ1WkE+Y5*t;Z44s|}^r51AZ{h(#gz>(`j>sG_6<k{SuzuUtWexv}Z#guMjt8Z|nu zQ0Djp948}srhEBQbt^QQZnd$B^Ra<~vKLKP+h5RvxHSZXzY_TN!LRAbkZZQ*zat{Q zEG^xTp`WTdc`?`@^DSZA7kIfpQRPej8vTbBnP7b8IstScqb_Qno<>b8iRHWb`Smsi z&N?|jWw-p?c4s~aEkKCX-iW&en&=%Fr3ZoyYHGkM95+}f_A2wt?bhdDhZZzn*-Wh( zY|w)@qM(48Jq8Kgx$-1L<gY7Ke0Wo;zqn!>v+8AT&^p~p{dvO}GQb9d`0|~$yNuWN z%YTW?w@y`2k+CAm#@Kx3NCG)>Lk)_gwP(9Z7okL--^*j=vba;>qz(I*1r?6NPospn z0!)xC_nC;ui=~{jWWZ_LBu&9k$WYS)6-?;b`EQ|}!3z6<+m{V5FzSXx1s)PP(N6aN z#7J;80<^_*Tez^v{WbcTfWNAmwkLi<8fJxxyal#%f|=J2MK^UP$cF}+xR|*O8D1YA znolHOFFWAr->gG#28l|MM}VpMQYtbU7FVgMsJQXAWD!_sXoiY@Y`v8i_PT#ATk`Mj z6|KvTx7H5YCU1!PFJvc;_xkiJ&MVO|wu@@M|F2Mgc0dUmlg<9&75rKqHI}v3g1H!p ztk+LFi2JCjZyBz#A-Q@s!&DRnOjn_~MOswa6F+TOY2)q*6#O=Kk2H9jgVUWqvDUd1 z4tk_r&(;hu4%<q_#oUj0I5^aweAPXF3cj#mW@2j>WVU%{@r~68q*y_0y?(woC~x+K z0Q%^~Dk3)Dqv{k7e)g8T3b`m`uP@XNaoWUOH_5}i-L1m2541#Z|LnuNK#1@X`;#N9 zsEL~p|Dn5oN$m?fICx4}DmDgem1UCGEP5<ozWaLLIrsGThk3lP3Nks^^9_naGf^;2 zDNTj<Y7ze3Qx4OlnOE_iw60jeRe~xc|NERE=r$f3BLT3Rx(+FgY7Ha8=p!Q-=kyfd z0d&V20Y#V4|BslT8x+w0zY#<AI#Vv=m)mbnhi8T+n?qC3LWa()-TZSR6}&v3n!pC> zK`){FrWS*|Ofs}>`^*;+%ru^vJn9wPk3UR?Y>!)SgWs|%N)@JPMT7Ux!bNz%ibTyZ zODFwHgx?t6aws$CFqn67eBw(^T)00FT)nf+9PRXPLblow=jV)XLP@xTfz%Ual;#8~ z36%Fdb2}eZl^s0~ngWQav!EhWd?YUr`PHqx>=qUi+w0FjYfr@G^J_X&{z<zETBA=E z=cbZ0gZ}UNU2pk#uil$e`kn7ZK4eiY-3-}g>?HI03j!#KIDh3{c64~UeCM+;i)xn0 zVvP*d>wYh9pKMe%GCKYld7IYI06{jX;UTw2ln?Mx^bcc$(I@k=O3KPiPO6*@g1v|) z?;!^QEYGvk#4MI!aO;?ukn$J{J_gfwZ&?r$(&(|bjN`@FNI<Cc=LV9CO0#Lvg&&FH zKZX6rJ32cf_ZzOI)Xl(*0$0=@1|%TdSBpS=sh>a@lRaybxr$^JeiZ;&$l{sVpCtQk zGwd4APlJ%gW9KVa)wA=LHR*LNtw)a9x%VURNZ+(`lLaq?;x235nzs_I`rfTW@I!UL z$UyqxY`IWzWy#v8KDU<kaH+dzxV8juH%-<!uP<u_)y6>l`jv6`@HXz{LMee1nO?ED zO<4C!D>O&DZC<Ped~m`EYSRfb@Nghb<JGg<b#@fsU8nL&B(Z&X5(bQ~QrB`-7^<@K z?qdri884Jw;8tv#uM_<n=!Gtagc;tvgbWhUu&)F;EunWc;yq6a6t4dQamx?L|F=n) zNJ>pY0W{QfU&;gELNyhLPd{Q3o%;`Z2Pu~CCK^{GupgGzz|U3A!}^5y5vr6`?-m-e zx1cW1@zKHus*gWBgn=)cK|ztH|6{B=JJ^l;g4aLJ_-6g?^3v&O+V5fU4C%pBbHN-H z-Ac5yyfoD?335TW8~s2fND$+D`i<Ah*6KYy=lAfY-dHqV|LNe)ZIZk?nx<%b`~g(G z;V<_|PjRQIFT@$Jlnr_P*SnV^KEohl4}^jfX(-L{8mrDClYwDRf$WdH3NJnPx5xN= zBrJX%$VxatbT9d$jN8*Sm|8MKLsm-QmFo3GsmY1`s4hfCK_w(84-IH$msfUP{XtND zgNxVK;wmC1FMqT=OG-xdQTr^gKc3Ak<clt219Yu5K(l>jEaCB;KP45V)ucWrHJgil z{rZkv{;mm)>PCk$z%N{cEAjK}wEFE`QdH57Wd@T$=0>T@k%~3l%gz5t+?i%+-HLIM zQ<a~cA#L<NbXXcKXyoSQNt3TRSVI<s*f->L59HZwZZPo^nV>~QDFE`U^0&!}fe`N- z>tZbuCVU#^C|@E_!+YA7C3Ut|S9<bJvQnt2)<!QB7pDgT65Y6}9a&ELM8iue$_xN^ z=yJ97MZ?jA{Ec{j)5<E^Ml`K6YA*No^=;b0)=*EvRMEq$3YCtjQS6FoD@#oxNv;bh z_{9~6NGleufX0-DZL^CzSwV23g$Ok_0Wb0G&PQ2I9C>n#Ag+Lnhs94Y0?)0kS6R7k z7TIA0VS1hr#(s};cAn*I=oXV)ky}1EUQwg}aVqX_Ffv~Tt?oQ4=%|c+V{aY>{P9ca z4-s<e*~_;zrU1SVP_fyDsL+jAcZ&_`o?fP8GkyUX3<55uaO?d$sHc2RDHT;9M@7b5 z3KdKvA*EkXv*4Gb`BDMz!`t%jK)_gtHt|hbMODZDW9+TNs@%5rVNgQ4L%Kmp=|&m> zN$G9`>68{pDG@2@k_PEsbVy2fcX!usqWhfpefQbtyS_iX^jfa9=6arI&N;>%bBzAE zm^sh7NN3LD03j_;D_NPo%=Gk6&4a>p(TD)fq+VMkt?Hxq71<n+iY6_Y<S09QrpFRt z&uEUUa(KRpuf<hlwD8LuEXAts2<SQMPy(U2iurqKYgNGp&~?;Zj|%p(9ry|7F2~o` zN+mu*qwPCG0p6witPdVc!PGa;LFx9i^2{0SF<moDFKzWy$@LuZLe<e})TI9Yn~4@B zQAOc)6z|HbDRss7&`JHM{vrlU+Y6b|ABEZZ?o(O3?#N>vU!U3Tuh9Mhvd(^JVQcyR zqz(*=P&Jbp)zuYI4~yUl$~h(bOvKO^`xs_jNiY-rkT=3A!l;>hJqyNOalN!wg%)C% zZacvvZEGFI@!>LM&f&;N@RgApA+Es2*?2(^mh&br;V8anL?Wp_2BZ_RG*OpjIBVN> zPO%9!;>Wdp%;+-omi<&IWJ1h%v}Sk+-LHScB?->TN|-<t(6Y3|s;q#AyseO8+k?qW zF3X5r)yTx^LzX{Wg|x7{4OgNzkp^L17%&y(<-ayIU9mrNg!_;9dTN~k=n5ulIl$HX zgSR5XQ9h#`{&wP^lN**yJX2C$uCJxd4<huJh?G?T)$Lh=OtS~gtNVn|ef(}qeP;V; z)WA|Q#TcZVD6wJ#n&s+Va1_%u$Fb73s%K|q^=YEaYlDs53|GUBW3_^7%^tmwm=RoZ z>&pKCx)vu;*od$Za*YWb^qj|QJxDBXp4vINn6~L)IPQ-IH+@d7=d-~G??(lduom|; z?wNBC{e7RwhZ&8QzOEO%#*(Fh^?X#T<2)~BbhNjjYXg=Ib%bnPh)pc0?cF&KwuRR( zZPC~*qQRCh2@4ZwuRnkaPI~nHTFjs(<BDvdqBlhtERg+fk?eM%dt_f0e|#o+vazgY zGdbI6C0;oA!*Uw8prD}0O~k8k_1<EsOBjmb{&L*ZCIr2YTEMChCXGbT16zjqzQ}Ks zjqQ!h(k3-+T%m@pGv_ev{Ktwg6r{#mglJ7K=0*+UxG40#q(S<x$e{$NBLzX8)CP|O zLC?AHutKYkTadA%@pS46@h5f;nTgUFnbn~EuGr9^F<|V5%3SaeLd~7~t*u?1HpIZ{ z?(`O-fUXIqvcgJ-#V6qSGTPH<x_-te3-&X5OUnl&T??jLaC~EV|2Po*g&o7kg&r%* zNs#E>x!{0g<t$v2#BHgcFi$?HV}z?(=&jj+EsGad!_Cznq`PZ-e$r88W+O*M0;CZu z<WB_T!Wbgjcu5&C0H91!IR#2wjMq&hq4)I(afgx`#oE~pFp(s(zJ6pM)x|<ft{K!k zNC`bbP$lO}4nVSYMsn3@^HfJnd6`+)&+e~(V%m`R0Sf~I<K^uEc`4g~%uDdbAj?#x z{7o!;n5O27SxoeY{aWkYyLSjA3T*B}m9MwA89*iI+*(^`SsF0C-{@5P44Sip@+X1& z=*MUEaIuXTjJb>;R>QoUUtobk-Hkf^O^Txs5P|TyIbMAlCp#M306EGRmn9$+pP3rH zDP5$c2@5m4T=HKVKel(+m^-fg*VmMb|B2T0AJCVEj45)2pgK`^i!Y&N*m-K8wSsUn z<>y~&&rC%|CB0gT^y@Lr01cfwYrvmv4;v>Zj>)H+$iy$c3$NO|k+BFtPhSKyIf8OB zL7<u>8bW}%6H#K~#tJx4wt_{(XaCv9D-H82&F^I;jEzW*;oZC9SiwQdwB0LBz3*9x z9VRh+qsPFQ!zeQI2_Sd`U-a>3EY&n0q&{EMSpUX5NNntG*!d8Rmc3oAc>Z0|iHQj* zKxBR3EShZYw}Q{jV@<87TK&+umnv$8_75mr9~2sxle;O(_!=Dp9Aj-0!I2=vc+Aln z11AaZV5^6zqM}mIeU9)6Nk+ZFkS#krC1Xnc^}bKh%y}1`17SZ^cFD@H3O3%?A=)2= zSR^FD(kl1+ekFP~VtU#wEphvb`6{9KeCPlJ=RRA1Z#>2AP@~ebq(Fx=<D`fEUvN9L z_Te$&Akib=tK-|FV$gbqHtPN^^fwnWh0EoF<~B1Hm}w+AaXrn9{8UgAP!vcG|G;V+ z0uBA<6+@B-|9hu9_7h-;yt+fwp5_@G9)RaUGfRH|mbKD(?7=w3ETq#%Z}VC_BlprJ z(PN2-%~W3GR8l9!`$T}4w}$Qdv@uOJpWB@`LzNFSrd;{CPVy4k<EEb#wUc&`t6Z&N zm*oDJZ8&ve9!Kvhy*DlY1ioXb7}w+PS{_3&Tye)RZl9wIxHYK=6{Gj1Sq|u$SDXRD z=X1Ols*Ai0f;83_kkOXE{1`ZXb5!lL>;_0B#?R8ZnOh<_>jNfWE!a5^ra6p!iHs37 zbtB2>L=!JmT;3=E4fbAQp3y?1;}0PfHlP6L@#Bd%I{^nz{N<V(Wx6kT6m-~#{V6>( zTzn}dSd=BDr7sIr2eT5?3Z0P&CrJw%7(sQ2lfCJ%^0INkD#*Vf_mu06Ibvnyn5&Cp z<3v#x1VHmQsYg^8x3p4g5K(2JEN&O8UGWmKD(?}u6dF9*j16h|5nGQ<zLzb1dx ziP_aF)jHcM^YEyK=BC3Gm*LCKuJwNQb!W#mq8aUnWRf-F11Xd5{t85MbC!k0SyZ13 z9mz?^B@~c!zDiSz`%{07`I^u(wf(Ly0txA4gQy~BG|z21ZFn-CR7>50?s-mi(Et{I zDi@zOj}%Y@u3bno^zP>>bgPDSXTm}93b_40cbqy^&)fN?G+y-39iPM6_pf;cc2E&G zWYg=I({~ZmO@4u-AoLHm`28I{i@=?9{K4S?B564)f?WANq%l^8S0QLii<^i|gk^X@ zw(B*)*7k__m=TlyNI)WE(8F8ANMYm!O`7~mF2lyDm!@t^jij&E!rcSL+ywpyiEBC# zZeQ1nu+!czJn&MDq&!mM?|D?m#=sUq@xY#@!J!pvI2g*z@aE;4nBtLZe)&&E9Co*1 z*4Ez6*lLIKIXyU4!5JAJ*#lD0uWE17OGd!)r6)}#e#)T1>u6or86cLcc}h&%alu=G z4=6KHwr;WVH!qJj<|YRw72e3lVj(Tjz<v^LUQh+pbGyUy>qZY9V=kHXuJw(mTimqz z{5noL&*b#>!QpOjTHdFOKs2eB-#!DZoemomp{KTfJkXZ_+w|dapTmAz%5-tDXCEV- z`Bx(Tbd5Kvx(nAfOI<zBUS_E-4mUhj%z#f-pmTM+ngtn3JeQuXPg-|SejOX--lqpG z2qxfUW%hlDt)p|(@HC2xe5_klngZQ}6sF2J-L_pO)NHKui#B!I1Ss11{{h^KH|2r& zV&*PVIGKJ2q95%G8&7eKx71!AAjpImrq)KDDQZD$xadUPxtT^(JD?`Ic{NOCFr93y zt{#p^d5*CNfMnnF+@_{D4|$M|KE>Z}nf1Qke^yYy&<9$!J#8H^+ix^na-csr`4WFP z^=&2URXzW*4R>Aj;h`EmHQ8%1AdZ2sXq5=coJ@ajI6>LeppQgfQXR{(bOB1~%BpFU zGmZG8A+d*yLFCEcr@zJWDX{@0|9^P_Gzb0$nV~+Zil!-`|6=yDO7MCxXt+9UQ3${N z#N7h|^S2aahgv}Z<!6wsOyZO+X<HMgzRym3FACY*gy?!cx@fEt`s%|D4QO!?j-QZg zQa5^$1GVl;IB{Bex!7+B7`*I<j$&kFBy+CYkXyuQzoK9Nd+GfC1OXFSwdwO^zXo?I zd>{#t6IkrJSYGm|?zKOB-%psP4;MOJg)I;N^{r`ppg~U4Vv4a5g?uW#X24rSGp~S~ zQOuLq@u(S~adFtoL>?!RzeXM8#19*-a=aBO$G*E2f#ODI7EK~ZC}1NOqmN9AZ2!az zq2!|sWP&997=8<I*^5S!DmThQzB6TA)$p=>HlsaKQM`Gkn;$3eC*p1~9@Q|>n&KxD z=2s{iYBukEsyXa-CMyGhx;Qa8{o?R2*wy9iduFxY=={yg?n7*LTwKsD3k!o-ai7z# zI(pR<53|nk@w%K@s?(3vvlkdJFy~Mfs$r<4QxR>H&)&qS4NZ=R*quwJDdj@_6H4ye zC;mTh_@6-Y>z#4J!N2k6`}vi-#ivMvQ$PhNxm+iGHBpstcrL{7z*s!7dj=5emk0<I z9e>;=Gs@b^KCQ;lASRrIe?m`nZQbDsy}VI=g~_~)yGE`4v$C~;wIcZaB~X+_*2|4m zm$;!wQ;=RD<d*O3ID+TCP0mtA*cw_o`Y3{vAx*hTuGh=mxeuY=9m($Py5_@OP*G7! zxS%kl_7a|b@6iFcb>klgtvJ<{4sB(kd{6+ym|Y>ELH*cRR<A6W7d_d422|01W5c1z zF57radz1a7RoHx*`dZ`7biaibCULmC5Uusds~vZ1DL?%aS`7g|cd)fFxKMdp;YF^= z*Vv>#LV!XWaUTxJc~U3`s4A)>Mxa%EcBTmfEp-Dzi9$uvn$9`(YR@(_uhrszLGA49 zD$rs3iTR}uns@be=T`M!b?1*r=P_xMu`xc*Xb#E<q2!e$j^B(oP8u*B5y()mWasAP zW7SvZmRv&XLB%YTyP9P96e1H0p(Kiy7LT>og@HL;X-YO)WBnUtjo}koPQ*HVj4$9t z2c$?zGvddAuwQ-%Z7#WI{1t}(0e9bDO`pYH`J3-D6c%g@cA_aeaRj36MEkH)JVaA^ z1#R`vk7Y)MSdVGdvNcW4_GbqA`}hX&wiN+ajfbn8$6WYuNiMsS(HP5;N5|bMKYw;- zWqZqa$P$TyM+(a1y6Ab1YX>&#r#85_PYS1x0!=%^p*;Vz2a*vIUT~s)JnyW@cWeX_ z0dapfD8!=>P#vfKxOkwim=FbyzkWLYzH)qWY)TiTi|^(=pgipmq_e(&0<F?A-NkiQ z-i3NWm8mw3cSHg|QbZ5y5J(9ae@{>Y!56kavNP57wrjo^Ht?i%_4KcHu3E-jwXn}L z%s{(Zj%lrxBAV=md{v_-<!6ao_|Obu{n2dt{)Ne|DOjn2(G@ss>wN{s<Nb}+qCr~m z{lYSe!Uz&Z6_=R}4`TRoI}FAD3Zzg(L>21O>PMh0>Q)15>cY}-jRx$^k9SzZdLHO# z@@i0XaScuU?O7u4cdhEmy~w9zSurp>)KWa#J~Y(j78OZ6yN@OC7gGdGe0~%jE-o&S zfbB>kG8GAg+c+KA!+~RmX*UsCO7<ejcjnRYwx+w*QRxxE8^LW4n%tkcpn{c=iHW~| z7OZqMB3khocOCB`cpTvDH$d_an^w*6`qoSUdKK#c>Ml@&C(5m9M<pn;<+CEuQ^e*! zPxqQsTF*!ZLqdnXATEvVbqHmy!EJzO&0WJ?%IQo94o$vMOX%wDTHV>K&30id&Ji=p zZ658z@B!8k;OMBG@P5$N@pP4^{metA+?`UB0%^0T-M+}*FhReiLaAlNAf4yMgCHUj zP)qul;{Yfg^<Aj<T(8LZx?|Uzc;^u5sE%s#fQpRW2W`#E=VKXGGBLmHc|CWg;YN-@ zfktJ33h1ergOKqg_(@@vhe<oE*Toh5k^PZE{;1>TV(Tx(R*>QI^Lw%id0k`KG@ft* z_D0QxE4@=<ybgujS_R~$&xSk8j<&<<3GhBP^oJitS##+HLv0YwM8#D>(hJHl8(-=L zggs$9AD7IY@B4<6@cwue7C$evx|WY^sVIoIYISxn>2Ey2rlFUB{Tpvp#(A+FHNtA6 z7KIjyo8l=>@~J$Z1qIH6%Vfaz{3?{RL69;^{1`{@13MMSd+dc{Q=OS;A!B3$R=XUH zX8pbR-v!;h3!}US>-ifbWYA#{4(r|@kK%0%^~G`tYSTf#g#n@TAsmX7BQ)GRD)~`P zwYq|EbZk%kV25~k{mCE;0TPlMJj^3o*T#YXMj-Nl*B<q~KY#mliAoF!=U4Gsa)an$ z>6{<<1;0O{{juqnEfnV2ezQ4?)PwN+`{i^4fHfB<)4)kF6C!#3=fBs}!w5kENKf|A znwlHz=i9<+bLsW*R&WkZY4xHKl6rQRt=CU}vN@h?HZ|pZfT}-T(&9f^5p{Js`656$ zg^lJC1tSc`tOke0TGR>a62V$^Jt%3Ed1)POY}mO*?6@}v>I8TaK$)qT0ZYv!68I_B zJPs-Met?2$&Cdvg0c%h;V$Mr_dcuBB7Li9I9&G=`RXepR^CgVd0_#f3_-@y9$#I7_ zINN~XsH#oi?agH=MhC^Nj^}5u9kV^*<$2QmA1<-&bq2ux$DP<E2-n5@`B72%^jI|V zY4-~TD0zTEn%}aXiVnunD<IpnrTF>k=R1u$SRYJJ1j;=U1Clkz+%HR*s@}(spLHt# zQddVO!I2Yl^dq_j-}_DH?_m@GQt5vgw5<Pa&}#oL1~I6OkRJ64hVYeTvq5!peC){x z;lXCicjVusUMk?byI8OPg3OAA)t1fK13G7fiulkyhm)XKbH5@c);RscUXndCj>+oD z!S<`JRCK5qudCNdCa-C*WCS%uvaOTiZqIhUG4Gt?E)DfZCwmF3Y~X?EQPA<n++~5{ zFoYNV!>$jtyNAA{va-#*VC@%#ip1O0_tMks%K+Q0KRlHBi5-L1*gm=2$pi5pA{3kz z6+v6vpArB81$O84fMbl-?dnph$>03Z2XYQB15zG*c@-5Lf-nhRdt%L%cXIzN&XIum zI7so<$=UJjl!jao0hC@*{!vlUQMcS)GV%X2&b@j>FNS4j>zrn^Zv948^|K?l@FV-A z5ylcl*cw=Hz;e#>ROi?M9KJZ4+ck**S+RMEFZ_w=Ix{@*(3sut8J%p;*NtsEPlZH< zoBy_6jo-}o{9CYXwEivF(hB3GBf$Cvd~v&?f<N0&mv1CPkg|M51H@W&4XqarbF$co z{yWrQwJ?UKhg)6)#t`Fr6z(rSlgkWS`Peq_OSw9|GxI8N_%(Jj?=WI$W%Lutfi&vg zU1Ubg^V+|}?Q)!GsxIWGx4Y(}ha1O2`(HM6<KNeTQLd?FUy5WYkUf@jh_ux_o>G_w zh!>)yUCRcD)3TuSiOJWPH|SN)TzBLg=g>e(ZvELFSy3{bQ;?HhJ0%8e4`%XaQmOvt zB9I2RjM1s6$YhDN%M;!nVB!R+3RF}uun4H&G3nQ@bZiX>dIZK*t#ed+SPoMZ%hG*C zBuN&GCvCh9uF`WO%xLz>uOl2Z%2>^Lm|hfO;adZFs|i837LQ$I@udMqh7$e_i5{H` zux^!}pP=l$>ebRyRMiaDOJ#1A@Eg6ESN=yh_C*8P-&|Tc3ln5v&hMsyzQgyF$GOGC zMAfSzNM+vZ?cH@OiqMO-Wca?2+11rGbbO+*g`>hSV2sys+MK<(a4I}%g@+CMqD!Ue z2i!=Qe%lM19Gbpln%egape4#(e#(pWhU_1urR+jNe}qd?7kNB{JP*AfT;uESrye(Z zguq(5m^FUq`Fg*wsA#RGQQq*5He`C|oq`3ApPzZnT5qU<ts@J<bp${H!s|u4e^jNu zC++{_I9WTFh(K5eQXDp3%r9>RsE_Hs8np+KL;a>h6_S!P0(P{xF&uhctep+$T>TGp z{IKp-z)3;06aY(1_n+?ov4V+4nqFG^NJb_fu2C)b&C#5h%(UapSNkUjgQZ!?{rVjR zfxsoRY;3Y($9=c-Ch{*Ai0<`53NrpwEa0GCt3)^7Ttz;njeB8<2{2{L+&t>>=)Kp% z9(;QI@qYF*dPcK>v(`1N)b^CNrH#zh(aYhuyPMmc8Rww3)}N)1{mfI5qE+foPOcOA zW4d=;x(}$xt#QuIcs*)QQ}ebSNuuq={$^pfF)^O{DP@V|Dh(E@(Sob|ruUzw4Ugk( zFbNDS&B8z@P92Yi&8Y?~65S$o&9?_gJ91Yw-)fD+-N~&J{j&nNgxq4**98MUMxB+8 zum3SSr~4nFp~5cwx@*7;wfrq-EWWcU?2F(a;4q<HU29zayV=6+=k^(Io#y`N);s1) zEm6>8jLcyM32YA2k3Pbq*Ezm`KigX<*}VTlByYzis5GJVaK5;0hd(@Ugk1=mL^5wm zM2wB(*#nWK+(gI2bC8Gkb8OP<sYy1#pi>FIB?ADrjWMr29WyKoPK`?v3UGE5%vBq; zTVMYA5TgGVA1{vKI+Cd>tw;7K1db*xt+P~-nCbq9dmD24Qu1?2RZk?%;Q1S7i`@Au zd+nIThTW~k9s%1gF7W+eB9m*T`lPZ{T4@e^@T{vbx)DQ=5*;8XDD4-sOnR)hmWa#f z53sAJdimYmQ($`%aB~5|Bc*Wkeh6oFMUlqX6o>I^&KjV|;#w?!BWC>0*t(A#OH9!g zcBi~IujNbTXYTYTHz;bYU_ySs$&#JVDD}(_{UPzN4$?+JPhY)uFzD+3Bt4f<7dP_N zaqqo3j2si{&HhK5$+BA(48_|mkNp%RrAKYOdfkf`R969KUs4gtt)CrX{no^j2x?i3 zb&AT#vd|wGkB2139z_Sgl*wD?^O3X(xV6Bv{*ca)gVY6778`psq_nL&vemg?d7!+* z8T<pCYY@a#;0)4LOR++{d@o*qKNVZgx-4{4x$3h+_VC#+!66c%#6cgU<kIQHu;L#~ zY^N)YC06tGq@;Sxy4MG!K%J!HdjrP!Y`m17de>lbNK8IOmr-(+XM%Vf@^*cZp&7Gq zDA6UQsAB6TvoWGDnjH-vvuP#QCH~$ESXkFaLHEG9jlsv&>uJ-}IU?F0$5Y&s%16p^ z^~@aJEt}S+<r*+2vGK7{#@gT&0tO+aAEG-kJ#A=28ytW>PJgx?23$bu_{Rbk6`wGS z;4w!Uxwh48XuZZ2yQO=;i4en`HYpE}hQ58i__*$LdBbPmXpK+~-(SWv)ka8lDB!uJ z1if&>`~!p|Z}G8-JyxT#h1NQRs+oiH@}BD^UW+xHGW=cPK3|&Zf8|{8kS-$s7O)BL zo!>ewv-h_xMW|t5&IOW}iSWdK?Z$gS9~^`jBS72cMDRZdxwp~u(SOsZU%Lom-}AAV z3L(>}Uo0a21<otBIOAm*?x<qq1e5D|y$P<ClmtD!VT*PXH#XND+MTIWxI059qZD$# zsuir?J<G_YE6$8fK}*B9D-{KEK^t(%u_}6n6apM5g>o_{6N?wHm(VQR!5Z~9`>}&t z<xmA0ca$+PxHE!#&R*(QkFO{MWCXQ+r3)$C-m%}pofyrP9d8IZLeYlPfaY<p%;x1n z8@w5wS8Ua87s%Tgybxl#kNk@D3~)d*xW7UWBIKaRtWPDDTz0bS3y45|+A$*(zy#6R znw5BEhmhZR`QhvGGjPJ<NdzJ-1@9xD-V+9h)(J6BJdfkyu3wT|NMRBRWoJaK1@gK8 zUqsM`Fx8GslY-NS)DO*xuLXNDX=&qyI=5_8GZdcJI4s4j$<Pcbv4MTpsB1S3sM@vX zTVhqo#94t)24NnFGBH`;;4Th)uc+(#*_#ctRVdNBxN{ORpl*(}Th}qfcS-e-vk63O zGV}W><u(m!bT%1$pxTkIwP3}-zzMyIU|v05FD<Sk0c*l40nmc4dNFT&uUJ;&pC!KL z=}bp!Y-;a|KO^QZzL={csSh!Nk9cvp`4i;vd8bp8U*CL(aHIH8zmS=z^`eh-5!0;@ zP|G_mtyCK?pREhwVy-`Y^gQ~LLpC$2j=hu1@}^pHhpJ@{Wd5wwv02HqFGXja6Qqv+ zJ0(>_*k^3ZN}?CIa$L`kXrywqrNd~Y<n~-**SlGFXvt_HBx)U|Rf-e5e)Yxz=gLsP zE`J$xGijRY__VcUfx0iVFla_nw|6^1_ft$!5dl?>_p7x3C6AdJ{3f*sJD>pnlhD_5 zu^stF%EvpY{3fTdAl&!H5Yz+Pzu9&;cKD<CtIa}gsgF12r3Ap1GgWyq!Y=*kR4hDL zB=^nGWEGZIvlr2f!$#iEwrD>zBrY`2BJpXzJgBpoYP_UroS8%m=I7h8?`d~N#xKF- zS{r{X^0+rbltN_kXqb7$bai2I)lNg_Pcpprb5cHCfesMN#!WwL!`n!KTUJI~92R2` z1H|`Q$9>;@E66x@>p_mK1L3aInd8}ROXMvApp@)yy6oQVygS@-ygju3YAljn2Bh4A zF^8)q)<kWF%xqu@#`b<X75ASZ3mlePWA%r1tLLjtkUs-<yq+y5+kd5@fYH^~N>E^z z{&Vd>!8k)btLQl7<K_F}B)AO@CzLf_cn{18hd^?22B&r7^!)k<n?1hVsa?D5nXRuS zZG@cCMN5A|3VAjfJEl=dRs{trLe&*REWP^mS$@sc3AFpSPi*ffY99d*{&Vr0p?Psx zaYaKa^zB526COE68qtL3=cYR@&S}F8DlA>%*+F$s>2U$Z^O5JzzUO3zwycY-SFhwT zmf*~PGY;SO<LUm!Mikys*I2dc);gX_q_LE|pM8)Gv{h~X$F#7)*8;xvXMgvyh&T>0 zuRD9SXP8O^K)98X>#14QKdQ&Vnql9=ZV*|<5dZj5-loqI*ApB4)$r~6!$1ph0WPfd z^|i&x63@QnM33^?kRE((n<Vemh0KhcRP>Bya|r3O{QJq8n`#dfWxcq7I<>wCcbh)9 z{5dONp#D)>X}DG&AA8H<!nw-OF5VuO_j=7QovsmG4Q(8*{fFttitYMB8)j9{TY>mZ z4Ju+e;}btcu5wbs)fnq6rREIz@by>}_e=9TrLm%^0B;2sFwtjHf%tHFSf}S|(&0VP zAOO7|zW0}QaWM}+Z%4z^Fn00<>h9L<0Mw3fJzgC+IB+zW2>Q{p1q3>N-dpQWh^O_J zN009INl6cO(^H-?7uSD3<52xQ3rGgSbpcmhvYO2ZAVGg3Z|kCGAi|SPq?gyIB#GS4 zM-f;j{FqpnmJNEAHZr=Zb0(+O^B45gS4tpoO&ks5J`!P)du0D`!XnVcU3*6R;%&P; zIA?5_!)klg_7tkS=lP(z`od6=sl`c~-~6*K#vB(y65U-pXQ!q0Xd%!vEcmA}1s`RI zL-xSp=uadWdYQ^jt4=^{+4K8jb;_}G2RJEOvY#Lv8BLnaFU(F7!e<lKaR?jKI(mo+ z05f7`|Hlc-@)P4hit;f*8c%hpd+Z}ma;T-{@zF~UYgMLvh_3il{I~d^^bWE3eJ*Zq z&ZK?ZLa|(>#f=>w&BB(!_TcHx(So1$CryfD3|g)J-O>`T@x`UY=#G<<I)5mNPJ*fo zSy?702s^D4@O$iNlC{aa+sfMNy<lIDvge1z`c=1Y1zK1NKo#6tGrkIZK&mZeAK`-$ zf<xn7X?5J43mpil+o^9ZcI6YL`GW`bn?`5DHcAFB=GJ5+G~wv+3aM!6A0&s8lCt*Z z$q$Q0fqEPP52ItT-3Qc}?Yci5NaXMHUT4;C$-sJoh?aNG1_!Hio7e6{iv<kq)!#pw zUnDIawNv;d7D$oe4hi{t=t;2-8gKEvw|<S=$&jY@GJ>9lS4}xiud7kdD^P9T8%Ksd zpHP}+#NHK&{O--0R9e%!AznTAt9JN79t&zIksAZRTcvmU9dCbk1zP=v5S@>(Vl#uz z1?5{aw#}&w%BbXOEF>9}mkmgAYpd7?cb0mrr<1_|{7TvxAl%=1r?9=>;s&m*&o7Y% zJCb<b6T~_fO;}j**qy-qe3gxV{!3?ECna~Pt(dTCnH1BuT`WDVh%eqeSdG9Bol9ap zXEnO&yEI5oQz_x)Sd7M~p3^^nZ?k8pq&!r!o`wOK%Y#NVhzWo%Vq&AR5=!&FQT;{X zGj;>$F<y`WZ^+S;9=t+o*O#QPsDQ;t99I11yzqYPa;jWo*kXQV#;=sk{$i_9&xjrW zhl}mP9qnG@o|RrseN(;J`r^V$*YbVh5Z!JRu$KW!$%){slOhsm`;LdFpzjR|TJbjx z)tg6kQC>{L+Go;8aslEO$L=l91YMV9uz`ShB7mt)Ueubu-!NtFv`OUD`%Nf+Qb-vv zHzIKU?9w{8F}S_guDThO^1oBxCEm&Ho!$o=GbMxTG-A#UD317h$dqps$cj7;6&ZjH z!a&zV7eGT^;AH_j8I5xVEG<2K-i5)XsR;a4i9w$Yu=pApSu%bA#|vD!%KO{?I)XWB z|HpkYojHx3&hbT%Mr_*_d{&Kl-p_^BWiN^Ui>{}K`sJwv!Z-A#7&73;`u+8rzEOY7 zQt^0+{B9dm!JSI3UUGeXpIzn%f7UtR+364L!+=D8Ay~B;;2B|HR4vC1$a{`hHuS8w zKfNHwr@K@sR0R)8;Vr|D@{;!cW`x3g!tlN1oKXTW($IdpUhn(!O-*n4tLq6(?}t>| zha7sXu~QbmBMzap*E<5@{^Qba-^Y5bGe(s_*KOGbQ-3pmNKJfNnr16O=vtl@OIhRO z6~lwM8Q#fk=2kK14F^};=0A{Zr7i>-HM;UwKj(kS@QGFos-MSB_)8<>J%7g7OS|3s z?CWzJrexy7lU@JlF<sw@(98&g&2tecy=cgDO)FN#`J2gaLNUxj|B&nce+W^L0wYg= zRq}0cAdmNR1le)t_>w7es_S=%JnM4HgB5(b0yhe|tG=oZp9N_$=aI-gRj%^P-+m2K zQPh`WE<fW0xgp@12hB%TjMozMNDY%ira$EjJ_g4Z{+kx`>P(I@=NF#CN-RpEP`qoO zo>DApye#;q*pf{`je;)C2P?RtPQMd(^2}msJsif@8Ck_@2}0A&ik4nG8^-dAX(T@X z&X$ftG-{iD0$unf`M7e*fB4M1ylb{iD;?Y#!CSF2-iC8S#rF;CSM>G3dPRygwD12J z^4wFUyxVxm;@IplHtq%hx7LC+gFNPav@gdvW9nJXUp#PUkrFo*86}OGcaCZ4iET+* zhlISCVbRer*EIk8HUtxx#0`(G4ZuxQ8Zc*3;{Yd7*`GKM^=ob#g%(e8tiw8^H2o7` zuHv88BnmJd&*?E|G&cn|@O@j6$s3CuEh@y{u6ZTP&a`PHa@Bx7q?T3uNi{)Mo7em7 zUhpEE?dRs&W!e48d@(#Jq{fd+?+#S=zytEY<Ckd)2OB4KK*)M<5neQUdo)#rczv(0 zS32u=l9u5Wd21&eWs_abl5$KNF5P7F=wX1{!t7i;zjt}<&%JM~k%Zc<El&@lAX7)z zuVi>|+oNZHv19|kn}wH078o=!gpVRmFX<HUkgf-ZFVtoOKp)cvh^a(S!phCh<!YBb zrdXwfppzHmx4Pvje;tMPtcX4z9ji{6Cdl$jb$TUqd5zNd#I7(e46dJsB(e$V;k|Ye zMot-HcmP;BYZ7ke7uFX8)A`I;mvLKR5HM2Im$PvJ2Ej~uV}kaj>LRwtm1hDg;KCL~ zuOnTl@H{7k|K$bn?>>?S$6iM@8IF|HG#yK;fB22kb2@BY@Z^P!o}g}Ydq#vlDuOJm z#ILZ&c+`Gc@>RNmaW-mcrBuwd@S^SQW1@22gzHc0f4QySvui~0<jJ0$#`Hg(j+Wmm z8VTTGGs6ErB6bme@y3hAo7!i|OD&jJXo&g>Z&+mT?xdzBWr6tGl}WqT)>66__dpf= zMU$-UQtb%Iz1M(Es&gB?Di~6;nT5D*dO@a2=m5F#x`2hgVNQh&TJXl0e3FL;<lYPn z47LtV&BZRvEh1O(W|x;`y;<LHcQN<{__yv?Gp3{rOnI4flZ<Jfkl#XH^F(<h1Iz{0 zlpbN!7b1SA;u#{BH~L)ge#4#D(cbKA+gr3yHJ(<|yvsKV{+#+~#Q`l9O3=&fY2b3y z2H{i)n5W>_RU2~-U&KIl;K)~GT(4?yd@J15+1%x)7kkM+9jqpt3HajxSNbTo6G6n? z9I6ooINg<onvhqoU#%wxN-V4_HG#<-BQT9N&Zo_G>IlbXDgg5m9E4aKc<x!@bTXPx z90JG{J2ps8X<3n`>5Y!d!u&et$rWb*_pCDVJv^m>WTPcNyERsq2;wlk{SduhPPg4> zYT}cV5A4q^q%SyV$5JpjJP6sauS#o6mIgr74o-%al-HInkx}sxAa4WCuWU1cAWa>t zW2V`9pAaNDIq$`^Kjjc~*6$yC_j{OD2N>X6!?aPpL>7+hd5qIM4Y636GKn88GSx_k z*3>APwyv<MYYgiv_T+QyE0rx}kcCFGZ+P8KImrJ=7&XwS+Ac1Bp0us$NiTA2%YnxY zM{oVQfX#f$!1Z$d+VN$*E_#T#1S~CKW0!m)C!#&=Qz$ZHF(`WnL>3A+x+8nUhc>CN z*g9XLx8Y+ige+n-bK9*XDzZc-==f~qS;)p=qLirnrKUYbN8hn=o&EBnjYf>1!@6Hg z)b6!AGUCa{stEXcPr@OczyNsiea#QSHQ_!NRksh_X<8ejEk}9B@@hOI@nwHmDHL+1 z5Q{Ki3Nzq}rrEZz_+x47Xdg6^7D>*XdR&}XUYLlYhmc2>+l=Sbpu|553fhKb+kNFD zAn8dm4ls<YvdG_9=}RD>`+B6>7^<aYtCVJb<w2<RY$U4!MUSNCd8z_0K@y`v?K1^e znKRmoTFibX#Gv-q9Ol}yNz)u~wEn$Im-ZH%y18#2y)QeL&Qcl~eLLk*ENg0WGPh+a z&R3)Aey_<<#f<q8p%%p=KY!{r*=sJ@R*`pke8)22v5Mr#8x{M<=ytQ`Jk)ut7gz2C zBwV;5@A^R*NV6=WtszA7iYeSi7MJe}nJpTqPH(BpHIv9w=~Qylzz?fZ=1woIpW3MT zbYxLoJ*mzQhoO!fWm$3DyBlJh)hzb5gn;nwTr203G>g?4xAyHS!43SizP1Pq)N+Ca zZ+h82*ZSSt<pK*M!P~Yd^fr;YdD>XM^zW5p2j^AK>OPmU5koEP?FG=1x=*|#b>bLR z<w~g<t|Mit%9(Z1+x;NLpl`Z#G0j<-TpwODU^hFzd0NAKiVp{SB}|1$$(WU^o;ire zgC$OTefb_3Ltv+?cA8Iku*js^x@x{f2tz}_45IBLdYl^uBktc`t&uN$|Ka<1l)Sfm z7Y?e_yd8Sf_Xv@*Q4eF1ftDpa5_XIA?S0tXFdsHrCPQ^svN!%&+}>BLz#8!uUr)00 z*YM1yXGL+bio^Xt{50NA>5sv;b>1xtFZFH?M<t=aiA}_=BqE;t23c`&>Yd#Ldj<JV zq!DSNzEJFma#@f<0iLGu`f6OHlw>1pZk9mq@dge;jHjWQRf6J9D#RkKk5Sqw5-fEc zl{UAKNrliv2VX@>`T5HaG*|X*Ui5TV{Mr`B=j4a__{akVmbS`fGK@=4BhW1jcTcw{ zj+bw2r@GA$1DEMl+t}+|_2?KFT7OE+b@>;G;&12nO0qFaOqS)0Gl@THlZ&d0eO^56 z5B*TBI^8-0ky0g)1x^a1uGz;Z-e?F<XY%h3^==L_RIGY4p8fna_fAh``Nmh|>(>|q zpT?9$9(X=Jy2O>;t<JHz=B>43=kA`@n|4MP_52iuZ=|xXHg~SChX~ec_dU1S$R-;5 zc0^-vliqOml;|HOs$R3G*p`;4XD!OCF>-m;x!39+mR|RT_w{t5$f#A`Zaceo=yw-d zt=}8O49;iJB6n^ev}zG&)OcIkaO<9&R!oiYS`2_+j4S)@jtX{NUaw;nX;Mmt2L>u9 zFQ=oNXBjz`=c)LZsl7>p(R01ZT%uXe;G~bpDyFD!cpFG~lTf2oMys6}@%V<%7xos& zqTa4D7%(qC&E5MIf3u%EjkciQ@yeY2^fTX2P<V@N&^xv2=jo7U|Am%ALshhz(0gUj z_JaOUacKOr6pU;V2QEg2Na&o61+kVW$@?OAP|>bJd+Y4+u7%L0cZaQH>39955B_Wl zTFQZGX&*6&3E`kFPCnXbSy{eHwulPp5rgb_d68JHZR}YRCFv-rs2Y~OKH{3EyiC|P zu(W*4rKT1d{z=U7&3dt<qXNsah}yR7or|PDvLsq^Gg3ZYi{hS-VKf1zFmi*gjdMd> z&(nET-P^W6+GXvvEZkTgjnZ&JWtFot{v+-4k=R@r`p=*VW+CLP_b4z`p}R)4Mm4xu ztW*6<_7t}AWhr!dd`4YyWSx8J!MKbS1agu~+k0m4Dq)O{zaFSK>Sjlc5i&u3Yv19< zi*XEWKa*<DVh2<hTR)6z^H%rvtYEoAKfP)_a=mt6Ysw*5UJncs#-u@Bq~hTE{X?@~ zJHt<9tD|vLtjp3KY5J+&!P#-T1`l#vq+grmRi_T^hR}!uUfQp93fL~^!FtwrTXN0x zxTSpKPmNE&iC?zXu2rtvyBb=>UhL&QW|uLSESbEW98~>q5nLOxMemv%A@7uVYOiNH zzDg~uZAYXh(u1#^y+(WI0!>`1-?(plcfRo=Er<l`Q(iZLM~MOakP<4ng=5g<sUeeD zeQx~~g<a>^3%gk?yF&XyrF}=Oy?5`Dvule~A1BF0_<BUQpL4Pg#1;-)*qtgdu|}8~ zh>grj@YHTwaO4z^FLg85cshT)f<fI?Xc|+bh{V5r^!}-|i+QpGp%3|bCG~dRK`mmV zZudvk{EtZuDDSK?KR-!l(qO(;iU7x92~?sh7>~AkR#|80o?D*t(tz1iq*A0}$YNk< zX5FP+$duodP+ZVzIdF@lS9A}}!4Z=sW!W3+xoXIydiw%F>#l70GjrVw$*S4{OmB@E zTyM8UPa+}P@5;-wCA;(t-dlG3+?1%KkaM}%s(m?Uds^3dr|TnDcX*iQ{!eVRz>T!5 z=dBYaX)52c6o?ghcFRRG-FOfv`D0=3S&l+J$^h|96(KXT)xA?u7*Eyt9`7gb#@nY# zl_uri?i(tcRMcFs^t_V;#*?q{?oL`j`3gs=xEHlDV=_j%w^h2gLKkP}EZzB}LY~-= zYYS#wovV9WSMTrb+Y{d^3CC}DN24OfZ3@%_XLj~vo|L+2U?P6gp_x!Iy57qdxDe#Y z81TTb*gs5XyH=3b&Ud+TeY?rKS#+DkTdcg^K%!+|JUCyq7HaK98I#LDYW4Qc3ry(O zs~dYy@LXJaJsrtWO?Eb8&Sv{ur0AZ%;Zji4oGDCM-dKX0Cr?vtcqw?>2$DUf=EnPD zoTBxWg}!lJzm$72<fmh2jaukb6L+KQt{cJaNq0B>E8gCB6|lRy%!RWGZrdo)-;cJ? zkkHzG95kU8E*EuC&MW`EoZ;z+@saEB<D}U@w<*otkWQH`P1%LHPjMw!=okWUsH_@Q z<@?sYV!o+yho7hT9H-5sc6TuFImYt#l?TVhmlq(8Gv^1(wo2SaT|X2;Nk{`jXZd6y zHdhVDpDULMzeazxePLH;H+$?l>oS(CI!+so_b}9u=IXt}2+rOHF;DNSFl+wL69yYO zhuq$_q}F53RuV+H4Uc^#BLc0sNS?U+68c}}<mb2hG`l=aJ|@NXx!-%ubty>Nco#&( zK{5@Q3F^cZ>P(894%?oSj(@)7nx8bk*Lf&v25Kli=5oR7tra5q_?R${AmKUH+oFZ- z;#h~n6tv2;`f|k9mdBxp0^govPHLNdGOASF7#i_-U2BHyB`XvT>YauXa1kwIQXEc7 z&XaRxL%E<FcHYq5!*f<DMLDQ?WMJQW7*XO1cnf*l3IMeyxbc3|h`-hx6%PKik+BoW zQX^kr*Y&MM5>ylOdRd8=6)Px<kB*CBTt3IEMHKJ$mZdpyWVS~*t?ftfu-#!-L=GKf zklK^Cw%S1Igd5!E0<Sv@FTSQ}E<UmzBIBIGBG&7<nq$0$_V$QHZ%oqiB4+spvSKfA zZdUueN=-|g$YynM_mi2P7dES;JMj1s5DH!37DFnYU<Us(C4VJ@2ubF5CXsJAAQOX{ z^EN9>`;|d0PVu21r}G`r#~P8?z9b$y3>mdvNgXTVyAspKrNt|YkihJg_P(e0=f%u+ z#`kYqO}<+7#pl;yf?8>ym)l&p5Tq7LuRvaSWy4?>m5SHB{zpK?)NpvLm6bzo`Qr9g z?y`{4X163%(7~X)7BZ+E(;k`Y;2IwlT~<;deh?RYKy1&|wiK@SjSMq*5zCe<AUtfJ zO^<G8m%xH#WQ0A-tT3<0H|gW0@-&I=EeoeLW~CS810Q)6;dld%CBqWk6T`L+$%T=~ z<HAa$2m-{KsK+3;Ff?o>i<Y*{NQD6rKv?CCuf}<2Ykxl+X0SdT)ueiyy@VkVl~S;x zLAL}}c9`$XD>zBory>$CWZ)q;6^c{%<F@M(YG@J-kKhv^>1ck*GG7|O?;_e=Tv`hi z{@_(jZw^)OIcs1^D{k(JepG}Lm&67Y6pZq*E)ePNbUchsP0e5`@v!RF)d0n^BAb}` zwUq-@+`;r6r9-)BOXy(0sp(HI>Dj>Gz-uUB^aVU3xZ)4OT|C}!d5QG6czqZlu+^6+ ze)7K*^82So2*4SdiHRwS^==bngtyfgjMv>J7nj#sCwK1q-Y^6AcLvq&@9k+<oR@2> zlwxr0_`Nn5&h`aAd^(a2YB$X3d9J9U@iBpm!wUCHdFDdC$l}CKNG|r)kS0_NPB09w z8c|u~!=vZnAS{0K{6qbudzR9(tu1!b^#Vo5cld&rF05;UtQD>PmVEB@8@s<Rr_ah^ z-88n5UMKO-0|jv{cG#lg*V_soU9==Qw7$LsbR^%!+IqBkcle5m3fHwP72JybXPM5< z`1}-@cdZhs1!Q=uT!@M)s-LE|o||)^z}pHx_Xqh8YZemrUb2Gg)ybqG7ua=PU@et6 z9lPDwtrBPt^mQ!Wy8rMvokzB^p2hp-aC*?>_iQ-wOFeAz+?=-h3|I!LuJxQMZ6%R; zKd|TD$#s5NMQfh!B!)T|kKRrl-+qm&yDQtQCKDp&T**zN5BX&x8TXA1Dkpz*A%WSM z((N6cv(-H_$Zr?P%D@T^*}D?Dmn#@ecC2Tw>Vr-#D1YR2ryBs3Zr9*c$<6vAZo^(n zNGHNd&zk+F!DalsdL0r73fg)@L1E`xlku~5ES(8hXUI_59uq@x%|p*jnD*Y^DtTPo z&w<Fb@89r|lEmX(q+)Ho*15k{U;O!#vpwm}(nF8)QIepxN2WBKi9_B-?Ys=?{78^H zVxb@xanc#<h6R0_tnE8F(&F<hlGfChLP26nxZ*^Fqa$O&7WDpgftXOXOvjj?R8V-X z67aqx3YtBF3C8D9JAOgeG@iq<ni+3IRuP$+^8=sDl4C>Y*NK~zE|vYziDDPzTSV`& zhWd@~;puK)h!H9h8as9;5u076fd?_MsM*7vx;P7Qyg)f<*(XT~DCXRINZ?&pVopwG zo$5FC?vHKGHFuwN$~a6DL9sd$x2M+KkYR3?xJwSZ)hS;;a}s7LS!=IQzc?YvzruV3 zb&SjZ;XRTjGbDX@Q{ih$V0G@f`be@H7d$*X|Md|;$hm)`_m9DgSaCeOcQ-VJ(;Zw+ zW9r;>bk%8Ylg^aeJi1)Cb_GRwqUNrtiWr>|;L!k?yra%r7cDEF&EtlDVRab}DnR7R zG-viQGyS+oF`mK10TeRg0SET?8HfqjoQDbV7KdiFdyV1Nj(XX$1p{V{lk#)bEb8W0 zixU=+T^&ntTAX`g5>qd_a7tA}jGT>;lMF2S`;)I=*9UZ(L-O$+LEWvp9J;#nxg~NR zv9)9bI$s~QOx+FagS((SU9ULNMee%acVDh<<6<F{H6M<3wYDBH^*J`dFuK_<Vjp=A zdW5pPqQly0J3hGRSHgg-Ey{3`c1nIMwZY&qTKPqbD8H^k=Y2}XT$MGihbhH>r1dzf zDqRr!*__(F%D%Vg!QycO%+2rQtk<5<u$nU6yC1(KbGn8m>g7fV`%VI=2@($vnk%%E zy)W7q?k>bHYAQj`w;Y=-Tb9U+1LMS=dSge^bP=84kzIHNWo3AGcP;Y@-bCIQf7;|E zOIJNDo|<^dxURUGoBckW9;T+jk7frgTmH6R`BK%h0x~slWBb=yB7$nBccy64Az}XJ z!T7R{-?SHlK78=%;!pOEn_$uAc86t+Z&>ViPklz~(z^WQ-Mu|1=813Bg;Bqf6-bU7 z5^4{_7MBX?(!M)igDJuPmO!B0sOr}&>}Bi328l>7baKf*$oak<W@Om<IobXR7ozm9 zfq_;q;+CbGC`RXW@SkCWy9UX*tIw%GC^#5ibin6%-nMaCX>ZRa$rAa(;c`A<dwu=G zX7F<4(P@MNPK>_!gnsWgJT%Fgk3XGU=#_aOeT7z_k%%G7AXB>PmKho$iO-6>OB$KH zf69P%NALAgsICn?Etdlf)>F3F`&-pPBM}S%8f=wljtj$9)XXfRmc;Aj*F|+}y{i|T zRlo1^K8DL$pM4E7*Qf5#46Iz3Gz`%no2|QG4H8PL(PoT;^(+c15`)^SVfOZ10~p$B z`5)x6C50h~VD;8S=g3<m-!~lg++7aGYJn!FI{tdhtHSxC&HZz5m7GdaFp-l^<B2sy z^J`<ApnvtLKoTz-2FPM7JtM=xPd8Yb;cjaSfQF)w2wdp8lkL@FX=rz!>i*eB)GSsd zc4d0h2w>cw+<1vF3!Ul_NMj^L%G@b<6ObAsb1Pnpt<CPe=^a!5`lWy{r0lFj|ABc3 z@Q1H-)y;MEee+y+qbsHu7Ur6g)4(|Y_Gc2&!7r?H+kEufj^ChJaAs}0&&JsUWTjS> zY!!Ro&g}+ff~(E;#9HUs_K0l`r)VgSYs-EaTUMl~PkgC1&5?x1DjeKC?UEAugrV5{ zqpWNd8A}79$SOU2*un`N<^_ed!;<e48tGKs@M~LABpU0nw+C0HRMpjV2P+PZ%R89A z-yEZR(>i}J#V14N@&y_Sa>+_u$ypi4nZ0;4Gurco#Y>4wj-<y|BBS|;LtfLdMqI0% z5;_%}ikP>a43oht63!kaH@?t`tI~JN_cx}L@W=N|&dWjmnX=nbk$?G_uZLwdSsirx zG-My^O<@8JJ#ZiIAGOa&et)VYjpc-gYi7b?(AJ^)iR=E>BcYg$mTPc#9A~?K1}u>= z9f&Jn6Z&n&V~;97NlH^a61+Pf-w<j-qQ5_Km!k3|fAz}J-#B4-^W=i+Exib(??Kzp z^=D6NQqqPh*u7-wixZo!lNe79QW-UNZPj=Hnz=psktovx?J1S24D3|5n^K5C%Z}Pt z??kAmFw=CN4YXzii0rLE0mcz5MyV{!89KV=Q%i9LoM`>A+e2`RKZsH2EBF+PdjeH8 zy}lkX$~61~8GF5rs~C$M5zjdL*u|3|SZG7xmrcK*yZW9jz-3yG4lWgF7UC9JBWC1n zg|%zI3WrWyJW1rWhv;nQzh5+gV(6bj#gv1_z)(E5Rq6t?AGp}L`rHp9&jQ6L%IoBv zG+?o%B{SHlq`6=bkKY6|ny|XJ8s)z6-I_;K#r$&*g~h~1%qAx$QgxMWF6wGaaVwcR zej<pO>tU!Ww^JrX+vmv8r;b`^HQ;Ikr_5vjGC&wgw}vf1DdZ`Mra7(zWby^HYZQvc z>aLy4s<|I{+6OG|bEy`HT05_Q>UR<+1tslu3R6<xnG)*|q39rrpB&<2d1te8mm+UL zC|O(d-PJlvEmHk><;GZa^TpED20R(U%ae0I?M0sxV)(TG1GZ;AJ9hry&Ue}vW5eo2 zksaZyh$&7E3hjdkk?2`pKnI?iiEQS*wB1;cJAa$!nkhxq(vrQdKfUI7W3sBbaY}(q zNTOeDlU^uN`cf35*Ix#+<lliQsO?f2xp4(^7J;GPf>`XT!-0=xW%axI$Yh}kQk{s4 zD&xsvQnP@M8;s02{zrr&gQY7f66Wgg&_dl9Kk)&fUlc4GK|@RS^$lC11$Ujp1-ZtH zwVNaH2y1XOTe?=OU*Yvx&FZr*_qBSS&8tG(z$R$fu~a}jyr}P6^C7tQK6Ol0?!AnC zN5U}p+u?F6ILBcm3rEWBx=dBi-SkpaVv{L&<4oUfj1OJbk)7*cflR2n_UXLjU>srf zq8&`7cTM)FJ_UbXuB)8vJNzMK)I|wlgsaQ<`1X%G+X7y2v1s^rBpwYExORO;EIphu zUrszU%t$<pfx`;pS%t>8?=E|Zqj;`Nx9WOYD4qzIiI$~;2KG38YyI>+6;;ha(?7qi zHSq{M(h&_}Ih1Kt>xD|O2!wW0Jx>_s#!L8-u{kl#Zt_-3z%yrbez$)-e?P%qe^!-! zu%#7}`i7Ta6|R4Pk#z+Qb)C6jq=#eR*}UN`2Vkvo-uGXO9nDt-O7&jSQc$E@Bh$nY z#^t!jcR#aXGV?Gyo=@mjlLzA(U!cw{()m5yu;|>xqc!^u7tZ^JT#r*RM|VfbTBmeA z|J?V57uV8iWAPh7eDQ>Fe4MA)y+#A~i9y>@%`g#oVhxd_+E4ptn#9B$v)LQHbF0F# zXv30%+(?5tZYaU1K|$749ZM0k5hlVJgZh`>XcZN8!>kg9Z7)SoGdYz)Lr8<|qA0?q zs#F;;S@fQaeSu8##jNcH2f3NBG(UXph}K{GPas)P+W2;fffXwp@>z3U6<$VqJtFCi z1JaCu4)Qr;g$l!vHo3aYmG!>*w<=nSEjkimu&eOWagt+V215br_hyEWu8Y1Sw2fOT z6O&E{2cPc+QM`}|N5y@idZs!$aytd|vA;g!R$YV9+p*-JoD8bON8s*-+bMM+N65%@ zE9&V*xai(b93)mkNanm=t87j`$9-FbUO??x<-*W6jfL?dj*Nn$wolCb>S({<YwKJy zE_31g`q{>_DwQW=?R_0f|GGHdylEx}H;^@hVt-mQrl+M;*ekibqof!ZgGoaJ1CN=k zr8B9_z(W2*mEqVIeY<b$(I%fbJDZI!LC2_&tV)WH<F#Z+)WJ@&291hF2!q5Z_$ya* z-_4g2+CWNCPYD=j8nj<HITuWxf1`()o`dkQ;ZmeK6JPH+4%SMneHw2lF(k%iNQTw( z-e<a&#r=_%>?^)q9T2=7<yud+6$TPH!vVBFJH|Sw2&QjVuGZ6db<hnY4kS`6S4Sze zw@|32iGC8wXf;vDXo7-RxA1ONPtd)1Qo>FxqX4B5_#=p{_dOZ3FIoA;JLQ_P-~#^| z4p4Jc^vJBvcGp61rJq%{h8DM#GSGGjJf@s=cq~vYA@XV#dSq;@xxXKmH;E*^vwyjS z+97o6AJVw6#Ifw~`_L?UQfRUk$$hKl<RnR!w;kpgoG~l?G^KFg$Kw7UXI}vo<<_=6 zgp@Q$mji-|q;#hs5+W^KB1$9OJxGa&G)M?YBi&t6QqtYsUH=|D=e+Otp7XBt{okzR zTCQQ{nJ4zX?)$o~d+%F5qmALICLS4{FT>}lfDOjela(4Kt7GNHj5-E}Fz}-mLD<ut zx$ji!ho^6wC|n^z%0ES7eG4JH@^K9wLpNaK;N~oM&qezkHQ<KRc|0p83Q#Qo$^O7< z$RSV3JDn184}Ni8-#9-<DZ%#k9?Hh@qIlDQ=)~eLg*@LJZX3@7z^T%l&kpGbwxS1G zLQnyf+iI@8mxNTXSTYVM*3pSPR1b8o(Ny%&{r$08bDVZTvReFled1C3^GY63ZPmU% zennFAt*o{xf?t1MrcG|YoR=5@X&<-_n6ok~9+wSTspaTxJJVY|@zt=eEWMT`+6U3k z0iZ~#7-Q9r6d;*C2w+{lIw9HwooZ&5w~f)r2z&NakwQb4cIns5$6gv@nD7pMjr{q> z&fhad`K!O!YWq`KFagHXmF(y#7O_bv2B~$;Mi4+}8_?gk^tIC8k~`mDKAyqaQh>_` zt!0sIOj(oYD>7Nq8*iDdJp;!Vx)i*t#zW)QCc0AmQmf`u5Fm0Scy`<jA@FOEVnu+^ zrg~fNF6**E`O1fe*VioO)_Vtb#%!+}lXM)xMt<1nWfRJi_ste?tB;pFc9s~a8^_zQ z2r^oWUk^m{Yig@{!7UuPW?HLtjr8WtS~+LO!;;e0Ka8BnpbGWFHafG@UU|BnBt%^g zoh_+%8E_)&niQ{pQpNZ_cdExbw=A@dk@MQ20F5c!ZWT<H@KiBY8)fOBWnH(o;j_E@ z`An}5Yc5wIB7fEbbdTq_ACp2v%o?SO%opXcfw<{Xewb;AOkkeF2A0q~(y<q2fMo`J zUuw*GtD_YJJgCC*BZ8no{2RbXv4_20l!El!`i&n@!iDUe#WTwLKUf;-MRa+1J7#7^ zK7IT7aV{6mxMU}!<ArT+*>T#DkgSZqYWVd}GU8={!!VwaZbj&=ptv;w^YM6;aCP6& zeSAKFt5S?+dP^BeNvC^4GG}D?fu7%rTH#Jp+<cWCGYr;%3=*KE^numF*-LX6*<(xs zRf+ki95dIC#diwP!<oin8K=(H7uVJI`h6gthuaKgFAMHK1gf=nH#)<_mR^UYqRJc2 zqfXBal@Z;AE?J~m2z$oH@_{OUvFxGQ!NNxZyBibOVbjW9BmZ(%N;rkSebVKG<-|lQ zfsVa{LKUO8Kxc<m-v@obwnii4gCI0q<(6`)RdaH=BO{AqisI6838f5q=R$C)I0<bS zP+=`lKpR9KM$UjRy?`NwiFqKtwCg{YOWdR51-xD9lBw`rhcpp0&3RhrdTY{cFyh9Z z=H~ep1tA((D`nZLIV`J=gKJy6Z<&Qd>+rDzep#W^d_k903SUyH@eK+|*1pRqtNemM zpi%)dGgA>jN^4RG=q-sYP8OWsmiAIt0*6K+0Aclj+^-QYa&hQMugWL2rk3vXp`-No zWuG&DV45a8Q#Q^eGE=$pc`mMdRsH+LUB~lsB*;~z4dsEeAVd#Pe>#uK+qaw^Ubrr? zznUT*{<a&(h|7pyX6Y(nRO1*!pvcmn;51_y1z5rJ4;gc<X*%5izFv?cn8<5!xW)*1 zoY`cXci*v`jB>_|yfc)Ul_U95X8o4t+c)bpC6SGj;^+{bC@1umhb6Oify&-LVRuIk zX6+BCSFpZjD`m7==85EJF*k*zum`nCN&M<ec}yTw`<O>bqDIzbzg}~l1Xk&z{;1JV z;hsTfYeKs;KT3s|e*x@^>NppXB~z^;hA33e>bXbiTrMrtC;Z@726&D@`_Rk2rFnT# z0~U)0J6u;&$*7r}oH*5YwsmL9HLu`Oj3xy--A;FKz6iNx@HZ!x2!i`Jf*3<dji<p7 zp14dCxK&;~K^QdV_NN{dx?R%Hyk`7V{on#gdr#6w_}z1bmr0a;k?YCD<rJ3rXSmtd zyBr%EW)-s_&{Z-y_983dykf?2)pVNV|A;y@7)7b%v@^~R%*8_+=Bxb%F3=%GU)>P` z6h<uX`fTPvKAHSsTH4F|Pb6+V{y+nN=0+M+=cRj<4t7+Ie|(q6X+t2<c~%q#slM)c z6)%+Nhu0@w(HF=!^C|%)pXHf%P!J;6n~=Y0ACwHx)m&<1Z#&(jrKW)}5?Ndg(r0@k z0aBc92w{O(;?6(r0tzH5EF7kKZf1G@B*bZloJvaXJjw;LK|<;*p2wVUL7p8V(hh?x zED^Fse-Sc0tm=JNi392)rY?^6nG)6hNJNc*Rv<b^tnsm}wgy~oEb5dp)kWyRx92=$ zZg?-Ee%bgC;K5-iCC`8vr@EqgOFIU&Hdo+elM)HAbq(SxHHAU(m`)^1=6n~mB`-@x z9Cw#CFl8lv`I*qeoZqBb(!;;D|Hs!TNs$zFk6Pc`sTVl@qLdq*NF*GPkXS0{0k`~T zg3|_nHsk(`1Lsmm##E_L6*PLq3@fIVOm@dnbWjkCb?}pTcy?$kw$QctuEi9FlwV%~ zocNG3B=-%9Z_zdRg!M{Xdrf8D&u<07Qn~A0)EDFrT3f=J%b$ibO<87XJ89gjo%~K7 z#jNEcpDo_LQ^ji8aKIHL8oSo1wfhVTe{_TTOE!HDl2oqHx3(bH`3EmJ4C`$Kn_dBq zYE2CZP_j;Dk~C+wR=sC_HuxXMz2P<*p>e&~&hPW^Bv!pzJO)nof>t-Grz-qc81EZ> z7@dofkVt6gjK6^#+@R!&Z*VgE^G%7BosvlMCtBfS6pX$1Y_x3H_}`Fa)NZxPV@fS4 zXgt~G9@;u9fx?SH)~_5#Mn^@Uc&zv9%eo7r*E&D`KI&sQ-fRwnlyQKdjdpRJI{D$F zhMxru(VQ$)U+*nu=I6KZ=X;pF&d$vbC`-!dJC`c~?MOXuIITJdk&>Ini;IuHQNPce z2uMRfA%aWsEL<BRfO|-UV0+d0FzLM9=aGOa#%<fF{unolic&%YX-4r&Cgfye5$m^X z(n@ZNTHV|l;7FN8H;+w`|BjB@?C>)ukW3U5m3ifc5aNsaf>g)KS_yiO7s;-KY~`IZ z2y~do0vzl^d4SPQ2xt|!by5uuy?L&i-x8^7gSA~#LX!5aw(CwE9dzx{uNay&uOBip zJA)Szux9ARc6$D7+Ke%}goDgmJ9?Wuowi>JN{Ybl@|LKd`EqqKNvov4trq|aZQv<! zUHtW(zf$5y-u~FES&#Lz5-M{L;kxUO7K-0=)^nOE8U?~*I|qg9QE?^QSS>VihbkX3 z8b62d1KE#8tzGXeqhD$S?Yb;3o)0?o?P2$l!fs0|4jut{z_QL20fTR5z%7DiJ#QG< z4m(2%)sy^TL(4I22;2kUy$c=|{7;W;ueu%(E-j0$E`|QY>$(N!Vwm)>b}<(RPOTSg z`g`wSEm|pme)zeziXSa1=twkAkZvUgMiD$nYb@1IaZKdE2ep->)49!G@4vEq(-bmn ztrTtN3qA>;B;3!&y(XBk&ZwcG0WUnYt;DgWvXbojZ2gS@y9+*%a6GJdyN+#i`nHE7 z^6jkzZ;X<$orwmws@=VQ2A)v~!Fo_R)NtHEsy($0Q1yPhlC^grI0Hr9@8<#j<oSjK z^i8d;r7i18SJPm}wy2ru>r;$gW)I3Zp(|aZ1YYy+3ZftM&ef7^ZGemmz8GGTl|MW) zQ_BIt(ulYCnw#I&;I>-6J=->$X4U#ZKbb^O2%EIZA$r5%-`v@cfm^t_s>7-0w9qw7 z^bF6H9)5d0;fFtlR)no^QLwSicF#f@T?dDT4C-g9FndpNFzP*y)8jUV+wz5>QXy@n zEZlR2Expz$ft4TrNf$%>GW#NRHv($Cr%uE{hV?n$3IEpVL)Qv-)c1SOSXqA(%geL# z@bHnGW&ab_m>-$^_#ft}FE0+A2rlx*q#g38TApFU9It;V7-*t&MEXb7$QH`bH{6L} zJR=;Xb7{>H3frk>=MN8p)B;pF)jGESc2UQVCr)n{upFWi65oNJH|#f`nHA2YzB=eN zVRg9T01YLPoOu2GxuRHfn))O6cl(nPJ;BHPkITT)7Lkin-Cih{5TY=pkjd-p-`x2V zvrUe(3)|=F6F7lugsZfhz41zI9<BY6VR|j(by7O=caJs%5Fs#F1v$u`B<18NJ^hO) zGD3~@jbzcdAHu`KTF849ZmXq1g%2|3to8=$b(mx0quj`MIEVa~v$C@FmT4Z3*9E8v z2ZzM@<|bLr7!lZoSI3Keic-_>?_$G6MgT=uYywachER@MXdtZwpb7Zv!Ykr0m04s2 z=@R0Pi0-~#fgmHFlaDPePl<p=rmyH)MWK-*t6B9UfwS!w53+}@x?=v5j^RK)FjBCw z{=|uCBR{LLI>InwcR4wAu};NT41iBZezV@5?JvWtxST>n>{3qI=3r-EeZ#jw@r+Sy zbaaG}4{nR>7Z;aIXTkq&TQ&w(dIF@t_)3eMyvoh{H4f3Q`}=*~2eIC6f7KUyfo1<i zv({k8_qFx)-JW%7^2C8(!%gG8Mc_QGRk<jiePWh>9;HHZ6-q(r?c<^VrRTx~4<;Zy z)lt<OK2Hy2oNK7g9kvl6vhl78ahu42$cFUIhU-g*C-Xlz%M;^5n$C~)PS@&4$6QaQ z_%m~JTOPCIzS(tG!@-e_W~Y46p*gd<*&WERM)zA+1g?m<h~kZ~$We@!{J`q@eF{Yl z9rHmcNheON2{TOHt4&I?-Tv6Nae@7x)xx%b7h;-_gMEdPtt>E@{jqRF_A;tKlG)g6 z!fd$YW<Bu*M9b=~z!hvhmpKN|$h>KCfLy%EKhBK*`uN%1sE)#uS(n{o&g`8T!yr~8 z^~~8rgCQ*BTho(E=gVC#$YkAe@_dTKJFDC16^#h1{Mcd{Lvw3iwczD~u*_e>U-IjS z)3^|O;>*z>!0jNL#OeD-Eo8~zwJT{=XiC<1q9GN|k(znd#uR@Noe1fBXdxuta?}=5 zASrlW$!F~Y9{X+=avx<e!=?*FWFifiPqJ-C*x1;PM~j@fwSeaQw)EsoxUa4no}_a1 zFf)&w$dNH5WZnjp;*r)zSZ>^l!iP-g@)%OTIb$!^xbTrOwA5@OFu2(81EUdI5E^?m zGk)OC$VZn#w@!=d(;s|;Z;P#tZCVCL3n|HT|FGA<(8f-l^@`@<Y+X5o0pI7lFu^{N z>~bqziag%xp|fCy*UC|L&i%HzfGtV48kDExrS;uD$B%IjC7dcAs2*EBWMXa`&lYN4 z^ANz|=N}g;q;zvNu`;1wL%De-r`uB;uQ|Bc9tnK(LdKiq0;7p^Rf-Mzu)G%;Ut-V= z1m-@<?+zy7hiidKCN}J_c7s1t&wRn6i@QMJ;F@+RlJ^;oy4TPm>BW$APRS4f;|q!6 z#hXM>bFc7>iV{Mvm_UCkJdW~B`Nz|{Ysopg>>0H%@Bup<oh6BPs2h``R`Z7=z7H_z z{)$se-Fy@m*4z*pX0I6KKi^$xzrqCF4CnId^$w^xq6t7-)0+s$?xN(8&BPWtyp4Zo zJndG3I?Z4*an$SdTPkzN>djm6fln5YVBA@lzlf2QHaEk4YyoBPg;`VqCfhhF*$_yR z!rU-eAW_ygmGI!_`XPy+1=NULN*+y-`peK74cZ*lfNpapt_9&uE<C<+$ahih!T^>y zv%h_YduX4P!cON=dV4r;Nqp;3(<c`LHUS$nh#t`Bf|CdXBig=y>3dz^+8uGfU2V4W zkpKB7U2iY1S}U864Gnz_KXt*}Dm9mHTD;BsVN9Bh*`2Oc?!8%2U1{`rJP`7myjzN9 z*0)1|cWK2sPsb7K+@4?rmN>+~{k6Z*`=FaJ;!BiNwm*K!14<^cvfs=sZNUpg46VLU zA8qk@Z!ivnN>B>%aiFQv;0Lj~=pZ`WP?A9^hA>Le!Ys5Yp;|Ny(pL1lYc22i1cjcC zz5fZL5hy$mGScm61In3KRR+G$CHccK-QPYBTxb1n^uO+M<V%}>Ne2n*2x|6}PVoD^ zhps!lKva7t3Ty(R%NVk#Z@s2lFYOURsRVq8RatyfClFd5a?*95T^;V(J#tzVh18!N zM--YY<H^RdRZ0p|P33{PD0SDixUCN<qj1;9au=xExOleQFUqJzK<wGrG*2E{vszOp zN_4bm2G_>e=t2*(h;XZNuiAC~feGRUc(N@2O?~s{`ag%4F@r7JSb}B)&{fvz@+H`0 zh6$8tJ{c-9oR<P+^1#_Mn5VT`Yd=*zNB@VK9veSe6qK{O`;JX@vyG#SA38Q>m$zFe zVeRjC03w+`*U4)QGz3=iZ4!QK43ITODMhJ>B){X}d-mm>(ic_*<CF2Hci+Cr|5Ku% znOWO1bJPM2IM2?UtM|{^{FjRl4|k6C%}VOH+KiWMWB-d+?9_BmF?28eyb@4EP-ydV zbNM8>Xr%5O8I<0Fls1_;TLK7#5E`3zXxLkVfxvOQcy~haWX~Pzh90?-nhJl>NfmWG zASo}ev=)Oo^ZpJmsd35ARNmbGB&S<1`T9&O|M^X6duG`0(sl?WKGJ4{e`M|}XIPoj zQnHhWd1z<X^29@D+52F{B*1PIpe1@0e-e+j^$>u3kx)U=^-yRs^ed`vIal%fdh*Ja z@AD%MFnYHjo^^Rzs5Lq`5O}$EN79y<Xd;GiQblmgaf|ix-S6yh__IeQ_{I-`SVw5= z#<r84{1?Y7xAge%*n99veGCCHmTqHjQmvU|4Im#{6Q87?_K0Zbq5#<$Bz_B8=DJL% zwn+n8vvzmfYd)dos-p6{kdKT>-^GhX{OtCv?YV|xNY;;+z(RkbKKOyiZSX$}Ti!IS z13LFV-SaI02~~f<lPgDAG)%(d_8AFfOnjw6=6rEA5P;eFPYznyudBZoo7(|5qwVuV z(59ySe7D-KWSvyMswl?vo>24Sa#=TdTRS`M-NR*5;1B*G$Ix+9H8@Kcv3{I7BLp48 z(Xeouo4>I!o$c%&i#aN@PF?Oh-`7r<onJ;^p`I_w$4eErGV-8bVwwqiAn{8&%~F8) zsl?=*&-F3)#^j;kR?Ins>SZ}V?ZHl9>K#;g?Ej&+Fd_Pc-v6IrivP%R4_JK(F`4A> z49r4VO*052G7w0sLM#BK6d}pK#FXhi5pP%}4xH{k32>hXiCuexS>ZzBdWiyPPv*dX z_;=ddX>oRB)wPe%nVB&JPA9g@XJI*nH($<Ya={?>5DJp?&<aJ5aC)DR$-Nc?3b<)O zkh&2T6uB!BqkMvdll*cM1XEhsDWO1UfQzxWHz$X7-Y%`|?pj${&G_Piw*kvzSI8ck zYdcVefClHX0n&f6HHJ_M-^yZ=OV>+WRytpBH&Fl3lKYt4>VUgqQ?#!sKo5^HMt(<E zs=Ll}#V$Og5`&Gg#KWbGG=KOvz#mw%jwbt@859}$iDv&q9}OS9RW7~}+#C5Il9tEu zaLSmeFcNiy1RyA-H8dci=p%uc(N~N;l@pswY{JDTJ-sh1@$p7hR%SA#ph@80{k8Dw z2Y2o)MxNYXeJf3enBGGc1XH{_a=PhGA^Z-WLs{86G~}Gmv+?}I5o!7=cbll!JHL*b zA@N3vDxw}BXSR-I4dm7#&|n0$+gRI{;)Bup(FIVlIwRxxh_qao91QvA^HZm$F(XWp z4IdRtmM4-<cZ5!si9i!Qn%&tBxa4@VP=mtJv1)EKirTNMk4g$BzO@ynwk6w=$y@oy zbyi^J@|+-ng!cXQE>o@E8kDcu{QgoBwqmdxWea<Rtn4f{eHjj*<pcUIB2rK{d-z`* zy^>KA$HC`d?FbD~5H0~ZNLIYbZ{3SGgRb?R*rF;DkHwt<sa33z1J)8e6qpnFQXAcb z_*Sz)_$;rQgGtjo=s5wp)6XXi3v&r%+z2Noac)%LPMtd>i2&ie*??`(I1(tUvql79 zc%;;RDNRQSxE}vNWOK1#8jlNQO#cmrf3EZScQ6#E!!L2HTlqXq2A0G81IqPw(5Fy^ z2q)m%g(IE!KUdTANltE>9qJ7LkG#e8vWBJv&uXbzQk>Q5>cF^Z9d7&2rAx@zpsc;& z{JDAnvXZ;YZ^Zf3E42K{C5`=$I7{q4jLFPy^?fLw)mpkom^~Q(8oIXY_{uvoXk#)4 z5faT4S#l4Slkg9!E=|)qG#o9Ase%}+1ZoBK^5Qj5Zac83$tF<VhQN}9Fve@8r2t1@ zwY8uCB}{))cS8u|Bm@(sdu=l%66`mJvmLE>QwtnE=ua+>>%D#3_Vn&|e3074BQws- z^ZP`I3}V6cZQ_yr@sMFd20B)tt=>p<Mlom3SXWgHcObajcJv_C`>ksjhf)i9Vw8}% zBcbR&7s374`$rpQ<;@S=$t<hs|2RN0;34t6P86V4vyT9u=Pw;5qLA^Ha{{)aVQWLj z(*|_Vz4%7WP1MPkYTZhJcm_r|&U50~<!;xG`D{l#J{v5bQZK}=7(Hd8f<Jj^w(Zs+ zI^mLTKz1DJ78PC(I6`I1zt3*R&rzA&`nSwdu*c$lfz%kNKzE5AGc&Ap7QduK7M);h z>yK!qVteSze{;NbHK6qvqF&zy01rA-gQ0)+<!|LbDJj)tKunn3^P`Y9Pa`1~#-FE` zBrEB?SVer3^_FdiS+~p0D0Vi;71U(u=1qI0T;Kyc#4jQO6FU^!zqcrD`zZ9Q(|PH; zcUm2BFMuB(81}K&A5_$=<rKn5?s)&Bp#<U38Y|Y@eFPS~*`m_IA+6?UTfU{clF!#) zs&R542*97z|H7%qrQjn0G#g$FB!CG4^%#%;(8m8;&WWXa#k2~y<fm$x5=hnNnj)-0 z;V6-iVB3A3ogMp@A{Jqcm{h?~DxQ!KYS$pO>6ggBOL_`(rqziP9Yk<Z0@(FFPQWKZ zbTFWWUP26t2Wq%KB)K4g-kZ_kB&dPPh+nf24H^7t_cL<Rk9P5JZzDE)dwM1#eTY+n zK-#;RMeI~K>16}`)gOud{E2{ucEnxXn*w+8Uu}-}-<hve`3JC>h=DmjxWW_}l;>4r zo*(Y46Jd4`YhF=@laQsf52RF4pFgG6)>3U(Ulq>sn!5>BUkYG(bQ&G`x4rm=JQT?Z zf&3*c=x%6(rHexjE^YnQZkm+_T4j8HtDZg_7r4<Qv!jlh${?PFyAhzY9O#X1(P(nI z-$CKgAE{W}MM)heh9B+2&g_sxpk;vuav?3h%NHDm%O%RRuD?8d)WRJ%P-t{2zH?-a z3c0Xb<Xbs(-jbE5fd|(&sGx3|r<{cn!m5wYVhv59|3?JMIvvccz0&zGh6cpcl-Xy; zKxpEha4-QX)BMLy<8npiSFd%IG*Q3h5^hn+K<X>Hp50gdR<VzJXdv)dbP!t#=hc_2 z@Ay85{`DR|wE?8E;|5ANnswBS{6|Tw*?yGx`xdCce^T-_tLX&NXKJC0?q~PS&Y_eb zZEWnSum+}`_<trm+#oHhIUUJZu8@QV)fDdW#tYgZhOszNMZD@+^2I$w_!FZ#i#jAG zy#U+QSb3t4&|{qFJJ^bnI{zC_2A6l)89@H<RL%>aN7o4YhYkwx0F3VZkF@3a{l$2? z8{SFkT!Sr~ehq=Z;`kx}{{=hAf0_rTsWlXqDGqQj{FeOnJHi%8$(^0OCsxuVH}D3R zPmO_S7#|tU<MgU(zYT7Y!1cJ)hl6h@gfDbq&0wX0!t71r0^a}LbyZXKJim{|%!orv zPyhhmAk!ZR7LRt5n8+P*>dbNY@%{+Re4FEBZ5~vw4`9H`UeU@1mZ_%C#Q-H>);=NS zRbvs)vubT$=>yMVjSEyT@Swx<lJ?k*$r2<8E3^JXWb~-pc$|uNBJZ9SX_wn`Qrno2 zrcV$A&4!CZApeEhBGdhY6^#Zz!`~@L*=TK>!h4A&1>)yQ+Cg3prv_WJ12kw@#N=>a z`EN$hgdY5yR_rUCim(INk06jadWB_Q+;C8!`rk`IATz_A!-Wf9Q4t{P(+aJB)RE8$ z36L(cv*^zb*aE&jnOs|xzht=jJ^%so)Y8KrY@-ZcH=SopJhRg9xW2zb<Ih?ENBu)F zo*$IYb6$u;ciz8(t_cY=;+D)lZ`A=y!l0}?$hDWKAe(KcVadQ<TvUnXu*F0ct^Wei z#SBAh4WwPhG&lGUdda-Ok7yw3sg8@w`VPKe8v^lG$LyBF%F|`^R?YUyj`O~g6PT$9 zbS_15P@0bbX_i|G8I~~mqf*4m6c2KLlJI_)66uxwAM2=r4*5=F8eZXC2nu5D0Q>s_ zNKiUMsML>PVKEhT?__-+wzoP0py*@bH5fARWZ{nFt>JJxW`fdEdl(gQ8;EqY|Ed>R zmbqbq2oix=T`Pr+6|0zD(i0W6a)DrRpm6OOnvVNEPG)XRAfS7HKBKYACVNQziZw{I zXIKC?=>k<b3mB+r+dOCqszKRNHsMb2BGE0b;?aL|vVZ9NN@B59(?@k$rDo<dxs`qW z`zk=F1uGb$a8gJFN_T|Qfuh=6Sc;5SBKC+#fA*t<+2p79DyC1tVlb%n6(I;{WMjcF z%qRVtNh$x5=yqOS94L+)N%DOZp7e{cod<d3ue{pv7k{fgZp2J4p-<VO?O#81mPb;I zn3uwy+0Ir2g(*1KCsqU!(5oz|!KlT>^x|{DKPu*d?;S!gB7SnR*LF8$s~$Lm7KN53 zO~e5wrpLdN9(a5uwKHXtr{YI0RIeW??*P*dya?cC^<&_UfQo7;^c5)o(t^+FyV=uj z{t+S)Ziv0IxSTlj_LmS(kgWg4hBpB@wkDvMAIq=*Ruiq^P4WZR+8z~gAKr+ftGLKu zN1WeoQ+xGTEE&aU9S(r^Jq~#^a0qyw(@8NA6$<`>NlAplLM;s5KjeW|vP^8a$|K(Z zkm=FaEeJ$5x{Ev?Zu2~987vrF*6!_YXvwh!n+J)!+du~2iYZwRXe$4#?I}gv%_UH_ z88qps69yN#RFwS}dhKV&hFsedjH7K79{EdQvyCnXd7O~zxR{{B-R*jA-SnnktwaM$ z&HrtM0VO~UQ?jNd4qy;=B}RF|Qzv>0pxah>SH|*zB&a1G6(9517nEffjxTX&)qVg= z*$L<8qzop~q%E|dZ-^6Pz6e<!_iREu`{w7tqG9+rF9JUQ{KaY~P~4Fqz_v7^;t$(d z(F6a37sXupoXsj+yQ0`GD>MO<@I#c+$ZYR3S4Z{*`B2BX7?&G4i|rw}OAhmmquH6? zOi93!)uwNT8YH9y29DGDQTc`)&v52WhpELip7P#UFF>>$Dc~ZNA?Ag9&d+pQA*he# zXTd2<l;42v3ox#)CnK;y?!HI`ZmWjGYL+Ju&wCOtZb7UbFp>is`~vm#zhO+}JIue- zDI&<sU+R?cYXWj%L~j~@8W<iqwU?4q4-#b0*76HYKFMSJd75yUFYi?JHkz%Bu0FMd zJd@aL?2OM;;CDZ%dmi<<-k~0XiA@GrD>NtshTR&Nx{L>|nj!QeIJ{3B0kZZT<Nl{m zT;KHR^-mTUgiGHlT`|Lf%wV<s6cp~vW;88>8Y@`edZ<lX`b^VP6i{sq`BI!lad>#> zxO^CfmH&2qeRgxX1MAt%*e!8T!+_|`86`&2q#U7>^CAO+;#XOWXd<uE+#j`!1R?B! z*IEPR5EDW2n+zEHeMTN!TwIyMgXC9vc{zT$LAkS*ij2efposZzYbIC}BO8XN2X@qF zAE82c7#9m13R<4Mr3#Ryn*(Y+_PnwDph+a*w8)Zz)Lm&hAmHJH=seZ}){i%cUhcN| zQ(D>C`e#HjLhAn;sEz6h3*%_<INX6e2bMJmG^91mH<;G7!uy|9IT3y9|60@x7Uu^S zWZw1fwfPSx{ex}fH<Q3jH-Ob=Ic}!>H!jyJig>~v03C12!WA1%E+PoKsx7@|dH+#s zGC><-KfiKxt(Zlnm`XwiNpVB6Pl!&~-)J5L@|QjoJ(bdn7v3O!b`2VO>M1YQT<;#6 zX_&BUJQYLK$MU_mfv)LUfQG0eQM_eW_99E$jB(64U-)q9jf=~;%v<xfcN`>eM~@58 zzthI~CiwbptQ#r3S~`~Ez<nw<LU2-j^&@`2%eKe0H>SgNSz!6J_6Py&Wz~EX_Ugd5 z0Rp;IQpH%K1})_+I@CD@#lB&<t1l{BE+~qsBJoKUrUe_{eASe72NFz?KZXyr3G8V8 zptC%g0l~#5Yd<jAuE?x~js@%mUAva|9oC#n4ODp?ICUZ+UKFTzHDmL5Xb(r<$8W`c zT_7BngmWyMcQ+Ag<HXVR)IuiU$U{!`2_Hhk-R7f#>NRLTU&W*aSI($>T{%g0wZI)* zyo#lF#$pyS>#EEMdI#l!Bq2Y(NiM80KD1(8KMW(&IzF&HHpD5=R`yVd9J&}TDni37 z-f!-8{{jhWx6`+(nzZl}wV*ft`c=U0dK1_rd&>?&^p6Byh~kAm`I^C`f@=Z63n%M; zXv4X}pi!|3Y-&!G$jE~=evv>pIfi{i4`0%}p2W@Ajktpehwys@(C!H5H6Dggs)n|< zAN9#i;3WTezsTNcb7a}Kuxw^FIYvQZp|$v=i0*UO^cRtv12KM0Pmg6<S3_c5`y7Y? zKl;t#M+=<@Lg%2M$!WUb^6Oh5dPpp=f9vs`;P{pI60?3~xANg#XcU9#sj7U2(cVv` zvSB7oRuiW3N!c2QM-`)fUDvPgWhlJT+dnqGFh@V29IWXO@;FSL-cRy~_Ui5>_GA&# z_Q?ofD3h0id;yy)`PmNJZ0wk7$rIO{+0SkTycO<+Z<cg0X>4S&Fu=x#FD$I<nj^&b z9GYSzwjcw#oh;1^yA-GkkbLF=i#ozP`&pR%Rf4#c6cq!bq8ON?;%fM@ciV%%m1|i{ z#c+m=cuj(K?fg%9!yCpeyYA6(c*2g7qlyK~#=GmT&v-ZaeRnTBgab#-poc~%5b*U; z4^5GYf~nwG(%!xWLiizk3w_>wkyywYU}dOykfT{bSk@^yu@ips(j442t<Rn`{NalF z6eG5To`xL{)CC?%*uAF_<P4gdd(kRK<=<hHqBJahh8?VbT<Q~5UGq4jsH3~b<cFZ) z&fZ?VS>7fw4b#1d@BQpgLc-c!bAXN-sFke}t2BY|$HCaJX<z7m$PRfjoq~}GwJ|;j z3e!J65{GO|9bqBOxj>#PuhdiNwHWc8vvARj$SaC=+_TIssP_`A_a8Omv(yF~$6k5a z2R{*?{Qd>kgOUbMk^QdCy^ZlA5aJ(+i8ZbEW#l75p=uII2$Q_-Jdr7gsOQHN$<s4q zMvYsRVAoINi~*Hlu-7S?i)Ts4m4JXi=C{0WE?TND6pzb1JVU_e<umrHmq#~MW!`tL zG1<Cq7v2n{XT|Q4=H`Kl^9=f|E1!tN8JphhC)<`Cb;Z;Kf6?<0Yx0J`UQ@sC-fD_9 z*0kbH0DG*rP9=041g(!6d6<uv%<eQqKo%AWK_Dq~UiThH{{GqmAJjb#4)X9?b6|P4 zXR<z`q6bTj5#b$U1CA(%5%9y`HqO{rO=9>=WXe#q$kttBzj>1t!|B4@U@mrt5U=9? zs_8Rw?XvZ3B+zg01^qPd`{vzUyH4Bhdojne5Bp!R?-ps2I*dO*Ydd`H%>PZpmCuFu zNrV@8!1P6dUudo?N8{gPP(C^_!eR(bo3kfhnJi-zc05sqfG!9z=T{iPPms8(SP~P7 za@<wv70ACKo#eu#v25?a?|JxVmW$xysaVNr1fQ7GwQV?<q;&Y+kHM`NHSG$R`1vbe z3!y#}J&Or0R@r=oPq~6nYB~DF1w}J1VU%VZP(iy;`S5>2&ClW?_`FDKQLp=M|GE)Y zNKl~u2tF#+jv(Z9ZkrEoiAh-8CtIT9PRq)Y;mX&%UEf|<_7JYWL1pMAVok==u|7pc z{=CQMf0V$-_^<aTf82x6$w%kt!Ephcq-o+t7iR6~HD_X?4kjt@OEB}|8nYiyLz(W0 zd*Q-yD9Uj2M+*c~Ub?xhP3`8-z7Lvqaf*?4_VTBc4emmReP7vrB!AzzIM>OU4UwkE zN0fxx?33`MMS0V}PlmZ|n7{O6RGgfn1l^?bkt(>{Ea{iEkpmB61P%r_?K+9NG;UjP zD{WIgv#`IrFZ{A&HG*{H!j05J6VVOT&hmS&?&Zqy#WFPjpX{2HvzCmvI|n-pzrLYE zst-0EJqaamp3)9@z>@vNFiLvsY&JJqw!{ZhC*hO`M<J=PRPNlGFly&0r<W~G4wPMJ z1^VAycj#gEo43#CLX&(O^q#{{X@!`$DXS&!TV`f+XD2~aXZI9kfTK~XvKBM<c2}&_ zM!ki=RaZh<eEZBM_YyunnTW*s&(1N6A03#9F{cnnk<K;DjXz?Qcsf-l&J<^&v7x5T z@qj@oFc@RF0Y5xj=2*5JE492FHpyRVz!6I#<XjD(pK>A}62Oqh-r&WkFI*seL`9Us zC;1^UR?^z~!8|=qlM#F0x3_d|sI3Jq;|-lt7LCzfBy#k@^aR>2?`W7fl~BcRLMJ-8 zDoaLsWo)ev3IPdWjwh^LWr#({e{viL0eq1g^W+FaR52L|%yo+sJfOuVq<DYqoJ!Bn zcbJ?e3X1vh;|(R+ZwPVaZZ<tYo^)h_s_FiJV~2Ie`KzBE4)7!OyGmhG(g||ja`6<t zM8Up!>P2I&D-*ebwy#)d?~f$?ETDmafNoH9Uhry^NJ}29@%UQkV8VVm=gc*#J8o~z z<GMo!apJv2G2ycD@wofd{ym1O=crHN7tGOCY5De9Fv(-|2#tLVSbGT@4YNP(bbN}g zYuBhp&J(1to@l_%O{-7#HM-m^Zw*jUC>SXZaO+OH_mY(6T*nGuAG<t?sq=~+>4~b= zHKfPZ-!)l3*?jErYCXLM1SOjxk_n44P5lqQa=xc{Q%`dHz2CFBItjxG1U=J1SZAac zH2>ypg#?2Q;;M&di>lG54UK=dX1cSu_-=bt^CqmfbEfUm^snV=I+d3C!la+6D9`Bq zTBqhL-&$SXYNOIq`XC|E;oDyFj$Sa5cABDopfm1d79O&T)&FCb=Z;k_mZ;tH52Nsj zDk}ry2CVV`QL`~#o`F0<6@gWz<Q_br!*9%Rp1J1zhwZ86M*Nlb+uuC@n&*evpCPPg zzs3nM@hWB?T|ElCx6iFLU|!3&&jb)MZ+u@8elnbj(Mi2vz8bQ}k+R(kQJY&})=*rF zxD<`mUSxCkKBHh>I)&O8{xp;Cf0(OVLlM>abmC|D$tmi605eeytMByLNlAIqhK&zx zD|(6H5Hj4{%Gn8#t~O5b25_O(zn2>?-?5OKVUe#{s@L<<sw&2LTivGUcFc93jC@j8 z{nB>l$vFNBf2K=>+pAX#d#hEQ?LF-ohVSUy-D^+O-PV5bzwcDv>39=LE#ESyuiqSv z5KO|2=BX0IRb<>mm6iRr;PljH+AzX&nk^n|w@B2Z-C5X_Va=<3ur}^jMV)s!;kr7i z5lq1i6XgF0TA3|XL?z7(#aQm{96XSC;<w*lR0PrxPw{hw=NdXs+g7C0r}&9L|G*0z zwb}d3vzr=8hFo#ctj0V@0S8&f7wHB&lciuwAX%_kBW0C@%`lE-i&4KPI{ezzM!C<A zZRj^S5Jc}DkmBZpsmVF~e@Jbxj+SjIVlK;WHDj%JXU6PO$jQ$pXzWXqXvRG%pR{;Y z$D4$mK<l`BxXSbX*=bu69HL-DTb)Nt=ckL$!VF4IKSXKzjv6&@+V5}Ixyzqp>kC{@ zD;RLaNegOtsQ2&EW;iXKqLN?Yd#s(1zPdOQf=peGyM9zPGn`0EmUWpS0>uWcslqWb zM#_#p`O9K#(!4PZj~`P*;A0xdAz<FRtSsQ08XpjN7W1vEx@zt^Pap?BNoa)HY~Dg1 z1wc4opO^}vcqZR4r++s~33>IK1|e}2yTu+W*kM1R@=3pg9%fY;rCD)Z@h4fZ4>t*? z5LIfQNEsF73K@Y)nGh{gK-Yl)z1*{AGd2QTBa@SrtBuLX;Eva6m07VM{j%muK)y9z zh&sY!%NGZfj`WGgC@d^ET}xwbylRajqS4*-m?+{t=aH=G)btT}5gpP1O<ij?pBK#y z#FNkklfwFp8eg;bl}Le|u^!tQ1Qv@`vfs^>(a?A>&Xtd(*USWuur`x_<Z74{{XvuC zwfgmCby>uB_zF<f*DuGYam^SQ1<U$s-@ljH!6`Q6NW6SOO*p4}>>%pS#>vTfF<W<~ zUDl<cB<@)Ce5~|sQ?n_p#@Xo;o1#6CvOQMXq7v8l0yvFTG&;Cqf4NaBSNkNp`{i+2 zcW+dRm{gGgIMb7}L?(_mh0zZf9}GXe2M2<+vG49oa7M8*kW{cE4YK>ZI3c9UqSs)X zDh!H^X<J_t9Y<UU*pCw6eq~%N0)|ZxIA8M3PK&Y^FTh6gF{$Oj^?Epv3nVkgbH}~) zDC)O655)kpnhR)o@$yAa{Aj0o%>m<_|4~Y2z1P5Mv(X!#WLCY`dz?$lMEIn4zzI%H z+aFcPX_3@^B7sS-r?El%vvPDb_(3#rRH-HkU+?^1=812s%U>BOlMvw$fab&qG@x~` z_<87XuH|?hmRAG9ql7xIAMf9QpbR$2O-t;eS(jzb2>m+$u5xGZsD7Zyd(`NQc@-CO z8M;y51lV3X>)SVLo4MCxEpAJBW1@kn@MeLkER!-x<we}#<7Ty>0U*~&^|L@PSg}0( zVYjX-G4R?i;DRD7OtRQ7V7A(EgU}K-t?^@lUuuZyF&UNspCQ@4kXB1jw#lqX8T)3# zu3g;@ck^qGo%XN{w&Tk?X6iLaag$L(NL53*+$5L+?DN<}U}uYM_kAMHA$hSz?`SI* z#F?f2Ftvy%CC?7C6p%g#CvXsg*3Mvl{qR=!wKLsY7dUSI#bG*cd6K*Dle>>W^x+ul zr$|-&apBlL0+iug^2gR_x0iDCuTU9dT-BgH1ZP^Rm=bAr>;MeP8fkJH!2bOPr;WWN z+)fX}x=$)V&={6*9_x?Wv#SGHQ{q`mjjtr~G;Tfv^o3fnWF88Z?MKyQKpk0z^AYum z-#mi{j$$;BV&*pX<kvPUDodP)CajE<#DOE_$}4*LXykhg&QxY3RZ+Wm%8BCNfSq`M zS;@U83gvPQxZ$cDRC@De`GP}lj*h<y3+4LILy6P$<@@JAud9OV^@M09BL7gb?0-cl zyX?<%_7yN{K15;RD)1|R1i1+d?zmq$5WNMa*g&vy79KODA5&g9b8C>ApqUap*>=B# zC*7uh#I*akH?i?Wu4V<YXZSpW0qe@UJ9wt;opsI;#h-$+f9b%FP|WxB>pipUHv}j? zUg3uH@X!T?G6kpmM-sgq7>iU^Q^6Y-4*l?x70jiV!D)+{ya^vW#1$rW%FG(#OJ?jJ zA5Im5@@Mwz)W~?D7_aW>sNJ%AKOLSQ)1P;NmXiYxQJ{sT0*D+J7p}X7XM?%Ag;oUo zb$lWkro0Vzy<{s)FcVV8*{+*Z09-q|{8|R!L@!I)Q-8OHIjbhM{L?1_6Gz9h!J_=- zh#`_-6pK2SM=XN;LHdDUyX<S^$i>Ka!J$TLHHK_4vJjqsh=$S(@8Lvq-Q+UM7!sWL zg*;y}&3N_ykmI79+g40;U5z8ofeoexyBtI5Gj@7%oallD&;6?99Klkr*}JZ}Q)biE zVaL~nAEs-M2nVRj{d`=)c2^YA0XDcl-K3Pfm-DhXW^YfXx<=5N546dmjcN06R@3&# z2u;#&&EH0vnXKz(Fm2&#l_Pr(WT2zC{nekm-o&~|q2OM4T$nQ7P4;STw*S=uwebOb z-85XDO0etOyj0=n1qrKTIU;y?GXFoR^po>v&pOg_ih~vB=h?`x-mUj}<*_Lqjva)p z12qE`Gys|L6;gln%7%TPd~@GmY<xqN?@5{Ac8?JRo0`WfKc9OurAOzEJjnn~L<g+H zh)%ytq;|?4?T?<e(Hu;8MJ=-6jAm2MiyYLx%M*sjdFIJV2Yo8eDxEvrVVnO==!$tR z=L|<`E^$F7VQ^e(NrF3#<e4IddIVvl9~hlpNA%t_`nt?DruIJw|FyHZy}O+!*s*y2 zK6Arsju*=6Vx~`>Qr&dS&PByt3XLbJP#6yMhkyBClJsX!IcIk}bDi$&f6e@fAoA~V zXzF(yN}z-Dw7i<NtcRsJx=8hz)?{;ZP$ngi|7&nKfL7DkN87xOk68rJJSV50fS$7Q z8QILq_uH8osr1Z?S*j*wo?2_v7>CZWBl-ve2mawqkisw4`3g{HiH0!ak?3)|Y#gip zn4zj_<Oou8Zf^`gt3$STG48T0#?{22<`6U(1Zmf=OY`G0gc)kRrt!82-MR}B6dzFr z|8}`2P4Q_hqqR;*U+>*}_kK%WIRoqv3@esajl)T(DlzHVFh;(0PC!P43@F)iGkfY^ zkH)<_R~HrvIgYku({;6apPauB#%P+;H@M!Swd<%)c57Sz6{V(Zuv?7VPY>|~Lp3}~ zQ@L#FhGFb{97l>~dS!rWzue@(l;YNAh3I{;maAL$2K8PjyUeqP>NW2e-R9m{-v8&2 zG&ooU<whEK#!_?ev5|(UW5c5h%E0JjdMqfLkvB;QkSdR&MT%T^)xOAm2>ZztJ1kTi zioi;O3C3V}_xe%6&H!ELy$tr#Jc-J~jgi;f0HN|G`3nyAu{K7&Cw+=AfxVta!BN0T zad}t2W!a*r+?dk)@#NJsI?&n{7w-W25eOt9*Ic1T@o~*-EpfYr&I6B2J0(lPE{>+q zEOl8vezr88SkOPTAOaV~FD`XtG8Mq6US`9uT~%f>Hy92&rhxWBpuLg)be<8Bd!>I# zXYJfV;XA%}fix#B;w5<u!ga3`x}a)a4VROg3gJXF{EvN-gq(3+1^JZ;T_Nh~W_yEZ zqXLgMOC>}LclOVMuIIj9pQG#OB-(ZAI@=Q|T+Zn<LcKLN-v+@33p4<M0?$}dqs|81 zqAaeVNec9Q$E)xM$WQ*uEJIgIwnjosBJDvXg67Mg!tBK4A5{@Q@aU(2S<n3mC{=)a zXExX=2s;QLLB{RI%d&pFxmq99Kt>sJnllmp3F!ALIA*XNZB6{x*16y`Z)`m6<hSKu zghx2;0oDxi7=PYCElLpVm|DjoORftD7-UtUaKQ#H!Z=Y#N^VS*-b(Fj{+}Yt{8;3m zvf6;da|$of1~4SS3$Emj7Q7;bq*}zlMV9z^Kr6yOryyG097<Fw0_UUi9`X^y1Em>D z5iG>)VblL%b%C^r;R{A|ESYLv6(DLztOf_U(OrD^Bqe${t)y9QiQaHKp!Iyr=b}Sk z-6Mh%^jz`(FD8`-5SWSl=()DG4@f4@sp4eMLuac2{R8bV{TjXTm)r{G;(L4Fh&73y ztlq|%yU%Rcz>p2Z!#hC%et3o`tOgS_l=tD>p1U-kD<?H4$9E-KN1}8aWJ%K5KmSjh z#)0o%1lR#*zcY%e<<0(GmNK-WZ&%U_I&M|{*_LXMhYuf~c3xNaeo3yeuA1y}w-ut~ zliHbE9{`fhUYKw<kbOS@2G6GswNY73M6byEm8*p*GKYMJ06+W}Els;TNF-5@C~nQD z+h7pk+*VA0N-K1yl$U?9jtJ0ak69Vy{FqmB-{;R-fLbH~#hZGmmodRlK=mT8EgyNt zz|-diHGS_#z_?Vns`(U}KugM(#X@Sz3wrmlnZAU+3W@<bVw2Rd$#$L*xIGnXz)8r9 zd%XIMj(Q?2BZ_;si2$T0rYdJGZ3QmpcTuocTei%nS!rrfA(DRB8wjeh%UT*}Xr&R6 ziQ}(zfkLW3R~-K6(v`MC;}qocF%KNV8s6;l=5J=*i+jNQaJGS34&e(;U36?L+0~Bj zb>7?goNsrlwKK6l0$u9^`Ecc(5^fzF${1K!SQwZfxibGN6#rg#qF>mI6ZEw>EuOb1 zs?d2=O}55FwuTG!m})*7G;cb!^ni_;A=V4m`<I0=)VZIsVWRk82g9_Kz^obe8(3ZF zOdBIdCuo~ahYttPbo^(9rpr>7&XeZ_!pZug5Q$=(mRHC6-QStY<K^Y4p21`3S<9hW zaemI@3a}eo=)h`Y=QxrA&h1x9m(2iD!W2wc76XK1y(h0Xa5GCc$3TPB`u<aL&@iBN z(Iz4${Mp*mp>*8vYy~y?h02klc}9%AeYCLGQo~z<0A@`iQJPzJcKM&hxGrZ_8iU5} zQYNy7Qvsl;@$k{bhHAPXPhxv!YXGH+s_O1(TXsk1gL)&bK5){l@?<abyMd+%+W0Ls zc`#>b5|Q5d0aEi;+Q(aZeG?@YTilk4N}F_ak6`F!3@ebpb`_J~CQx3<^`!?(f$e&f zz|KinjKC%pJb%nNDE^Akh(!Z^LEr|uPr`4QmZB(?i@mE~-)jTMIi?%zkY@a<FK^Qg zBo+LO0T88VcW^M3fiju?{NHsW%XbgeZ~UR<k@8A=qKv*WS!Gu6A$^>J-SOeX7c>$J z{wd%%a`&i5q?})39C?bIVFs(gmwew~NN;6i2{(HvnK0nLX@BP46$L=uDs``*@@%$_ zRe>kjX(a=K1UB7f8g>3Z-rG1IY>BInqrtDmkK9eB&&7fgp+0_*+^#@!A!YT4uh!$I z*`E<UI0og(z(EkME2kd}Du>>qZXR!oD)&|V7dSqBS%~I28)&$DaPZ>KlhRU`ljFP* zvffnt^T5jyZjO+k$vuDnVVZV;{JX6&ZLrZ7*y#cYivsI*^Cj?~0azOpnH1GDr9I9T z(~U_j2;U4053j;2v&UCUWnGP>Y10Oe+YH&=-UFO@+MNfCj9`!FE5WK#%hutwHp*jx z_VSK0?lJdK@7|FD=UW*{@*jBIp$Y=~jJFiwV%S#zI%eu6h@OfB7Vv#8UdG^DovOWm zzjwOsl4MQK899Bqod245C~&LBklc)#1iw)mNAQOl`A0&5@Hk(8-VPO8tLb4Q<1_+X z3bE~4Brqz&Rhu=Gnm!-CE#LQQ+bKF90PdQ&6#aMo&F@c=9y^}gM`^`rY8zHVAOx3c z;{@;wu|9e?f{n~BD-c#p&#OTS`Y8QOwvDPZR!pe00TH4BJ{oN7A|pS4H|%@@0WCv; z#sn*7U%ayjadbH!WZFO6{hE@a{Kx0qVv0Xz{g81!q!x)QC<V_&nsI(}Q9DlgQ`8U> zBNJmwr<8a!oFQo$ud5A*exnq$_zqONehfcf|M}U~r)z=pcAYQeDKfN`KY7!Fym4Xe zsi0jvfmGPc+Y+|due|~0^kc+yz48wsYpPb5`dZxF(6@jDYIvI4*$CFhF4jYYS=W#A z2L<i<5&+He8V~7QOs~ws$Qbyh_kN<DgEL3V`fU^oir&eJwYq@7%{^OdU`U$P^>7ba z#hLIb3yVCba>|-2E^w;g1&h(jw|1*>ri>dhwuc3E%52cfCY^+jg^y@&DZ!ti-_W3P zk@4kA2)<=ZCj7=22nh*Y9QZ|%YXL#6y+@j1H=a+-LHL3P{3}OffXgZUVEaf~xLIl! zk}iH@mM)+ebC8|ZdAOyCz$P}5Zee(7J$*X;>oW<b4!2Iwp98jf!wh?C+^1!(p&;B4 zoH%&Fjoc!Op%)X(#MVtxb8kx?{OQFocWv4a4Skj##AW#NgC&-|%r?MWM#su9F{1`! z7Q6u2-Otfg)YODpH%eflB&dR<1mu@;Sd3BKiptHQxp8bHn86fo*@#f?kOcmy2WU+7 z^<G(~v{0Lx6$YaUv$&6{ib~pYNnX3iC8nu=&Un+rZHsEEukVN9hl4vt>nF2DFBGWS zUhNx-T)<iHU%y>L=!s{@pjW8=*A)?~s;G1<*}X*|;Z^;&%Qq(<R#a6Li`o)2o&HvY z2tV>o`t&O|i*k<R<?l~Ud{Q<MV}5GCL|QvqN35X0f)YlN>#0RQ#8}S3E#u}joahpQ z>V5$UR}5!Lsq?Nz6ms1c-?Oo)SUlRrq!c{Js{X}zSt%*8Fg6g0UP1S?GYZ1$Xap4! zss%QHF$uRZ#g!KV<l>m+I<ELrG2^f4uOrp39#dA?Eqvv^n!Y1Pm)Bey2$SwiVI;8W z(_$tDw+G<^kHtqss151%z0|&TP)Phz*~9PDPvSVZ_F?|1OpWRd?&J>sr;SycQC<+; zVoPK@*Nz@9Pb_$TG?OQ_g*X$bTP@aDj<+#c7!|wrFwt!>%C5=H-o1iAD%|II$`L1G zw7LAjJPD`{1pV_KU7XyOrx5aLY4XuG3<zgsp7p+zd?NYXdG?{b#gwFz2CwK{(Fyx) zbHIECS899ROqe^)v8@G8d7eItaJ(Q6D>7N%dUEc#NSx1lB;5MlbSR4F{rf;_QiaBm zS=YTIihia3)rp$N$AV7OREbm~P2vSyITIplyI>!YT_0e+xmV-*^tF3T*v84<zkw3k zr!Nn+Th=>3xyCJt1qzF(xZ_tfQdRPsCx_P;WIzR9jW`ToN)WK|Chff@i5OM3Jl#B= z?r9K2by`^sSgSPy<q%Wn>n`tB#=Mq2t_4Ep@Taco%FwZ~?$VdKL|h{H`j*VDP78(P zcSlj`S}doQ2^lP{_Zk**T<l)^NO_1zYVb}P8p6o&hI53lH~8v<iQQ$5o$gsy?-9QU z{c6?b<A6(o-(GipU}<kQ)>BZ}L>{71*Kny`W5u~>d<W>4vo;kYWri$a7YA$!&w(-! zzZLT#=!KSgeWH0B8<|`%g>4WbbWf1f)TF(;$9NBietvZ^aMU2(@Lhnjxqk?y|EPh5 zDtlr~;?rL-X7p3MfXa)BJco!&;TT@`xOK1PTe|83D;SU@YxG_9M?^M7Hq2|B+L>X^ zfq^G4-Ow@7)5M46-vzFUIaXN-)CjB!e&JsIW)izG-Mb~#wj#eVT|(^HbdED`czEx- zxG%Wx;Y9KviAgcjs=A5&4ZvW<0G3&825TKK=!IVeRdlfQcB_2Q^c>J=qSn1Iocs>* zKcF9v=UmyFkG6a+(X=AuXC(_EBqmZ?yI+$z{4QB2$Gx<&j){uM?C2~k`(ZY7d5D)d z>dw$vKI|FH<oK810d>DFSTGFYXhol^nx?@Z-1h6oy%yn@yyeI9HXaL`AMr~dBR)#% zZuM1lb&cauqzQT5$G-Nx>J2TTUX5m@x<q+(BN9k{x9et#PH*H&DzEd=dV@EPFvDZP zpar*p=yy?T!vJ$pK+4l?zc7Q)ucTCfirjB0QjOT(SaUS-HcGH1Q>%K%-W=V6pzCAB zGmzY+rC0h#NXc`apK=N7?tS0uIU*kY{N;nl=Uz>DW666U5#I=YB%dVU)=&%Da27f` z;!=<y*C*FZKmVX<rI^1gbI?ZtnCKLe;pu`d^+28>{{_;e9Xkl0h?VTt=rXtWJ36JC z!sEnMBuQ0_jmx!q;^s=O4>~d+Bejkeon4_J^a8G~yE}WWEIiTm1=6la-WEUhmG@6| zsR0!aNOXt!QL`+REbBC39jp!SSD(|9Meta21PN(0gDW?g(<TUf23rnzOl7c}@$BUC zc3m4#S)|p;TjfX{8fu~Z5<j_XSv;IhSx!)I;r9u}7r=f7ISZ<#ge{S=rU{R8Tja-` z%7A2};TjaPY^svPGO!5xeX=}K)R=qD`Vx;NJH(#N4K37c+Tgg5Q?33gue9ONs)kW7 z+vYS<1@ywazFNYmKFJ{7J=lB0xh(qDp*NQn5?$n)3c@(CoEm*>a!RppDnFOfiw_xk z#jW#db2BTUEFA(w62s-mj%On;yKA{sXlJU<5YHVpGg5NmGno}9PInUQbT`(A$kdBt z&B;r_hJhhNy*~uQrp2p5H_3qi<V=)Qb506xA^m>KbI77;c&#aSfd1F`QAG)sO_8m} zP6Hx9qJmNL;5eJxg-C`7(WlL_?)!Ch->TRg`r=z0ot;_T*G?+0{OPb4+r5myqg^GL z^m=eCZ+el4R54qwZNZf{CXrNLOB^W;PyPal{$FEn9T!y>wv7%JN{EV}fPf;>ozkdu zDk<Ha(hM-5NJytccc*j=3?d>W-9wCYH$%r+_`LC*?|t6iIr}%RKX7L6wbxqry081X zV|9Z$+^r4;=Q}YxE<S%xJYQxvB~$+kzfFX?NOgVBZ}CvXizAj&M1<U@sp-J=U~^2e z=?IBAkrIgQDd|QWjmwI(S?*<Do%fr+!+6C!5L_I<Y<9}D9=vp)r`|d{M&pqQ6zL)y zt;lMps;Y<#u@4seQ-x(#zYI2<{t^IAh76n2Mft6GD3xDsN}(6?RyQ;p!#Kiey8Ae} zR{hnxSNFZWH@5JBGscns0CLJbm0HlrpnKfPc!Ygx`>2<480!Imv>%M<es@s0J3b)2 z<1&|*UqI!)2Q~B${Y5d5)4FAcc6{Jl``In(YteIF#(?d1`?PALPlw9snirfxhc7w- zrpsZ#L6%=qK*h=I8>S=h;NIlE*F%fkfxNuDrv}>Bz?xN<;=ez|dD7s59_GpO>amas zA`hvvVUoy<4C^NkB!AeIVxz|^5dWA5`hH3^Hr>d-Y<}>C3{A-c51-`t(q3!!Q-J)M z8NEHku_XW^`5Bs7{=2z<LysY{7OQTG1uR9W(#W};>gx4pijNY|=(f?CO}pS$IgO5E z%=I77TjJR^)UKpgL5+po%dmDMW#SvklWfuyoLmlb5yAUaF&K&>i_=(|MBM@=F{tOU zIwf%b-b2xm_#)G$5l1VcqvHwtheRvJMECDo%y4mAKL?M@NSEjRmQcVDwsBG*zBT8% zcZ$Ze>ojZ!Jc>I$kvqF~#*$z9ss(#zKUi^Z(yA#3xbdB3M|n^qrZw-hRYE45c_LXl zSp}PB_^R;NFL=Q5&mZcvox5;$aaR4i+}xl_%F4<$CFxnTB8Tfj(#L$qW9u%-d4JK8 zX6j_6a{c%T&D*F{S>2NrVuZK4CG4_@Gd}ByV)KH-%))|SKrXg~@*FRZq)Xt>!LDfe zLf#n{1mB0F8<KY^EF8%0zl(f-UPR3-{bR++VRelDU}H=YT?>O$+EDgH`bsv*NA13> zJnVD#vevAMd>~10l5GOMoMER;nVbdsz?|gI$Nd!3cVSwigS2D@Gr40#Xcshgwv~#W zp9Td2?o&*!yL5Z?IP;C5*I&?O=mdvgViV9!;DAY1;j>Q-NrJykIg{&MX+F#@|ME6q zBX!?yBjDJYaxc<o@K@+3|3W^59bLB*jtiVpZ$#YVZc?)75BBmHR7}*)H46j=TH<~O zkbx|<Q6PMStcLL3yrP`-;oiwIXC8s~<y2zK*RTBuZ#xNv$K$hTf*AwSTl4_6@(#xz zQAoZL#vUoHW$5_1b!kU@i1t`gJy-Ioswp$uf@L6FevP)}j&Eo>aA4g_bYnzbjMmg@ z$(E|Kd5n1MFHa=belAo#J_88r2Vt+Udw<6Ori3*jrCNnDff<;})Hl=PEei_if6L^u z2?UpCTw~~x$uFS8{@&6>rbww*bn$WQHR|3y;*aCj@A|~Dr0Y3$g#64D7;obrm9S|v z2{$GVBkMLaKQ(aXmU?jX4<zwJqyF^W^Rz)e;j{ld*U=Vt@_hp*M2b;$(>?;WPqsUt zFyp|#R(xtZ`#GUN#<Jj_<5!2+L4Z#4{dZzwhu^yvbDsiF%$G&Fo?aUVFBb)tR90;n z1zQSoR1FB7Yi*lA*q)=D$;QtyuuevX+c<dov!BbHLM(e{O&CAK#O%(r?SbRZL!{-> z^irootFfg}-@cFwH06MrP=&*Af!t{cV=*lm85xaoPa;~yY{Rnmk2&0)w%lstt`Ggy zaL~pZNXg~%Tr&KfhK9Jxsf!%aS$j$)v-Eri-yc8B2>tQR(2&aBAc^9Lj>${d*~1l* z_(_k^q56xjCGYWV<DAxNB}yf@s<$8-<8_`$`#W{+#~8!YDb>`Rl!^C62`!Ou$HC;K zM+KY_$&oGC-k0~x<ndt_7n7$y8%G-sd;+zF0fmN2_e$dXiuZ``Uh{%eR5J8a(je`f zt%1<?%`*ABt`C8gbWJ4|&RyRFf)jRf_g2unUq2i8^1)^wQvQx_>pS}BZ1eh)Z`{St zmZOuCZ&tm!i=Nd!dKcEWYUH2vrq4fL!hdC1<mKYRf;e1Xq8+4c8Ep&r^oc5j<~i8Z z!-bO-PhIYxtuArW(KDMIU_0Kvi(~CXCeN4r^%r5d;O*}LslSZ|7mGtkKUBXQ?@-Pc z#EkhaO?@MqKwz)&cuJd?yhuq&0e;oHPvjPnh+J-xre6m_^|EHd7fuu(uAg!`Z9i4W zPFg$<7dF|hDYIFvQ`58ZEwemeFQr4Q^z7jdlK?O)IT=`MRV~7Lfet>|zF+r);YlmC z-D#AA<ctYs4r~pMTqKgVcl&qlxVfFu)lw-*R`4Ysww5>aJFl2=&@nR78jR@!#HqvY zn&lXfx8>Eb`DJ)DxLIFc{}TEBH}UT7F4;y!*#S*-<m##=M@9KnG{lXuveWVQ&Youi zwD3D~5+G=0r)J_yINA4)z8&y=C^iA<=YDhGx#lq57Cj(8%9%b%_r*5gNMujq#HaIV zno^k9W<yGsBrhx-btnU;2uhp>#+{b$r6>~pO5Ehd8*G9f`fPn=Z#?RqHphyVc!&!Z zX(JZ+cf{xYl{DXfzX&a46^MKZ&wLtwzNW6ErUK%nrO0)_CIGL<*&$GpyyJpsoPM<S zeT!%hRpBb4RkM+iRe8g=Ik06Wt+H|lK%J_sN%|nIUo_>G7^uVE-U`K(-xtDPN^jqb zg${P;O`n<RP%7j`Z};`dLL1fA#($Xb6vi3`WKKg9hf!`+4<~%Ru<!PlD%u@vh)AVQ z3nfmfDW7iI$NVv?7!EEzPs%Wt7vg2Al=m&G?~|YH=34}Wsk~oioE;CXPOPT1D$&9d zRC&tSPpvXHclt44&dwT+S*yv&%C0gq($l|qn#Pmf-qmZi5;QEhv+euY-ygEw#yfrE z&BjYr-?9t8G~6o6KGv^T=Z5smjB|T_vKmbvIL}WHeob^mr(PU&?3EC6Rfgiy)6u2b zxY<xe>Aek1-SB8y=KU7BwZD45Wj9v(A(6n*V&8q({t-dq<kzODk!576f^6>C%=ZnK z(PBmh2AY>Imp}E|FsVh;gbYeEQt=W236!k1Ll%gu7czA6?GHntVJ|(xldtGu(?D~w zuvWvFKj|&|AuAHPFyVl33L|^%joOQ#AO<F4f={0;&2GsEd&YeFlu^H=<>l4h%b15q zP0v37XUwXos5~I)CuUVt1o6Va*!b+{|KN|E*>D1eI_!ymtKl<R-td9~2CC<%l$uc7 z<K03Vr{z8hph5gYwU(zibH;bJF+1r-0d&HbhIg>AE<WGD<{d}odxe8e<>u0ARd41Y z%x1@SqMbX$lzTDT-?$zZj)-A!Y?(@XJc9#r6`ygeo*mB2_C~QimX}N6QTiCg$inO{ zlHIEA{aDGuK-etoNt#YbkZjP^t8zZZn{+))_AGOw*FvJghE228!{Z#ILfH>DNPw z8+@CZL}m<ZZreB>;2s3dywRvO<i>@U=>Mqg$6H6&^dH2=sxs+T2c3Po_M3*2oWIgc z<p&#(z(cebZI^oS!lh{=sI`XwthqUaB(`svC*i?F1@`6H;b}~)dLL&F*>it<WM16< z%9O7=E-@!e+puDGbPP!rDzSOmOwRPaHY5u_ak&pE)Cm2hf8nwik(09Kf*=ctZO3r@ zbqR5w#%gOV+?CuYk{@l4&kVS;HYVSaEM|MK)yANyj#kGNMq22uS$TW<N>VhG>i!sv z@_cDVF_nqXW_wouz+bL$s~)LFB!b`?MZ6!+e!{C0U%S)e`*n6~(`G*FNmI^*^<X+< z@+rR3wz#cfLbtSLNX*b6P~2BI#bN&jtwKtM6}$&0q>}$?^ebcVARHio|H>U>mvX^r z*P9^eu91C>T3uV-ncLkWx8&HxQCSfj@KSvJsRQAaQ|<k`GGFh;y;r_#DQ^4C#5GNP zb?%<K<JT-LEw?L<Ka7$qvsebHM}7rtKIf?&ZXnr6;ycbczJ#P70u_sqY-&yOoxq@p zh3T5CIX3PrP8cuE>$5%H-ri1=mtrexgtEP~6`jf=MpyBT?$c7*?otSXP(*OXadRZO z(>*|Tw}<}PG@FBqoPNk$GZQ|Q3r3+oo}cPQKa-~#mC=*|`5Go6q?F<P=4o2tcb!7N zDCy64(h`nqJhJB8IOqTqM;MU#d8pD!twdK+X4PV|&dO3JQ6NW-s#-m~zO6gHIG;{H ztK&1Up8@GS@LvI6$vrRWe(HUUWGvzHn-95lT_W>7Vk&&vMdZ`9>&K@}(=X2DZr!)B zDiw|k`BndgHKDNKz4*GAt!cg4Yq5ETvWeoDhwx#o#iMDXNXGPxqWz52K_0=F?+|aG zV~|BQaPQKKsK7~W;AXl}yEJlt)cMr=Y}65-MZNLU2r~x*UT^?7mNmQ&;muX2aq|hv zHOU|oo9-oV-)YT@qmDU6eGXkuEWX5U8Fz8<sX0Gc^>U|HQpoGqpWJ67v026flzH`o zf%rHIzZY_DDXn}`px#$c5_*f`c!z5d+4Kp&&{e<UbihoX_ojp<UO?szyR8ams$Zcb z(&3v`OL-l6&A5MAKaE8uw3|*1`hDZ4k8hwWe=1*tu=V`<rWUGCS8;|9W5|xqCmvYq z@l%=&x5^77MaGN6UOgkvKn*kR?d;YsYwXLJ1}nV2B2Wh&Bn$ke=X?%VMC(YnNC=Z> zKHu2`$Z0k6X%$-`u?;fspyn)~psH(OZ+9nq|60fAt_`rJ-YIGRS{bw`dLJ#;8)qx8 zaE^L1jZ`Ct{JmvVzV63BuGUpbDGLKww9TS-+2><Drslyx-#=+j{eR(832*Lhp?Aux z{5k%9KO<lF)ml0uzJm}zkP*(S?=#m@k9X!Ct!MJtkQ&+%mi%4xQ>s+OKC485Qp(qU zP_gU&uvUPdzxZMPg94?7EefdIz)Ir+^`}(w?1|ksBy~><W1npZ=sHCTT=_MqM#t(f zI6n2zn^aWjFM*pMoAql=CpaI6REBP*f4`a%Mw5|s@2JS+@pT^WlS6cSM^|If#r!G* zaBHw$?mZA`@Om;x8TV`Tg&x=V1zhCh83CG|Lh{|asdDDE(cK@M;d;|#m<j03H8tC% z(o*lA12@T3O4Dw7I4^2tWgBaMo09GAq2d%eEU<mEg<iY7XjE&khJALeb4kUW(Lu~) z$0v!|LhE_lZ-zb*e2D1YDxZ+*+G!TKlzI0~*xPH1vnMNO7jbz;w2{Hz-{@+c*~=dP zo9SfTXw%#C#KcYygCnN7b$SR5T&D<r&WkPiYFc$FjGHYEfI$9^E3-28;Y8yok;QXn z--;Z6(B<t(M(^*vo8fvOfjdmjxCLb*4<m_pYnx)aSbt74^?~=|<41Ud*D&Kfi$>7l zR%4z)n{BCs61`W>$HR4{owEz_PLuX>G|>uNV1ReLB%<%Y2gm=^Gb5@aBa=HSwQ`4; zwd&aKOxpemf6`JkemURs<?@wVA~+9l;>YH7J>`v}U!xP9F<LYa?vv`4;(zZL%=fIe z##$hX{ps|@^sJpdjx;VZv$80Jh-@Sx3Pu_{{#qM-@$-;8@4Cy2F!$%Y1dGNdZ#6Vt z1@!2weie6K?S!w5aE<GUArA0JIihZ+B72_?dgnmU^J+|!KYL2=zE#syu$OwOyh-wA zX}N+3=w_LjDc&VzHBIT%k5h5bt+9Gm3lL^Q>{*^L7oc3`_Wm)uDdhRD{Awx3$=_Rg z!SF{y{vq>yUok4GF?!?3Mz25;{s)S7M8B^vcBtoV4<!xMb}64wCZgiQ1TjM)!_NLT zhzNFEHs^4auW!^vgsjUQQ1E|`J`@TGuOaYj6^{$SN2ly|_rDSJ9KG)IQSni?QCG<Z zH6E!_Y3RhpVnsG?p!LL8vtQ?d1G`GXCx#TSE<QS~^hslHIG}H&=;E`mULK@=JLJ3M z^78V`r(<l?PjwYKVrxpcxP8gKfs~zQ)v>5$ue?Jro69QrxpX*jBk^$OLWFo1I5U43 z=AR#(Lzj33_s$5guG=-S!?TDc9`jnz0a!n7*OU8Z>9($?7k}8S(#1}0-RIA@9uQ7w zd~EbtJHKe0_FTu>rx1#KKEX5F-c?8CrFq?Vg+k<7FdVMHc0WIp>|5`t;rLN0vrA&~ zHc$E!;SUdCZ+bvZ^iznm*eewu<y>X<L}szi!>L9SAi_#VSzgWdt^%CUTSS1Tu((+` z057nmt&5D3mDL}g^arox-Q-ak0oH(|Gr?bY<bLDUzicZw^AttylFN@^As)0%c2zVl zOzTcp>B;nvFuc`N3xbDl`8A39SDCoYzFhPY)WSk`VE#w+S+5!c1-AhkQq9h@SQi9| z#+23!!)_U8Ekwkpv}IX*0igWar*%BasnkZqKGN=Pqjz)5V04^8=9l&9y>iR9kMSg^ z>JPHow%KK<#pchpK!GtY<g1S{i@d@JnO<A=CxTJHaMxWI*~M=}v?h(xaMj{x282De z=#tpQ#a%{~an?{%&klY!F=%p(!b5cXXJ*Ye@QB~VRxrc2^V(OK;sEBIxqaT<vDlZ3 zSg-I|2=3swfgTJIwyjJTx`a4)ewnKbww%`9be-IL1KXLu{`vFD%c<Z0GkmRMJe~Fe z%S`0@wQJ{e3{H@@5qfd}UU?stdZ&7!M~CiBD=aBHE}v_4ft(nY90CJ!L?qN)q%NP< zzAmX7TGDS$RjVUORgdW_k_Q?nqpry2N4qdIs~;m&?_|^sU;7aA@}=kO?eNLr?R9=H zkNq6G@aY)Um<qRU2cC_w&ZGO8y;kblvv~;bjP`dS-Ig|NGu!+7^ZBQgjX92iXr}da z1N=)uo<tHJOT0Z{cX+DEC5PF7(pWwHmnGd3FP%j1#vN=BvEp_z%9?U`md0cAm>WhV zyVU!FOHlhG^3H-O(RM4XRou?x`De+`kZqCPCclY|5-a8Rf6{bR-s@a5IIJ0dK{73z za(1%NwvTLzVeN-rju`FiW;iyz62ojt8kmx{@@d2n6Tf|@7F1DR&tpoAt1FB=x^=^l z;7%(AY5FH8Du)?Liie+0Jl9<WJ;|EzHs?0psE7#=7i%c^F#G(u1sm?^=z76ChsOfP zRU{w~{7o#E<KKj5J=N%29)#1$*g4+dNG6nlNChi_`i8fNcT}}C2s<LJnUGh2QLJbr z7+pBHpJ?usPmt+c{)z6@->YIj|0r;ThU?8EB~uD`3FUNc?@k#)c}~vqT`wb6R|{mP zMTQy8rvSN8=B5b(b|K$EYE*j{bHS)-OXo(+R~4}9^`K0&RtVKkN&2dF$gcTbf6woI z9V7*UTu*>GrG@g1hP6je0bUqZ>)aI0h3)JsBz;el9(?S1DujnTr?@4bAhGrHj?*fc z<Xf?r2Y2pUOfLn!wnu(j-ptw@A6`Du?Pq7wZVX{>k>IIC7UU!lt&P5YtK!pPOvO+x z)PjB2;+u^eeDn9kx-V)xiC>a4%lpjT2Ji7vb5?%Qg#)?w>7P{K5V>*l28YA8J$X^r zLzL*Q8ms&M&P*<A!{i@+c$c_~1pgO8+d{2_rc1XbU9!vXqSKd#RoA?tEgum{T-H9m zIO;^)xN%I_UxIINzZpcDlJq6>!VW3^J3+c?%ft7hr0HaHMxE91l-+bj4oX_;{XIfk z#kcy2r*je0O(0<`x!G$I<C5>_*+tWdi%S$pb{s4r3Q6{_4Ef9?WMkpmrT0(b+iQ^S z)WW3uPpsYa9x{(d(y^xhfw-80Wn`PYZOI;>%S>|^2}1+dCsGNukUuq4v-Kx^<S2-# z3{R^Hywn=60k^b;g;THCb3rdpi%~ltTh9h861-a|OnA%SXpQQ|p(_SQ_q?l@Bw_4t z=<n)kLOljO%oumP8l9DE8d+&bCaTl;$w!oo6)Bud(o_#;CPOavA?Nr>{(6^x4jba* z<jnnf76;t?A!MIBT3!dd@-4e{tGeDx{XZVh^?T)-=e1AEf?OX5JzSM_-=_Igt{Rbu zilxHbPL8TO&;R-2JC1%F_oJge_jdar&YlSOc{bJT7dSGEvny8F@=m<#Gd3X9@a^Aw zg0FTS`_;6vHZU)Ui$63od8-+rC@=nkdtsrwgpNTS-f#Y6eIs<di_l7+4VG@;c6?lQ zGCDj7y_sBJ*`jaHt68cKq9@-A{x&vSc&GYK{xpms2b|gkyyzwLb_nt<8l2=W+7?%^ znfZe0j&J|Zd5fwli;|yC4aM#<dcaip0OC`maMPeS;W<@9@kcO@vc1CVPFurw3cDpc ze&X9x1O`t22Jl{No|NpLEXpenz1X}bNO|x)+2kna++BH0{v|b)Rd}RA{aH_A!8gl( z(`xInH_p!Zz-PAUVa>52;Qv3enA)L!w|j@*ziM%2tci4(#RKV)nxift#*h6m{v_=1 zJ&0cW3q%K^fZUm0+F*TXfpS`!3SKq=mN&crc_?y`!aVWYm}6W@>a~yPYpQY@A5l?! zm77FF=U-H{EsLM3ZWXKbuQC&C*z`}MCI&Q%HEXMF>FDV4tjixpsE$*MLMkG?(9s$& z@3)e4cdUFO8dAKwllYcSg*@*@{}w=R#czH5ZGCRjgDpepIvX?x^3G8W25^d0%=7W} zR}75BQRDmPyCP@`!3+|`v$s)PhrPI!s}=I3A|K}OY<SkBr4JM5b+5UucB?;Hi60+~ z;h*JT)@snj$Ct;!+lNnV^p9$BVCE$4=3Zk#P&b1|utWtqyI54bbQ((<Olw0Ve4~`0 z1$GXeg{KK25>;F^%Tl&wAP?}z4|0bm6}nQuQjeFD0H18LtP^6YGgljCb92|RAx3ru zfock!G<)>sn{v(TUV6{)K#43v<7*JqJ5|-<*RRPrNSpKqpEFTq4SONl#4Uf!muZ&U z7s~{9#&WQ|ini_Do{F=nnD9JB`4%JnZeLDy5Oc|qjq8yik7S+?U|UYWJ-A{{L6NbI zz{oogbIH2_@>Ro_Hk-oZnx!^UVN0Kjy?6v>-;P4X$f)8g>-e%qaYN;9Z5x_d%&lmP zm@CNv?R--Dh;o3lBDZ6u@|c*b!Sk&i(L7NhTayZUoj>nci*l_L1oGns#7V4d=Uw?x ze5KVX3>|RZC_qqMZ`U8G<dqo9S~HR9BPw!ys$tCIC6ZAF9VZ?h87eHPe%A%}Y`Qm7 zrcjlZK5jVTGXy(nvkD&0RIf}jXx65Nh$0LU2>Y;{hPFCV9i|>j4<J%Y7O0YGGW*j` zDH4i~?O-|FT{Z2S-z@bll?qRJxVf!nCc0GBl%7S8XMN71Aa(tXKjJ$W#cg(gR(v10 z{XVxM&0w|t0##`nVB6$+?a^&WU&;H*yAXp}rsUq%TV_H$l7ldpwmZ##tW{!E*Zj=% z28-aZ0w0L?kC%|NDYouA5PtU3(L!!U9;@wd^9m%?5Xk2{kniq&lS3J~loyS6*E>eI zd2^KSfxa6h5c=@2cdyL%EOF5$3Fq)(eQ#}PzX1Uk7_a|qeeQ-i(|{tnpxSTaK@+Rx z#3H5~^~t#_CZE*%T;68$^2pb<urf=^aj^$JH4{N?^S)6jmB~}G>0)jTZj2vSNB~)0 zeH;(5SG%|v70E9s(DpuQI>anTHw}qQ9|d!`ZV0Zh(?n|oqrDSnu^^&xkUrHq9Tsq# z*EQ45O`pi0t-2mVLZ5iVjudG_2j*xd9S~nx&!u7)79QTe>ZwmjUREBc&>XrN-`7^d zjC9poFf~ivuZ^pvkU<z0343nI2i=B146cc)5A}^bmBzut+qJ2<r7`?o&$ZLYE)iO) z#(Jt^(%Sm)!9Ta7VSBo5o@btSXj52Nsw^6TiD{FNa2{PQ{I%v=v-A!ur`v>(`h-B+ z{@uMp^`3NJq|JE1T?Q86i^{Mjlg=sBH>e*D?j9Y@-?7*E(qTQ4$H=+gEa0fBu9bNm zEa-)LRnkQE=9*qB@3Z2?h9l3ywg7R7)lMrK^Nvr)4+;P2hv}*!MdXy_=sv=>|5EU< z2<C?7!XP|-wlL)b4_>^`8(G}pMZ;<|H52H;G{f0^N7pb)w;;vw|G5-}GUo&-!YRD% z3v}JwQ<#Wx{cb?gq783aLpFNMxmL4Br!($BAWjG9<Cv@I21E4kEqGmU{ckOpF>w9Q zwr?e#9t)CGSF8nB`h1s~+xeO}#3$GX$T_&cqWstPsod4y|GVv(T&Fc-WEBX1{kH=? zoSd)$H^0yP^fw@XK3%=Z@(us6Cn$s7?zx?I^LY?kebtd9-mXW?gCm%Up||L?Z-H!6 z_HFp9$zxhk`)9o=>L*9+s45#c=^EKXG_7vUv`69An-lPuwUU5MC2P;3udKQy@>V?g z*>&R8`iVJbZb7rq)~fNX-`J+Bk1qFc$H`r!2WDMGzBV<bmed_5WG)R$cvKBnY3?6L z@>gy}Fu`D?+_o_TZMl@LNP+70h{9}*aG}eWj~-u*q0in6Dh4G2k-cw4E``-+juq3; z1?Fzg8F~#SNpF=Uik|*Ait=#kbCUzTN~10Xd<x;^TAp2G_h>a{VxO|&LYW7lN|a5+ zk<bHENh>~R&@H{#QoPJf@$y*Bf0U{u?_8|wz2fZW8vJ(G2NsnD!f`BqDHC)U^L+S# z1tx!z1i~K({Kp-{*3r%Sec$$H%HK0FFf;5l3R_On#mmuv4@P>W@HyWxZ6$EP4-ZTh z{b;yWr(?_4lI-x3+{{bCrb<0dFE8VHYl8jSjd*5azLq3XcMRPIs(!C_k6Y_E$74Kg zZOC#K5PF}xh7<lasBaFwpdzR-7pba_Mlq;>g<gB5R=8ExYN+OAGj+#CtW9DK)iJs< zNFb~#{VqQWn`X_Qp10k!CS{nfYFJm?!f(<R4m(`m@;t5AW^oQ*+w`DVzrC1o?P|r^ zBV&o+t8=c8tWbo5L_}mSq!WZ34>YP`dHCfmG6zAK=iI|3WGU;{q9J@`TWd#v{k`k7 zS<rhYDE{Nq)8xv=^ydK#PuWDl6uqB#P7opm5zTGmqJ=C&NLLe$clMOsYS^R)rpeKH z8I`jk6ZHH_dF#I8yKaIww{J(JHl>E-M58PE8YL@1GSPPFTjiWafrFIt#^te1oZ#ET zxs;v-IlpM!8+C~uJ)}RhXL$tnj(ZX`khJT88zh-vCRPY~&tE+=NDo@MUz`_bYc{E% zY*Zn9(k98kNFP0Jt+oZbQ*ZfWm{w@<7d`Ltc^#$s9;(~H-|xsaO<2R@R^}_$-dah@ zro%6~o+sj9=M)zcw>-<*(UyZOzsA!o9hD`4RGM}=8&OMd!Ro&no+bWY0%@UeC2~DD zgGpd>C@jbu&@;Wv9v;o<O@b-tf%>{v!pP4aFqj_C`aA0$#K+}oaKQC{iZJ<;LpjKI zjli&3ttu>tJ^%>d`v19|_$Z$a|5lHI>)8J=`bWs4>s<!SfsoSyA<wuH^4c5Oy8WqC zfB&~Hen@3>t;O~0+8^W-jWZ_FXm%B&U%Apzra(E)**(s#VyrHxFIQ6!A<1H&8`uX~ z+>ETbZ@o@htyZt|cJ`vFq))8-`E90ZwJ^^3IN`U4v*<}=q(Y*doGb?$2WjbfgcCBZ zUrDNc{vNb3;c)jFMlP_wf>lmk-Z@$K{M=LA30HO3=Z5q^$;K(B{dc~lrS*VpZ@2P6 zy5|&hv<e5GpkMEDJB6osB7gtGn-|iN{RYte6%XU8o0fYg$<V!Faj5A5mj=_>@-2uH z-Xtd$WFzZoshHofNy5uxhInREzLqDXUYL6uV_#P$mR7AK+bwZb%-sT^w7>Edl@!I> zB-%y3XJEaFib>U4o^Wmd_Hf+S7YG*@l~)=QafJ*{7d;mwt>sUz7Nk#sp|fzop>BUd z4>>cj=u-EPe6;&eaLiy&sQUfya)F{Y!J54M{MJLywO>=3H4X?MrXV6%RaK*~9(4-` zRO6R}h?)`3Rx+}+>nRGlHsRfB;j2>BR7nq?)<`E&oX%g4yDdFXnhF)yErq|n4&lcG zFhW#dh2l|upMCCEruUgSzlhxhb%}^$7?!r<OR@U<#;Ajv#P3HcNDritYXIz<(GIo= zzbp7h3lifFyKBXRmr951U^_wD%(miUek-WNf!^BU+9NuAlCMQ*ArbYSZ}(b%iUSv6 z5*2vj$d)jx(IN!JwouO=mE|g;Bg=jv51W1v)CV#v6|!za?C-O2fH5{6pT&^R3c~C` zZs9GUJ^rNp)j?+eJURqGgnCjSm*Behf3@)PaI{{wB5_eVP?vw9k<USM4Ifqnb)BZB zibf2eWT}SLRrB(Z3ZT{^ZB!PsEOEO?JA;-~-unGW2Z`2p>*}|a3cofE+Kp%JfH?iQ z;WIe&Ryp(RIy#SOOUr$7b556&G#vKLiJP}$crvTL(}Bn<@kM4w)^?m*D|HUtcM7@t z;p}T=e5B69Z#Sl#rR4GDS7RR+CT=~ZjqafSUsrTL0^bU$BpWPypkD}cyTq*aGu97p z_({rCtrecvU*<-?kZyvVlm>-v<sc}NsHLHsU*OJ{Ezv}Tqw?9Uv6|Z>OPxa6Pdx%v zM<*jnURj)bE(y4JCLmE!`Cq>h3ms2hXJDcm_Hcw3)TU`SJ}@GBVyzm%92op}*Lc~v z;lgOQr9j@%8PhP$$v-mZQuuNyLdJoBv0gFC@Dx9y<9B%UmZ!&dpsC=_8|i^b5!Swf zULK3HpOH~n?aORJC;LL>sB2a3=L(I-t5iaq9tk+%F_wG&I#AD%aN`Q>{Gv)3)981= zoGP}h8NXh&+!(tHZ7e7*iEf)4nJE&KR&uw<P9RGh7UEH&2v_$`-1~Sdi)N)Qe6nEu z%}wZe_#RPzcYYkj6ty(=;nHekZce8$N9Wv*<#ejZ(6@K+wa)k!=ck-nS3ewR%l=@1 z1)<d!BwPMOYE@9R7ToJR|9zVC?t?b)P=1G}${v?>O5OOFhX;BCoVNg-2X4k7^juZX z4z=cC^Dkm*!a)kuYsT}sRtHc83fHxLrn06^h)YoH0t?pJORqL^q5UFzKkE6<oY_wf zCL3L<TR&d{qGDm`VSatR!dxlk3y|wF?PbJG$E`8JyK#Ks*_Pd5&C{XJM#qYHi*d%( z?<UM}kmAe8tmGp*cC=L>qJ>AwRDG>Os%p!Ln8JBWTgw_AD~8TWE6kD%g?sj=Cp|2B zMu}649@Yfa1qJ25{(WD-$cK%7?l7t{p3#gYj%75_|3ovDlD_#N@@f`*V<H~s`kp$f zfK39JI5>DXea+{5ox(At(_e(BrB!3O9bVeBP2tYqx#rO|4P9+U7@?Aa0#&KJHxil6 zHOx#@pLy>>(o{p_AUaY;^3DkCFI!x^Jmy>e!<H7->$}b@ti^(xWzT=INHZ`O^hs_N z^~Ic)40<;nMWn<F$t&c_PkV>n#i!&Jy*&Os1wG*<C@3q_=z?1dt{i|4@z=6S29!I* zf?j_9`H~!!TIhS~FicCY7Bzpntgs+M&9<h^Re>m5)!}6mf#5AK$|q0mCR!GTQ<@ay zn-1N%!k6`2HC@@~iKD6TrysP_#%eV;y%CX!LXAf2U`wl2RrQIy1zF_KcLPZ~7h7}d zs8)1FV0&gJuZN5A)q;&uamrFbn=W*19MC=OQ@nr?$IJWk)W|BmHcTo%Fe%I)XAT%F z(fv|}LYzjdu8|H2vB*R{9L!8y4|`htzWx=(VgD(Ei*@(V9|L$5bo0NhT0IB~vG>&O zDVXKB#T!od-Qy7|>N+2$PxA|zMkCJ=L~y}xT_aF?&GN<H7l_rh1TKB4j1r{FXKH#D zVVO7nFV<Xpdt*77#MnPX1_j-LQ-iu5|ChbW`yopvs8ef-Mf(2K>xHGI+-a}yu-7+k z95D(~iM={_isk~T25FStaYY0dWpceNshRo#^3l1vnPW$`#VLq>^FA*E`H?eTk->xo zEo0aB+-%%mc<+J9zEyhoWD|MAUfMh_zkifbgfBO-xc76v@kj*aB%Q3NXl}6uY&uG< zrWa}*mUF&-S5NA@-+e-XNGiy8@$X!;X}Iko+ub*DA4JW6K*UtIKJvqc_^@I9QChUL zc&9O4^QZwmko3l(PO}zl?@oq5=Q=sWiXYP!S$h?UAkXT9WMz!m9?ydpY7ex>Urx=q z-{Rk?&-SFIw=Cy!Ubj_DvSz%bZBEU^0Qtkn!zCZudotB>mV5n5RX9blz0-E;^rAsK zHQv&N#e*<_*l2iZFWftQJxN=>Zo?EUTqBo|hjp&(*(*;oNIy}`z_RIZg^qw8P{>5C z4x#@Tb3Fzce8K+4nuFxGAFtnF;G>HSmr@@M>nX{-*hoS-5_{JrC0cGC9ne7__B-~y zH~KobM@Ex(*g;?f{SZlpU4>(B{+55eCJT7O<j2nmMQN8T_z;O{8!m~YVH(}g{;1xn z`3j<_tiV8fA$zn=u61U9x~_5w*&zMe31T!waLen=a*?s#^@O_hm!#f<4Ol>F+s&&k zvXtcHsD1p1E+u3aHY!p&A=hd6ZEXCuqEYB@501%9o%Wo>KC_|?Rg=ogd{d2k1^kw; zg%Af%cFUVN$-Od&1Qhu&r*KaVGu>0sF#S`KsyWKXSBiRqehoE};k-eQ3A%u*ApRK- zny2jr^~#^`gpV?Yw`HR0p$m(PD-}eAP8tpa(oH8AryH6!IwjMo%$m8yWUhtCl2<eL z(-XK}yt(s5@{NWwIMujYFTcpEZnsuwRU|JQ&?ujX&x}C$q!s2Q$&VvZ{t8?IIEm&Z zx47gT@9bt75*oDK#q7->7+;jC^P<^Hx`h>oZLK{kWr7*2{uKAdden*@+$$`pd#2+% za3ammuVb-(#I-S2t%#2aT^*)XtC{jjmdhEWjPK<06x+Mljl6p372}puuS<!n(8fMo zXrDFfj|)jB<rJ3J^F&II2ZSw8ub{ns#{#yP;LYZJ@-^Xep?FuA`jIa)wTg4>0_~Jp zvt+6{_}7j`E6x}gkn%Cfw*=q|Eg^lMJOd-}@31nyo2GFm*Kf6-Q%3RpP|=ttF~+f0 zG>YrEgwnh>tcWn@xtH1YH)G~gw?<O3#E19(u!gr4l4~8TB1w2tD|?lz)^^-5HGGuj zqLxvxv-aXnVU@Je%a?lhr;>tJ`sJfyylO?me+xpre<wYf9_T5KClwT2sI-iXjV}g4 z5gGXe(|NjHh6@-d_twJA)%bgvW_ocY39I~#C#T0j%ftmk_u&=*h*_GOE9s{-P1=Sd zr=#*IMWAGMhF4&-_B}vM3@1E=-J_K5%Wb*Vb4~{qq0c$$yM{g5zio+_hf7iB5y|ot zWPQ`tEmhBMU4XhRCUYc)m-P1|?A9%+R`49@c!XsTP5fy?j3<nYoLa;p!LuH1NtL}o zLDAaqisVWS4?6?y3K+W*m`$?6SNR<%q(wNZT$Wi@=TRxQ_({&bimt+ph2Iw;J<2}+ zD!rA90NPU&Lp$R;16aJN;0HJ$RFe$uh9aSXF4kQtz!t#Bgbap(v4|9Yd+I_NHB6cZ z;Og|xD`jC{_A=AZgezfwr<T>cNg`7^%ytT)-B@EgSq%8<!kl_FiOhGZu~h|yHg6pr zgP$lMSf!Hkn_Hr*y!Ld#I@O!t^~y|d^GvfV>{ZS$D2i;c5U@o>aZaXZoGGCl3%UCa z#|+sEFSt;#SyhGA7H=)1Sf!+t#NDg3S>UkKE?&=xNfvm<T-atbPk9rM=T59d7Qs+8 z>!CT_q05!5eiw&DCwAk6hpK<*NykOmiG}N$HMCD<msIAxrfjTu&6(1m+|PbLDoalU z69njn*GVp_szdD?E;aSg+L?zn&+wk06oZ$%(+h$~H%=^@DBPi|^rwQVh_HKz0tU+k z7Z1;m85xx_cbDqWYaE!5WR=^+3z~KNvL<qmpGo6<KoLEjFQrXE=-u+2`E0~OIi!wz zaZ!h}CyVJW@vXlYDRjGjT@<_RP~e)5sGOlpA?0+>Bj4t{%`fyiB|Z3|^5sWb5AUzU zP;ZP!W|t2zNGl!IO8H6pTkf2x0?RD2Yhfh)x;xTn!=+Frh;hPt!jwSiwGu(2fL;EN zA8&@38P;RGQVOO32sC!RVbMB(zgs)SpA?%*uEojiHfFM<jhi@CuoAA=zlbkT1(;zy z{MRkXn&tYT38Y%{J8r7)->xEDLFFzhQA?5YxvEwGu{C?X=}$L6`kLcSyA!yBIUvc2 z<7how^<c|RSyn0UDZ*2FL7Iz<a}PM|2&tR87qy5yYhqw!?C+b&L^*p)7vu}u|KO1f zl;>8h(mcCh@T%C05VQ>^B_$o+Dxby)cVaLRwexD^RtAPr->Qk$yzPgT@>gCjq}8+{ z!0wm5Kv}9D!Tc5=15Jcs;jL_pvVD6KGHP^fc-7>#qnbPff**Dj3!ueyfNcRbu!9&c zfAP;rl{x@-WX0%eUbd=h?oTtAm@F=zOq3`Mm^4yL7s74SR*B$U#80TD`JJAs%ZGN$ zkn}BORUL2Eeha*Z|2Qen&yE$|pyc`5H5+d|GkJmoz;PnYI;YILW($R8sgM06pEPQS z^pU2iro!@fM#K4SgNE{NVAJkOVEY7r|8s83^h7iln5dP9sf?=1?_6|G%l!NqO`IeO zmEBolSTE}|O@qbBMvIh0MSUpT=9f0=o16?r;+KOMItwsADoQOzWJOd(eJ(919|2%6 zi}epM2xg1#mLX+V`}#d?ZzSNw)3;w*B=m-!K9hbfp_e~2lljoNDg}Q20p`}ITdYy0 z;T!NG^<4x6qAfV-gYWe0s}Cewn_bO=w@A&==*?+0`QxnlNTUUBiWVGxf3V)W-Dir` z^gNC_1QzU3y|vwoImG9?WqdgJ&dVP!kGvm}T=59}8BYV0;d(-^PI-$}<u#-cl@_@8 z>P<67#&>R~eb@16u^0vJiz=%`*ah+VqM}G<EyVuIm1E)N@$o66`_OREhHHGtuZq+t zfKiAB4>__!dY%Fny6+}C2bR-2nrP_{9oLD58KwC=T=Th>`Yc$j&A1XCQjE&7OVi_g zL;WWa9r@y^=pEm|hu+8fs>Go1d6txOERkhVX*42x!F>B0$b&VG9VTIr6yxQqZ*^L} zxdJ>yg0?mD7+?_<WY=j(M0dCv9bHk@q-126ZNlD^kqgLQm}M-2_uJI3?LfuNx>xoB zz1xglT5UO;-WR%@-BWaGDRy1$Ii-xD2D#kkQmDW|;6OeGl#kQazI0G%IMRG+d*`&! zm@UESe7>wf8Uk#r<U0_ZptiaGIRb@YT-M&`Rztou7nDcxmnGOmIc$G0F&y`<7VoB7 z(?z361$LDcBAHzd9Oe$uegdq9Wu*{_OkL<kvFxn2a_MsK&A%`$t?=kyX&-7;^%2Yd zv)E1)oH2YN6O_?kKJcJ5Zx;EQ5BBELUJbg6@vJL!vr2UCHJEhZ9bG)R{(6dYgGNt; z5{XdSg*xw%&!N^Uie0P<)-W>yUC#D;VN5XM(2mCi>~n+E(tbCyp>~@}M2}($)eVb2 z`4;1pm$}@&8aT+3j<Hi5h1Us%hRsJ*11xvi(>e0Wh%v2@wmY*(hwg_AC|{lv0;Rt2 zge8Jn9=V2%s(4=5y&?q`s?~Vg+VI#|7V!Q6hGk={Tpf@n$5~$0BPBr88{m)f%wC72 zcp21N7Od^BZY?opXyrwn9F8=Ic;^{pvW&)Z2CRviR$uTeUC$1eo%?YOqS?w%2<Z#+ z$CHf|dMPE5`3m^xI)QQl%2v`LSXEQIL|T~xU<O(+B*5-eER;8kdjqx^)cG)$w33=> zHF)jT!!SOJ)MEO7v?YxKQpPk`qlYa(LQhCcpxT7tf3r*zujpczO6NISP~l96d$@{& zG_;_gNL9ziHz84L-X3MblR7opeU~Do2!v8G0LX|!u8`rAr#N&9Ybn3eWrlZUqc3*x z(LBI7TduV&_%D<4Q1iXm?MFmc%GhVG`2Sn0!Te?(&i%X|a4<M~te01iA%Q@mN+nZL zpx{APpp2}#eneA>+a`<z&woz0i%&ZZwR$pFJ*+ay$Tb-bs1I|ZD=Iaai;JgY*6y8H zmU2|3ndH|_nL=vmsW~bl3X+1>J@x$jGI18TR>0l)<FG1V5zv=dSJ)&9N!^ha+TU!* z#=#|cSCjWm`CwcVarGn8&7r89RzpK$Q~<91_@o4u!&__57$z9a1`3IOq;jw+*zR`| zQrW>}HBzJzg9Yi4lgE4{tmb=dU65ZeJbS3Vl9(JY|G{IZuP+Y<$EdkwDbrtvuB{cv zYYKY%sj*H_xdzL^@mCq8fp@Rc{DOh`(yj56^+zC}11KUbZo1;sPjA*G1-7AAN-*^N zRRR?AMaU%8;Qlk(O0q|k*)bozDjl_Qh#PBOGs10$W{Ny9>x7wsQn@DwYxys5N6owZ zbvlJT#n^?Y7M8Yhf5xPcpFpp+|I`N%mTSbt*L^Dbrh`XjCcl}J138v+1oqKiXU@O1 z|FCdZt@bBjn0KtO#<#_y@XP<;krkHuC~W?rl4XBaMN2SJ4}IvL0EVBAsSI$G<p5yl z8p!jP!4?(xUv>pRSgtnK$*O9<y)wmd?p8Cj6VD5{j2Jm`UHJdis|Ac!&aH8;NI6;L z?HVu2b{%krbM+c>gzYoW&X}XZ6yE)ZDP?u_8$g=5SWo2opGaU<2@5ASkqR_}{aDK; z!J}3!38g~=vr$501(eg?jnMrd;S&76owvU)E)f8#XKqFQzrFv)9PwORy_A+?d0oy6 zzi0R(;)|QQk40DJ2EqhjqX*|u<3$y2Zl0CwrA7MFw@BN@HBCSNfaOweH^sz}4V9DH z^x}i$zc9W*Hi~P>ucp$`DhE{FdS~mB&SN*0-DY+!#p4kH7OXK3dlwPJe)GuY8f3%# zUl^=dxK7?lJygfN+aAPH1F}j5J%2<P)^vip4~RP=Qi_b}FCSK;hm`xv?(Y(OGyfhz zy|6@FJ?;vXC<vj62A{uRyQkkvULsR7LCcA{=KWQ9;+eaANiHnlOpqj?;=rT-jh*J^ z;RX}?EVKX51TPcS{O37eavro%a4Qp{1^U}DI3Yn3wLW2qFSt2&g1Yq=70Sm6s>u?U zs9L<MQ@TZCKCnSjKJHtq|CiHsx{)nTbh4aGl&~|O92E8?g8KX<Uws|}U9cZh(bDmr zyx<<j;NTx3o-Xlt4neNyCFd%9sOz*6vtIc-Ud4_-89?an{I8P#f6z4BV~D*!H~C64 zo@bx%FzVUXNMe12?H*RNTXWbqLm*2bf4T4vJMTz2m^7L^U?oWg&w{=LtEkA;Qg@hh zEh7VHtS%B$ExLD^0fSE(m*DiOr!YTX0~f7ZQXU0rl+e7O#`M*Gd0wXv@o_ZWhBoSU zLLg=|B6fGgmqiT?X_an3YHtII2ng7f68g9H|K;8JFHg_+n~N?U{Hh`lpRjJZJCL+< zCJ=-|HsYE6l^DL>taiSAv33<40lWAUd{T<;i4YoNkUjw@aQAR;)R{L|_OFj7vxKyu z5pg#^KV2Fy##hP{ENE(JFuW(*$qQHX9I)7a8RHGd?N}g<+QJkE)wMW=2L>`|Q$7B~ zoL4!gn4t*%cHA8MUjk{IPOg9dw?LXKV7e?eHi!xl8t$*-4yr2a<f?m%0K4{etl-mE zQBL;WB^2(K1#-FldRj~4)91kHB?2RZhzhWigj!nI_D4<{g3~qp`~-k1QanA{4{7Fz z4*?&UUa8^H!~>ZqoLw1}IK7-N(WO!2hF>{<r}wK9^oQBRqbUVgt`s^jZ;E;=8c(qx zPB4H{{&AAriEgv(_)!HMu<D)PZ^+jRz2WibvmmzvF*D&C&7dC1h*Z@7;llBl+Hj!f zFG<{T|JDNS`KOy#K4ng}e_fLisi~>j%q(aarYdxo03)(FwmTrx`pb1s$mR1B^5NV6 zWqUPs-^cO|L&FL#GzEGMo}Z?E0CQXt3S5JI3PE8ps}n_@byE~KZ=7_Xd#Nf`mIJnc zF^#HO`bM2!l=T`Ao5}WPzz|W<QZB7WNigf}X5YHIwj{vAH3_!<kvyR50R0YdP$!>u z-(p=-QB!r#p7jYB37fwoVPR=G@}mY`2<}BwAq`X@m+h>S^nkoF$NqGKA_$itckt)) z<mIBY`WNyN(A~>Ok{a`!v)GevR^7<L*5bfCHXRRTXyTGX`2~u<w_*hw(pN<S762R^ z-xS}`s|8$l-T%QPino4No~*d{-&`V)hyPgpkM06-{=-yx2*P8?{)WdixBquw%zx`3 yAV58VWZ$kNDT(OcT}c1wRQjLt_Ge)G5{s0rg22Hy`PJ34rNtG*N?z-K`hNfoxmHa8 diff --git a/documentation/mcd/anis_v3_mcd.txt b/documentation/mcd/anis_v3_mcd.txt deleted file mode 100644 index 12b1c28..0000000 --- a/documentation/mcd/anis_v3_mcd.txt +++ /dev/null @@ -1,27 +0,0 @@ -http://www.mocodo.net/ - -project: name, label, description, link, manager -DF, 0N database, 11 project -database: id, label, dbname, type, host, port, login, password - -DF1, 0N project, 11 dataset -DF2, ON dataset_family, 11 dataset -dataset_family: id, label, description, display -output_family: id, label, display -DF8, 11 output_category, 0N output_family - -file: id, label, file_loc, type, display, visible -DF5, ON dataset, 11 file -dataset: name, table_ref, label, description, display, count, vo, data_path -DF4, 0N dataset, 11 attribute -attribute: id, name, table_name, label, form_label, description, output_display, criteria_display, search_flag, search_type, operator, type, min, max, placeholder_min, placeholder_max, uri_action, renderer, display_detail, selected, order_by, order_display, detail, renderer_detail, options, vo_utype, vo_ucd, vo_unit, vo_description, vo_datatype, vo_size -DF9, 0N output_category, 01 attribute -output_category: id, label, display - -dataset_privileges, 0N anis_group, 0N dataset: visible -anis_group: id, label -criteria_family: id, label, display -DF3, 0N criteria_family, 01 attribute - -DF7, 0N anis_group, 11 anis_user -anis_user: email, password, activation_key, activated, adminsi, superuser \ No newline at end of file diff --git a/documentation/mcd/anis_v3_settings_mcd.png b/documentation/mcd/anis_v3_settings_mcd.png deleted file mode 100644 index 48eb22c8b53d8067c179cd735966e1e0239e8e2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7201 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 W`;b78ZvBtULs6DjldF(14*DPNWb8=* diff --git a/documentation/mcd/anis_v3_settings_mcd.txt b/documentation/mcd/anis_v3_settings_mcd.txt deleted file mode 100644 index e7e27c0..0000000 --- a/documentation/mcd/anis_v3_settings_mcd.txt +++ /dev/null @@ -1,5 +0,0 @@ -http://www.mocodo.net/ - -settings_select: id, attribute_name, label -DF1, 0N settings_select, 11 settings_option -settings_option: id, label, value, display diff --git a/public/index.php b/public/index.php index aa0f403..4e3f077 100644 --- a/public/index.php +++ b/public/index.php @@ -1,40 +1,59 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -use \Psr\Http\Message\ServerRequestInterface as Request; -use \Psr\Http\Message\ResponseInterface as Response; - -if (PHP_SAPI == 'cli-server') { - // To help the built-in PHP dev server, check if the request was actually for - // something which should probably be served as a static file - $url = parse_url($_SERVER['REQUEST_URI']); - $file = __DIR__ . $url['path']; - if (is_file($file)) { - return false; - } -} +declare(strict_types=1); + +use DI\Container; +use Slim\Factory\AppFactory; +use App\Handlers\LogErrorHandler; +// Autoloading for libraries require __DIR__ . '/../vendor/autoload.php'; -// Instantiate the app -$settings = require __DIR__ . '/../src/settings.php'; -$app = new \Slim\App($settings); +// Load app constants +require __DIR__ . '/../app/constants.php'; + +// Create Container using PHP-DI +$container = new Container(); + +// Setup dependencies +require __DIR__ . '/../app/dependencies.php'; + +// Set container to create App with on AppFactory +AppFactory::setContainer($container); +$app = AppFactory::create(); + +$ded = $container->get(SETTINGS)['displayErrorDetails']; +if (is_string($ded)) { + $displayErrorDetails = $ded === 'true'; +} else { + $displayErrorDetails = $ded; +} + +$errorHandler = new LogErrorHandler($app->getCallableResolver(), $app->getResponseFactory()); +$errorHandler->forceContentType('application/json'); +$errorHandler->setLogger($container->get('logger')); -// Set up dependencies -require __DIR__ . '/../src/dependencies.php'; +// Add Error Handling Middleware (JSON only) +$errorMiddleware = $app->addErrorMiddleware( + $displayErrorDetails, + true, + true +); +$errorMiddleware->setDefaultErrorHandler($errorHandler); -// Register middleware -require __DIR__ . '/../src/middleware.php'; +// Register middlewares +require __DIR__ . '/../app/middlewares.php'; // Register routes -require __DIR__ . '/../src/routes.php'; +require __DIR__ . '/../app/routes.php'; // Run app $app->run(); diff --git a/src/Action/AbstractAction.php b/src/Action/AbstractAction.php new file mode 100644 index 0000000..727da93 --- /dev/null +++ b/src/Action/AbstractAction.php @@ -0,0 +1,52 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Doctrine\ORM\EntityManagerInterface; + +/** + * Centralize access to the database and some common functions + * + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +abstract class AbstractAction +{ + /** + * The EntityManager is the central access point to Doctrine ORM functionality + * + * @var EntityManagerInterface + */ + protected $em; + + /** + * Create the classe before call __invoke to execute the action + * + * @param EntityManagerInterface $em Doctrine Entity Manager Interface + */ + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + + /** + * @param string $field + * @param array $parsedBody + * + * @return string true if field is empty or false else + */ + protected function isEmptyField(string $field, array $parsedBody): bool + { + return !isset($parsedBody[$field]); + } +} diff --git a/src/Action/Admin/InstanceAction.php b/src/Action/Admin/InstanceAction.php deleted file mode 100644 index e41fb72..0000000 --- a/src/Action/Admin/InstanceAction.php +++ /dev/null @@ -1,110 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\Instance; - -final class InstanceAction -{ - use ActionTrait; - - private $logger; - private $em; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $em) - { - $this->logger = $logger; - $this->em = $em; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Instance action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $instance = $this->em->find('App\Entity\Admin\Instance', $args['name']); - - if (is_null($instance)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Instance with name ' . $args['name'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($instance); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - $fields = array( - 'label', - 'path_proxy', - 'dev_mode', - 'driver', - 'path', - 'host', - 'port', - 'dbname', - 'login', - 'password' - ); - foreach ($fields as $f) { - if ($this->isEmptyField($f, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $f . ' needed to edit an instance' - ); - } - } - - $this->editInstance($instance, $parsedBody); - $newResponse = $response->withJson($instance); - } - - if ($request->isDelete()) { - $name = $instance->getName(); - $this->em->remove($instance); - $this->em->flush(); - $newResponse = $response->withJson(array('message' => 'Instance with name ' . $name . ' is removed!')); - } - - return $newResponse; - } - - private function editInstance(Instance $instance, array $parsedBody): void - { - $instance->setLabel($parsedBody['label']); - $instance->setPathProxy($parsedBody['path_proxy']); - $instance->setDevMode($parsedBody['dev_mode']); - $instance->setDriver($parsedBody['driver']); - $instance->setPath($parsedBody['path']); - $instance->setHost($parsedBody['host']); - $instance->setPort($parsedBody['port']); - $instance->setDbName($parsedBody['dbname']); - $instance->setLogin($parsedBody['login']); - $instance->setPassword($parsedBody['password']); - $this->em->flush(); - } -} diff --git a/src/Action/Admin/InstanceListAction.php b/src/Action/Admin/InstanceListAction.php deleted file mode 100644 index eba0e3b..0000000 --- a/src/Action/Admin/InstanceListAction.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\Instance; - -final class InstanceListAction -{ - use ActionTrait; - - private $logger; - private $em; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $em) - { - $this->logger = $logger; - $this->em = $em; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Instance list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - if ($request->isGet()) { - $instances = $this->em->getRepository('App\Entity\Admin\Instance')->findAll(); - $newResponse = $response->withJson($instances); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - $fields = array( - 'name', - 'label', - 'path_proxy', - 'dev_mode', - 'driver', - 'path', - 'host', - 'port', - 'dbname', - 'login', - 'password' - ); - foreach ($fields as $f) { - if ($this->isEmptyField($f, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $f . ' needed to add a new instance' - ); - } - } - - $instance = $this->postInstance($parsedBody); - $newResponse = $response->withJson($instance, 201); - } - - return $newResponse; - } - - private function postInstance(array $parsedBody): Instance - { - $instance = new Instance($parsedBody['name']); - $instance->setLabel($parsedBody['label']); - $instance->setPathProxy($parsedBody['path_proxy']); - $instance->setDevMode($parsedBody['dev_mode']); - $instance->setDriver($parsedBody['driver']); - $instance->setPath($parsedBody['path']); - $instance->setHost($parsedBody['host']); - $instance->setPort($parsedBody['port']); - $instance->setDbName($parsedBody['dbname']); - $instance->setLogin($parsedBody['login']); - $instance->setPassword($parsedBody['password']); - - $this->em->persist($instance); - $this->em->flush(); - - return $instance; - } -} diff --git a/src/Action/Admin/MetamodelAction.php b/src/Action/Admin/MetamodelAction.php deleted file mode 100644 index 2f74c93..0000000 --- a/src/Action/Admin/MetamodelAction.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Tools\SchemaTool; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; - -final class MetamodelAction -{ - use ActionTrait; - - private $logger; - private $aem; - private $memf; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->aem = $aem; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Metamodel action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - $instance = $this->aem->find('App\Entity\Admin\Instance', $args['name']); - - if (is_null($instance)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Instance with name ' . $args['name'] . ' is not found' - )->withStatus(404); - } - - if (!in_array($args['action'], array('create-database', 'update-database', 'delete-database'))) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Action ' . $args['action'] . ' is not allowed!' - )->withStatus(400); - } - - $this->memf->createMetaEntityManager($args['name']); - $instanceEntityManager = $this->memf->getMetaEntityManager(); - $schemaTool = new SchemaTool($instanceEntityManager); - $classes = $instanceEntityManager->getMetadataFactory()->getAllMetadata(); - - switch ($args['action']) { - case 'create-database': - $schemaTool->createSchema($classes); - $message = 'Database metamodel has been created!'; - break; - - case 'update-database': - $schemaTool->updateSchema($classes); - $message = 'Database metamodel has been updated!'; - break; - - case 'delete-database': - $schemaTool->dropSchema($classes); - $message = 'Database metamodel has been removed!'; - break; - } - - return $response->withJson(array('message' => $message)); - } -} diff --git a/src/Action/Admin/OptionAction.php b/src/Action/Admin/OptionAction.php deleted file mode 100644 index b8ac0d0..0000000 --- a/src/Action/Admin/OptionAction.php +++ /dev/null @@ -1,91 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\SettingsOption; - -final class OptionAction -{ - use ActionTrait; - - private $logger; - private $aem; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem) - { - $this->logger = $logger; - $this->aem = $aem; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Settings option action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $option = $this->aem->find('App\Entity\Admin\SettingsOption', $args['id']); - - if (is_null($option)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Settings option with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($option); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('label', 'value', 'display') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the settings option' - ); - } - } - - $this->editOption($option, $parsedBody); - $newResponse = $response->withJson($option); - } - - if ($request->isDelete()) { - $id = $option->getId(); - $this->aem->remove($option); - $this->aem->flush(); - $newResponse = $response->withJson(array('message' => 'Settings option with id ' . $id . ' is removed!')); - } - - return $newResponse; - } - - private function editOption(SettingsOption $option, array $parsedBody): void - { - $option->setLabel($parsedBody['label']); - $option->setValue($parsedBody['value']); - $option->setDisplay($parsedBody['display']); - $this->aem->flush(); - } -} diff --git a/src/Action/Admin/OptionListAction.php b/src/Action/Admin/OptionListAction.php deleted file mode 100644 index f4c8efd..0000000 --- a/src/Action/Admin/OptionListAction.php +++ /dev/null @@ -1,90 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\SettingsSelect; -use App\Entity\Admin\SettingsOption; - -final class OptionListAction -{ - use ActionTrait; - - private $logger; - private $aem; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem) - { - $this->logger = $logger; - $this->aem = $aem; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Settings option list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - if ($request->isGet()) { - $options = $this->aem->getRepository('App\Entity\Admin\SettingsOption')->findAll(); - $newResponse = $response->withJson($options); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('label', 'value', 'display', 'id_settings_select') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new settings option' - ); - } - } - - $select = $this->aem->find('App\Entity\Admin\SettingsSelect', $parsedBody['id_settings_select']); - if (is_null($select)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Settings select with id ' . $parsedBody['id_settings_select'] . ' is not found' - )->withStatus(404); - } - - $option = $this->postOption($select, $parsedBody); - $newResponse = $response->withJson($option, 201); - } - - return $newResponse; - } - - private function postOption(SettingsSelect $select, array $parsedBody): SettingsOption - { - $option = new SettingsOption($select); - $option->setLabel($parsedBody['label']); - $option->setValue($parsedBody['value']); - $option->setDisplay($parsedBody['display']); - - $this->aem->persist($option); - $this->aem->flush(); - - return $option; - } -} diff --git a/src/Action/Admin/SelectAction.php b/src/Action/Admin/SelectAction.php deleted file mode 100644 index f97ece1..0000000 --- a/src/Action/Admin/SelectAction.php +++ /dev/null @@ -1,90 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\SettingsSelect; - -final class SelectAction -{ - use ActionTrait; - - private $logger; - private $aem; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem) - { - $this->logger = $logger; - $this->aem = $aem; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Settings select action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $select = $this->aem->find('App\Entity\Admin\SettingsSelect', $args['id']); - - if (is_null($select)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Settings select with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($select); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('name', 'label') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the settings select' - ); - } - } - - $this->editSelect($select, $parsedBody); - $newResponse = $response->withJson($select); - } - - if ($request->isDelete()) { - $id = $select->getId(); - $this->aem->remove($select); - $this->aem->flush(); - $newResponse = $response->withJson(array('message' => 'Settings select with id ' . $id . ' is removed!')); - } - - return $newResponse; - } - - private function editSelect(SettingsSelect $select, array $parsedBody): void - { - $select->setName($parsedBody['name']); - $select->setLabel($parsedBody['label']); - $this->aem->flush(); - } -} diff --git a/src/Action/Admin/SelectListAction.php b/src/Action/Admin/SelectListAction.php deleted file mode 100644 index 273454b..0000000 --- a/src/Action/Admin/SelectListAction.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\SettingsSelect; - -final class SelectListAction -{ - use ActionTrait; - - private $logger; - private $aem; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem) - { - $this->logger = $logger; - $this->aem = $aem; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Settings select list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - if ($request->isGet()) { - $selects = $this->aem->getRepository('App\Entity\Admin\SettingsSelect')->findAll(); - $newResponse = $response->withJson($selects); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('name', 'label') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new settings select' - ); - } - } - - $select = $this->postSelect($parsedBody); - $newResponse = $response->withJson($select, 201); - } - - return $newResponse; - } - - private function postSelect(array $parsedBody): SettingsSelect - { - $select = new SettingsSelect(); - $select->setName($parsedBody['name']); - $select->setLabel($parsedBody['label']); - - $this->aem->persist($select); - $this->aem->flush(); - - return $select; - } -} diff --git a/src/Action/Admin/UserAction.php b/src/Action/Admin/UserAction.php deleted file mode 100644 index 582c91a..0000000 --- a/src/Action/Admin/UserAction.php +++ /dev/null @@ -1,91 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\User; - -final class UserAction -{ - use ActionTrait; - - private $logger; - private $aem; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem) - { - $this->logger = $logger; - $this->aem = $aem; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('User action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $user = $this->aem->find('App\Entity\Admin\User', $args['email']); - - if (is_null($user)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'User with email ' . $args['email'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($user); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('adminsi', 'superuser', 'activated') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the user' - ); - } - } - - $this->editUser($user, $parsedBody); - $newResponse = $response->withJson($user); - } - - if ($request->isDelete()) { - $email = $user->getEmail(); - $this->aem->remove($user); - $this->aem->flush(); - $newResponse = $response->withJson(array('message' => 'User with email ' . $email . ' is removed!')); - } - - return $newResponse; - } - - private function editUser(User $user, array $parsedBody): void - { - $user->setAdminsi($parsedBody['adminsi']); - $user->setSuperuser($parsedBody['superuser']); - $user->setActivated($parsedBody['activated']); - $this->aem->flush(); - } -} diff --git a/src/Action/Admin/UserListAction.php b/src/Action/Admin/UserListAction.php deleted file mode 100644 index c402a82..0000000 --- a/src/Action/Admin/UserListAction.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Admin; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Entity\Admin\User; - -final class UserListAction -{ - use ActionTrait; - - private $logger; - private $aem; - - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem) - { - $this->logger = $logger; - $this->aem = $aem; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('User list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - if ($request->isGet()) { - $users = $this->aem->getRepository('App\Entity\Admin\User')->findAll(); - $newResponse = $response->withJson($users); - } - - return $newResponse; - } -} diff --git a/src/Action/Meta/AttributeAction.php b/src/Action/AttributeAction.php similarity index 50% rename from src/Action/Meta/AttributeAction.php rename to src/Action/AttributeAction.php index 8b54e81..562c5f1 100644 --- a/src/Action/Meta/AttributeAction.php +++ b/src/Action/AttributeAction.php @@ -1,119 +1,68 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Action\Meta; +declare(strict_types=1); + +namespace App\Action; -use Psr\Log\LoggerInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Attribute; -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Dataset; -use App\Entity\Metamodel\Attribute; -use App\Entity\Metamodel\CriteriaFamily; -use App\Entity\Metamodel\OutputCategory; - -/** - * Route: /metadata/dataset/{name}/attribute/{id} - * {name}: Dataset name - * {id}: Attribute id - * - * This action is used to manage one attribute - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class AttributeAction +final class AttributeAction extends AbstractAction { - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param MetaEntityManagerFactory $memf - */ - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - /** * `GET` Returns the attribute found - * `PUT` Full update an attribute and returns the new version + * `PUT` Full update the attribute and returns the new version * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) * * @return ResponseInterface */ public function __invoke(Request $request, Response $response, array $args): Response { - $this->logger->info('Attribute list action dispatched'); - - if ($request->isOptions()) { + if ($request->getMethod() === OPTIONS) { return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, OPTIONS'); } - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - // Search the correct attribute with primary key - $attribute = $this->memf->getMetaEntityManager()->getRepository('App\Entity\Metamodel\Attribute')->findOneBy( + $attribute = $this->em->getRepository('App\Entity\Attribute')->findOneBy( array('dataset' => $args['name'], 'id' => $args['id']) ); // If attribute is not found 404 if (is_null($attribute)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Attribute with dataset name ' . $args['name'] . ' and attribute id ' . $args['id'] . 'is not found' - )->withStatus(404); + throw new HttpNotFoundException( + $request, + 'Attribute with dataset name ' . $args['name'] . ' and attribute id ' . $args['id'] . ' is not found' + ); } - if ($request->isGet()) { - $newResponse = $response->withJson($attribute); + if ($request->getMethod() === GET) { + $payload = json_encode($attribute); } - if ($request->isPut()) { + if ($request->getMethod() === PUT) { $parsedBody = $request->getParsedBody(); $this->editAttribute($attribute, $parsedBody); - $newResponse = $response->withJson($attribute); + $payload = json_encode($attribute); } - return $newResponse; + $response->getBody()->write($payload); + return $response; } - private function editAttribute( - Attribute $attribute, - array $parsedBody - ): void { + private function editAttribute(Attribute $attribute, array $parsedBody): void + { $attribute->setLabel($parsedBody['label']); $attribute->setFormLabel($parsedBody['form_label']); $attribute->setDescription($parsedBody['description']); @@ -145,8 +94,8 @@ final class AttributeAction if (is_null($parsedBody['id_criteria_family'])) { $criteriaFamily = null; } else { - $criteriaFamily = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\CriteriaFamily', + $criteriaFamily = $this->em->find( + 'App\Entity\CriteriaFamily', $parsedBody['id_criteria_family'] ); } @@ -154,12 +103,12 @@ final class AttributeAction if (is_null($parsedBody['id_output_category'])) { $outputCategory = null; } else { - $outputCategory = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\OutputCategory', + $outputCategory = $this->em->find( + 'App\Entity\OutputCategory', $parsedBody['id_output_category'] ); } $attribute->setOutputCategory($outputCategory); - $this->memf->getMetaEntityManager()->flush(); + $this->em->flush(); } } diff --git a/src/Action/AttributeListAction.php b/src/Action/AttributeListAction.php new file mode 100644 index 0000000..3ddc339 --- /dev/null +++ b/src/Action/AttributeListAction.php @@ -0,0 +1,55 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Database; + +final class AttributeListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all attributes of a dataset listed in the metamodel database + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + } + + $dataset = $this->em->find('App\Entity\Dataset', $args['name']); + + // Returns HTTP 404 if the dataset is not found + if (is_null($dataset)) { + throw new HttpNotFoundException( + $request, + 'Dataset with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $attributes = $this->em->getRepository('App\Entity\Attribute')->findByDataset($dataset); + $payload = json_encode($attributes); + } + + $response->getBody()->write($payload); + return $response; + } +} diff --git a/src/Action/DatabaseAction.php b/src/Action/DatabaseAction.php new file mode 100644 index 0000000..ad3e49e --- /dev/null +++ b/src/Action/DatabaseAction.php @@ -0,0 +1,100 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Database; + +final class DatabaseAction extends AbstractAction +{ + /** + * `GET` Returns the database found + * `PUT` Full update the database and returns the new version + * `DELETE` Delete the database found and return a confirmation message + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct database with primary key + $database = $this->em->find('App\Entity\Database', $args['id']); + + // If database is not found 404 + if (is_null($database)) { + throw new HttpNotFoundException( + $request, + 'Database with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($database); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + // If mandatories empty fields 400 + foreach (array('label', 'dbname', 'dbtype', 'dbhost', 'dbport', 'dblogin', 'dbpassword') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the database' + ); + } + } + + $this->editDatabase($database, $parsedBody); + $payload = json_encode($database); + } + + if ($request->getMethod() === DELETE) { + $id = $database->getId(); + $this->em->remove($database); + $this->em->flush(); + $payload = json_encode(array('message' => 'Database with id ' . $id . ' is removed!')); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Update database object with setters + * + * @param Database $database The database to update + * @param array $parsedBody Contains the new values ​​of the database sent by the user + */ + private function editDatabase(Database $database, array $parsedBody): void + { + $database->setLabel($parsedBody['label']); + $database->setDbName($parsedBody['dbname']); + $database->setType($parsedBody['dbtype']); + $database->setHost($parsedBody['dbhost']); + $database->setPort($parsedBody['dbport']); + $database->setLogin($parsedBody['dblogin']); + $database->setPassword($parsedBody['dbpassword']); + $this->em->flush(); + } +} diff --git a/src/Action/DatabaseListAction.php b/src/Action/DatabaseListAction.php new file mode 100644 index 0000000..0c6759e --- /dev/null +++ b/src/Action/DatabaseListAction.php @@ -0,0 +1,87 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use App\Entity\Database; + +final class DatabaseListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all databases listed in the metamodel database + * `POST` Add a new database + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + if ($request->getMethod() === GET) { + // Retrieve user with email adress + $databases = $this->em->getRepository('App\Entity\Database')->findAll(); + $payload = json_encode($databases); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs user information to update + foreach (array('label', 'dbname', 'dbtype', 'dbhost', 'dbport', 'dblogin', 'dbpassword') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new database' + ); + } + } + + $database = $this->postDatabase($parsedBody); + $payload = json_encode($database); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Add a new database into the metamodel + * + * @param array $parsedBody Contains the values ​​of the new database sent by the user + */ + private function postDatabase(array $parsedBody): Database + { + $database = new Database(); + $database->setLabel($parsedBody['label']); + $database->setDbName($parsedBody['dbname']); + $database->setType($parsedBody['dbtype']); + $database->setHost($parsedBody['dbhost']); + $database->setPort($parsedBody['dbport']); + $database->setLogin($parsedBody['dblogin']); + $database->setPassword($parsedBody['dbpassword']); + + $this->em->persist($database); + $this->em->flush(); + + return $database; + } +} diff --git a/src/Action/DatasetAction.php b/src/Action/DatasetAction.php new file mode 100644 index 0000000..02960d2 --- /dev/null +++ b/src/Action/DatasetAction.php @@ -0,0 +1,122 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Dataset; +use App\Entity\DatasetFamily; + +final class DatasetAction extends AbstractAction +{ + /** + * `GET` Returns the dataset found + * `PUT` Full update the dataset and returns the new version + * `DELETE` Delete the dataset found and return a confirmation message + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct dataset with primary key + $dataset = $this->em->find('App\Entity\Dataset', $args['name']); + + // If dataset is not found 404 + if (is_null($dataset)) { + throw new HttpNotFoundException( + $request, + 'Dataset with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($dataset, JSON_UNESCAPED_SLASHES); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + // If mandatories empty fields 400 + $fields = array( + 'label', + 'description', + 'display', + 'count', + 'vo', + 'data_path', + 'selectable_row', + 'id_dataset_family' + ); + foreach ($fields as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the dataset' + ); + } + } + + // Dataset family is mandatory to edit a dataset + $family = $this->em->find('App\Entity\DatasetFamily', $parsedBody['id_dataset_family']); + if (is_null($family)) { + throw new HttpBadRequestException( + $request, + 'Dataset family with id ' . $parsedBody['id_dataset_family'] . ' is not found' + ); + } + + $this->editDataset($dataset, $parsedBody, $family); + $payload = json_encode($dataset, JSON_UNESCAPED_SLASHES); + } + + if ($request->getMethod() === DELETE) { + $name = $dataset->getName(); + $this->em->remove($dataset); + $this->em->flush(); + $payload = json_encode(array('message' => 'Dataset with name ' . $name . ' is removed!')); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Update dataset object with setters + * + * @param Dataset $dataset The dataset to update + * @param string[] $parsedBody Contains the new values ​​of the dataset sent by the user + * @param DatasetFamily $family Contains the dataset family doctrine object + */ + private function editDataset(Dataset $dataset, array $parsedBody, DatasetFamily $family): void + { + $dataset->setLabel($parsedBody['label']); + $dataset->setDescription($parsedBody['description']); + $dataset->setDatasetFamily($family); + $dataset->setDisplay($parsedBody['display']); + $dataset->setCount($parsedBody['count']); + $dataset->setVo($parsedBody['vo']); + $dataset->setDataPath($parsedBody['data_path']); + $dataset->setSelectableRow($parsedBody['selectable_row']); + $this->em->flush(); + } +} diff --git a/src/Action/DatasetListAction.php b/src/Action/DatasetListAction.php new file mode 100644 index 0000000..fe022a2 --- /dev/null +++ b/src/Action/DatasetListAction.php @@ -0,0 +1,178 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Doctrine\ORM\EntityManagerInterface; +use App\Utils\DBALConnectionFactory; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; +use App\Entity\Attribute; + +final class DatasetListAction extends AbstractAction +{ + private $connectionFactory; + + /** + * Create the classe before call __invoke to execute the action + * + * @param EntityManagerInterface $em Doctrine Entity Manager Interface + * @param DBALConnectionFactory $connectionFactory Factory used to construct connection to business database + */ + public function __construct(EntityManagerInterface $em, DBALConnectionFactory $connectionFactory) + { + parent::__construct($em); + $this->connectionFactory = $connectionFactory; + } + + /** + * `GET` Returns a list of all datasets listed in the metamodel database + * `POST` Add a new dataset + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + if ($request->getMethod() === GET) { + $datasets = $this->em->getRepository('App\Entity\Dataset')->findAll(); + $payload = json_encode($datasets); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // If mandatories empty fields 400 + $fields = array( + 'name', + 'table_ref', + 'label', + 'description', + 'display', + 'count', + 'vo', + 'data_path', + 'selectable_row', + 'project_name', + 'id_dataset_family' + ); + foreach ($fields as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new dataset' + ); + } + } + + // Project is mandatory to add a new dataset + $project = $this->em->find('App\Entity\Project', $parsedBody['project_name']); + if (is_null($project)) { + throw new HttpBadRequestException( + $request, + 'Project with name ' . $parsedBody['project_name'] . ' is not found' + ); + } + + $family = $this->em->find('App\Entity\DatasetFamily', $parsedBody['id_dataset_family']); + if (is_null($family)) { + throw new HttpBadRequestException( + $request, + 'Dataset family with id ' . $parsedBody['id_dataset_family'] . ' is not found' + ); + } + + $dataset = $this->postDataset($parsedBody, $project, $family); + $payload = json_encode($dataset); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Create a new dataset doctrine object and save it + * + * @param array $parsedBody Contains the values ​​of the new dataset sent by the user + * @param Project $project Contains the project doctrine object + * @param DatasetFamily $datasetFamily Contains the dataset family doctrine object + * + * @return Dataset + */ + private function postDataset(array $parsedBody, Project $project, DatasetFamily $family): Dataset + { + $dataset = new Dataset($parsedBody['name']); + $dataset->setTableRef($parsedBody['table_ref']); + $dataset->setLabel($parsedBody['label']); + $dataset->setDescription($parsedBody['description']); + $dataset->setDisplay($parsedBody['display']); + $dataset->setCount($parsedBody['count']); + $dataset->setVo($parsedBody['vo']); + $dataset->setDataPath($parsedBody['data_path']); + $dataset->setSelectableRow($parsedBody['selectable_row']); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + + $this->em->persist($dataset); + $this->postAttributes($dataset); + $this->em->flush(); + + return $dataset; + } + + /** + * Access to the business database and get all the columns of the table (table_ref) + * and makes the corresponding doctrine attribute objects + * + * @param Dataset $dataset Contains the new dataset doctrine object + */ + private function postAttributes(Dataset $dataset): void + { + $database = $dataset->getProject()->getDatabase(); + $connection = $this->connectionFactory->create($database); + $sm = $connection->getSchemaManager(); + $columns = $sm->listTableColumns($dataset->getTableRef()); + $i = 10; + $id = 1; + foreach ($columns as $column) { + $columnName = $column->getName(); + $columnType = $column->getType()->getName(); + $attribute = new Attribute($id, $dataset); + $attribute->setName($columnName); + $attribute->setTableName($dataset->getTableRef()); + $attribute->setLabel($columnName); + $attribute->setFormLabel($columnName); + $attribute->setType($columnType); + $attribute->setCriteriaDisplay($i); + $attribute->setOutputDisplay($i); + $attribute->setDisplayDetail($i); + $attribute->setOrderDisplay($i); + $attribute->setSelected(true); + $this->em->persist($attribute); + $i = $i + 10; + $id++; + } + } +} diff --git a/src/Action/FamilyAction.php b/src/Action/FamilyAction.php new file mode 100644 index 0000000..6201b2e --- /dev/null +++ b/src/Action/FamilyAction.php @@ -0,0 +1,103 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Family; + +final class FamilyAction extends AbstractAction +{ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + $type = $this->verifyType($args['type']); + if (empty($type)) { + throw new HttpBadRequestException( + $request, + 'Type ' . $args['type'] . ' is not defined' + ); + } + + $entityClass = $this->getEntityClass($type); + $family = $this->em->find($entityClass, $args['id']); + if (is_null($family)) { + throw new HttpNotFoundException( + $request, + ucfirst($type) . ' family with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($family); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + // Vérification des champs vides + $fields = array('label', 'display'); + foreach ($fields as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the ' . $type . ' family' + ); + } + } + + $this->editFamily($family, $parsedBody); + $payload = json_encode($family); + } + + if ($request->getMethod() === DELETE) { + $id = $family->getId(); + $this->em->remove($family); + $this->em->flush(); + $payload = json_encode(array( + 'message' => ucfirst($type) . ' family with id ' . $id . ' is removed!' + )); + } + + $response->getBody()->write($payload); + return $response; + } + + private function verifyType(string $type): string + { + $t = strtolower($type); + + if ($t == 'dataset' || $t == 'output' || $t == 'criteria') { + return $t; + } else { + return ''; + } + } + + private function getEntityClass(string $type): string + { + return 'App\Entity\\' . ucfirst($type) . 'Family'; + } + + private function editFamily(object $family, array $parsedBody): void + { + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + $this->em->flush(); + } +} diff --git a/src/Action/FamilyListAction.php b/src/Action/FamilyListAction.php new file mode 100644 index 0000000..8a81b59 --- /dev/null +++ b/src/Action/FamilyListAction.php @@ -0,0 +1,100 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use App\Entity\Family; + +final class FamilyListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all families by type listed in the metamodel database + * `POST` Add a new family + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + $type = $this->verifyType($args['type']); + if (empty($type)) { + throw new HttpBadRequestException( + $request, + 'Type ' . $args['type'] . ' is not defined' + ); + } + + if ($request->getMethod() === GET) { + $families = $this->em->getRepository($this->getEntityClass($type))->findAll(); + $payload = json_encode($families); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs information to update + foreach (array('label', 'display') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new family' + ); + } + } + + $family = $this->postFamily($parsedBody, $this->getEntityClass($type)); + $payload = json_encode($family); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + private function verifyType(string $type): string + { + $t = strtolower($type); + + if ($t == 'dataset' || $t == 'output' || $t == 'criteria') { + return $t; + } else { + return ''; + } + } + + private function getEntityClass(string $type): string + { + return 'App\Entity\\' . ucfirst($type) . 'Family'; + } + + private function postFamily(array $parsedBody, string $class): object + { + $family = new $class(); + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + + $this->em->persist($family); + $this->em->flush(); + + return $family; + } +} diff --git a/src/Action/Login/ActivateAccountAction.php b/src/Action/Login/ActivateAccountAction.php deleted file mode 100644 index b55fbb2..0000000 --- a/src/Action/Login/ActivateAccountAction.php +++ /dev/null @@ -1,168 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Login; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Swift_Mailer; -use Swift_Message; - -use App\Utils\ActionTrait; -use App\Entity\Admin\User; - -/** - * Route: /login/activate-account - * - * With this action, the newly registered user be able - * to send the activation code of his new account. (GET) - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Login - */ -final class ActivateAccountAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The EntityManager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var EntityManagerInterface - */ - private $aem; - - /** - * Swift Mailer class is used here to send notification by email - * - * @var Swift_Mailer - */ - private $mailer; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param EntityManagerInterface $em - * @param Swift_Mailer $mailer - */ - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem, Swift_Mailer $mailer) - { - $this->logger = $logger; - $this->aem = $aem; - $this->mailer = $mailer; - } - - /** - * This action activate the new user Anis account - * Data is read into the http get (email + activation_key) - * This action send a message to warn the user and returns http response - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Activate account action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - if ($request->isGet()) { - $queryParams = $request->getQueryParams(); - - // To work this action needs email and activation_key of the new anis user account - foreach (array('email', 'activation_key') as $a) { - if ($this->isEmptyField($a, $queryParams)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to activate a new user account' - ); - } - } - - // Is the user exists ? - if (!$this->isExistUser($queryParams['email'])) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'No account is identified with this email address' - ); - } - - $user = $this->aem->find('App\Entity\Admin\User', $queryParams['email']); - - // Is the activation key is the good one ? - if ($user->getActivationKey() !== $queryParams['activation_key']) { - return $this->dispatchHttpError( - $response, - 'Invalid activation key', - 'Bad activation key; Unable to activate account' - ); - } - - // It's all right. Account activated - $user->setActivated(true); - $this->aem->flush(); - - // Send an email confirmation for the user and returns user modified - $this->sendEmail($user->getEmail()); - return $response->withJson($user); - } - } - - /** - * @param string $email - * - * @return bool - */ - private function isExistUser(string $email): bool - { - $user = $this->aem->getRepository('App\Entity\Admin\User')->findOneBy(array('email' => $email)); - if (isset($user)) { - return true; - } else { - return false; - } - } - - /** - * @param string $email - */ - private function sendEmail(string $email): void - { - $body = 'Dear user ' . PHP_EOL; - $body .= PHP_EOL; - $body .= 'Your account is now activated.' . PHP_EOL; - $body .= PHP_EOL; - $body .= 'Best regards' . PHP_EOL; - $body .= 'Anis Team'; - - $message = new Swift_Message('ANIS registration confirmation'); - $message->setFrom(['noreply@anis.fr' => 'ANIS']); - $message->setTo([$email]); - $message->setBody($body); - - $this->mailer->send($message); - } -} diff --git a/src/Action/Login/ChangePasswordAction.php b/src/Action/Login/ChangePasswordAction.php deleted file mode 100644 index 55b21ee..0000000 --- a/src/Action/Login/ChangePasswordAction.php +++ /dev/null @@ -1,173 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Login; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Swift_Mailer; -use Swift_Message; - -use App\Utils\ActionTrait; - -/** - * Route: /login/change-password - * - * With this action the anis user can change his password (POST) - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Login - */ -final class ChangePasswordAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The EntityManager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var EntityManagerInterface - */ - private $aem; - - /** - * Swift Mailer class is used here to send notification by email - * - * @var Swift_Mailer - */ - private $mailer; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param EntityManagerInterface $aem - * @param Swift_Mailer $mailer - */ - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem, Swift_Mailer $mailer) - { - $this->logger = $logger; - $this->aem = $aem; - $this->mailer = $mailer; - } - - /** - * This action change the anis password by a newer (new_password) - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Change password action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // To work this action needs email password and new_password - foreach (array('email', 'password', 'new_password') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to change your password' - ); - } - } - - // Is the user exists ? - if (!$this->isExistUser($parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'No account is identified with this email address' - ); - } - - $user = $this->aem->find('App\Entity\Admin\User', $parsedBody['email']); - - // Is the user account is activated ? - if (!$user->getActivated()) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'Account not yet activated' - ); - } - - // Check the couple email + password - if (!password_verify($parsedBody['password'], $user->getPassword())) { - return $this->dispatchHttpError( - $response, - 'Invalid password', - 'Bad password; unable to change the password' - ); - } - - // It's all right ! change the password by the new one - $user->setPassword(password_hash($parsedBody['new_password'], PASSWORD_DEFAULT)); - $this->aem->flush(); - - // Send a email confirmation for the user and returns user modified - $this->sendEmail($user->getEmail()); - return $response->withJson(array('message' => 'Password changed!')); - } - } - - /** - * @param string $email - * - * @return bool - */ - private function isExistUser(array $parsedBody): bool - { - $user = $this->aem->getRepository('App\Entity\Admin\User')->findOneBy(array('email' => $parsedBody['email'])); - if (isset($user)) { - return true; - } else { - return false; - } - } - - /** - * @param string $email - */ - private function sendEmail(string $email): void - { - $body = 'Dear user ' . PHP_EOL; - $body .= PHP_EOL; - $body .= 'Your new password is now active.' . PHP_EOL; - $body .= PHP_EOL; - $body .= 'Best regards' . PHP_EOL; - $body .= 'Anis Team'; - - $message = new \Swift_Message('ANIS confirm password change'); - $message->setFrom(['noreply@anis.fr' => 'ANIS']); - $message->setTo([$email]); - $message->setBody($body); - - $this->mailer->send($message); - } -} diff --git a/src/Action/Login/NewPasswordAction.php b/src/Action/Login/NewPasswordAction.php deleted file mode 100644 index 4b117b3..0000000 --- a/src/Action/Login/NewPasswordAction.php +++ /dev/null @@ -1,165 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Login; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Swift_Mailer; - -use App\Utils\ActionTrait; - -/** - * Route: /login/new-password - * - * This action asks for a new password - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Login - */ -final class NewPasswordAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The EntityManager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var EntityManagerInterface - */ - private $aem; - - /** - * Swift Mailer class is used here to send notification by email - * - * @var Swift_Mailer - */ - private $mailer; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param EntityManagerInterface $aem - * @param Swift_Mailer $mailer - */ - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem, Swift_Mailer $mailer) - { - $this->logger = $logger; - $this->aem = $aem; - $this->mailer = $mailer; - } - - /** - * This action asks for a new password (forgit password) - * This action needs email address to send a new generated password - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('New password action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // To work this action needs email - if ($this->isEmptyField('email', $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param email needed to generate a new password' - ); - } - - // Is the user exists ? - if (!$this->isExistUser($parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'No account is identified with this email address' - ); - } - - $user = $this->aem->find('App\Entity\Admin\User', $parsedBody['email']); - - // Is the user account is activated ? - if (!$user->getActivated()) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'Account not yet activated' - ); - } - - // It'as all right ! Password generation write database - $pwd = uniqid(); - $hash = password_hash($pwd, PASSWORD_DEFAULT); - $user->setPassword($hash); - $this->aem->flush(); - - // Send a confirmation email with new generated password - $this->sendEmail($user->getEmail(), $pwd); - return $response->withJson(array('message' => 'Password re-generated!')); - } - } - - /** - * @param string $email - * - * @return bool - */ - private function isExistUser(array $parsedBody): bool - { - $user = $this->aem->getRepository('App\Entity\Admin\User')->findOneBy(array('email' => $parsedBody['email'])); - if (isset($user)) { - return true; - } else { - return false; - } - } - - /** - * @param string $email - */ - private function sendEmail(string $email, string $newPassword): void - { - $body = 'Dear user ' . PHP_EOL; - $body .= PHP_EOL; - $body .= 'A new password has been generated for you.' . PHP_EOL; - $body .= 'New password: ' . $newPassword . PHP_EOL; - $body .= PHP_EOL; - $body .= 'Best regards' . PHP_EOL; - $body .= 'Anis Team'; - - $message = new \Swift_Message('ANIS new password'); - $message->setFrom(['noreply@anis.fr' => 'ANIS']); - $message->setTo([$email]); - $message->setBody($body); - - $this->mailer->send($message); - } -} diff --git a/src/Action/Login/RegisterAction.php b/src/Action/Login/RegisterAction.php deleted file mode 100644 index 84a6096..0000000 --- a/src/Action/Login/RegisterAction.php +++ /dev/null @@ -1,207 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Login; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Swift_Mailer; -use Swift_Message; - -use App\Utils\ActionTrait; -use App\Entity\Admin\User; - -/** - * Route: /login/register - * - * An action to saves a new user Anis (POST) - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Login - */ -final class RegisterAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The EntityManager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var EntityManagerInterface - */ - private $aem; - - /** - * Swift Mailer class is used here to send notification by email - * - * @var Swift_Mailer - */ - private $mailer; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param EntityManagerInterface $aem - * @param Swift_Mailer $mailer - */ - public function __construct(LoggerInterface $logger, EntityManagerInterface $aem, Swift_Mailer $mailer) - { - $this->logger = $logger; - $this->aem = $aem; - $this->mailer = $mailer; - } - - /** - * This action register a new user Anis - * Data is read into the http post body ($parsedBody => email, password) - * This action send a message to warn the user and returns http response - * This action returns a code 201 and the user in json format - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Register action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // To work this action needs email and password of the new anis user - foreach (array('email', 'password') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to register a new user' - ); - } - } - - // The email address must be unique in the database - if ($this->isExistUser($parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'A user with the email address ' . $parsedBody['email'] . ' already exists' - ); - } - - // The email address must be valid (well-formed) - if (!$this->isVerifEmailOk($parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid email', - 'Bad email adress; a well-defined email address is needed to finalize the registration' - ); - } - - // Add the user in the database => anis_user table - $user = $this->createUser($parsedBody); - - // Send a confirmation email to the new anis user - $this->sendEmail($request, $user); - return $response->withJson($user, 201); - } - } - - /** - * @param array $parsedBody - * - * @return bool - */ - private function isExistUser(array $parsedBody): bool - { - $user = $this->aem->getRepository('App\Entity\Admin\User')->findOneBy(array('email' => $parsedBody['email'])); - if (isset($user)) { - return true; - } else { - return false; - } - } - - /** - * @param array $parsedBody - * - * @return bool - */ - private function isVerifEmailOk(array $parsedBody): bool - { - if (filter_var($parsedBody['email'], FILTER_VALIDATE_EMAIL)) { - return true; - } else { - return false; - } - } - - /** - * @param array $parsedBody - * - * @return User - */ - private function createUser(array $parsedBody): User - { - $user = new User($parsedBody['email']); - - $user->setPassword(password_hash($parsedBody['password'], PASSWORD_DEFAULT)); - $user->setAdminsi(false); - $user->setSuperuser(false); - $user->setActivationKey(uniqid('', true)); // Clé activation pour verification email - $user->setActivated(false); // L'utilisateur n'est pas actif avant verification email - $this->aem->persist($user); - $this->aem->flush(); - - return $user; - } - - /** - * @param Request $request This object contains the HTTP request - * @param User $user The new anis user created - */ - private function sendEmail(Request $request, User $user): void - { - $url = $request->getUri()->getScheme() . '://' . $request->getUri()->getHost(); - if ($request->getUri()->getPort() != 80) { - $url .= ':' . $request->getUri()->getPort(); - } - $url .= '/activate-account?email=' . $user->getEmail() . '&activation_key=' . $user->getActivationKey(); - - $body = 'Dear user ' . PHP_EOL; - $body .= PHP_EOL; - $body .= 'To activate your account, please click on the link below : ' . PHP_EOL; - $body .= $url . PHP_EOL; - $body .= PHP_EOL; - $body .= 'Best regards' . PHP_EOL; - $body .= 'Anis Team'; - - $message = new Swift_Message('ANIS registration confirmation'); - $message->setFrom(['noreply@anis.fr' => 'ANIS']); - $message->setTo([$user->getEmail()]); - $message->setBody($body); - - $this->mailer->send($message); - } -} diff --git a/src/Action/Login/TokenAction.php b/src/Action/Login/TokenAction.php deleted file mode 100644 index 9467b8c..0000000 --- a/src/Action/Login/TokenAction.php +++ /dev/null @@ -1,182 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Login; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Lcobucci\JWT\Builder as JwtBuilder; -use Lcobucci\JWT\Signer as JwtSigner; -use Lcobucci\JWT\Token; - -use App\Utils\ActionTrait; - -/** - * Route: /login/token - * - * Get a valid token (jwt) for a anis user (POST) - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Login - */ -final class TokenAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The EntityManager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var EntityManagerInterface - */ - private $aem; - - /** - * This class makes easier the token creation process (JWT) - * - * @var Lcobucci\JWT\Builder - */ - private $jwtBuilder; - - /** - * Basic interface for token signers - * - * @var Lcobucci\JWT\Signer - */ - private $jwtSigner; - - /** - * This class is creates before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param EntityManagerInterface $aem - * @param Lcobucci\JWT\Builder $jwtBuilder - * @param Lcobucci\JWT\Signer $jwtSigner - */ - public function __construct( - LoggerInterface $logger, - EntityManagerInterface $aem, - JwtBuilder $jwtBuilder, - JwtSigner $jwtSigner - ) { - $this->logger = $logger; - $this->aem = $aem; - $this->jwtBuilder = $jwtBuilder; - $this->jwtSigner = $jwtSigner; - } - - /** - * This action generated a new Json Web Token - * This action needs the couple email an password to authentify the anis user - * This action returns the new jwt signed and generated - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Login action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // To work this action needs email and password of the anis user - foreach (array('email', 'password') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to login' - ); - } - } - - // Is the user exists ? - if (!$this->isExistUser($parsedBody['email'])) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'No account is identified with this email address' - ); - } - - $user = $this->aem->find('\App\Entity\Admin\User', $parsedBody['email']); - - // Is the user activated ? - if (!$user->getActivated()) { - return $this->dispatchHttpError( - $response, - 'Invalid user', - 'Account not yet activated' - ); - } - - // Is the password is ok ? - if (!password_verify($parsedBody['password'], $user->getPassword())) { - return $this->dispatchHttpError( - $response, - 'Invalid password', - 'Bad password; unable to login' - ); - } - - // // It's all right. JWT generation - $token = $this->generateToken($user->getEmail()); - $data = array('email' => $user->getEmail(), 'token' => (string) $token); - return $response->withJson($data); - } - } - - /** - * @param string $email - * - * @return bool - */ - private function isExistUser(string $email): bool - { - $user = $this->aem->getRepository('App\Entity\Admin\User')->findOneBy(array('email' => $email)); - if (isset($user)) { - return true; - } else { - return false; - } - } - - /** - * Returns the new signed token for an anis user - * - * @param string $email - * - * @return Token - */ - private function generateToken(string $email): Token - { - $token = $this->jwtBuilder - ->set('email', $email) // Ajout de l'adresse email du participant au jeton - ->sign($this->jwtSigner, 'testing') // Fabrique une signature avec clé - ->getToken(); // Retourne le jeton généré - return $token; - } -} diff --git a/src/Action/Meta/AttributeListAction.php b/src/Action/Meta/AttributeListAction.php deleted file mode 100644 index e89fd7e..0000000 --- a/src/Action/Meta/AttributeListAction.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Dataset; -use App\Entity\Metamodel\Attribute; -use App\Entity\Metamodel\CriteriaFamily; -use App\Entity\Metamodel\OutputCategory; - -/** - * Route: /metadata/dataset/{name}/attribute/ - * {name}: Dataset name - * - * This action returns a list of all attributes in a dataset - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class AttributeListAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param MetaEntityManagerFactory $memf - */ - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - /** - * `GET` Returns all attributes in the dataset selected - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Attribute list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - $dataset = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Dataset', $args['name']); - - // Returns HTTP 404 if the dataset is not found - if (is_null($dataset)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset with name ' . $args['name'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $attributes = array(); - foreach ($dataset->getAttributes() as $a) { - $attributes[] = $a; - } - $newResponse = $response->withJson($attributes); - } - - return $newResponse; - } -} diff --git a/src/Action/Meta/DatabaseAction.php b/src/Action/Meta/DatabaseAction.php deleted file mode 100644 index 8edc407..0000000 --- a/src/Action/Meta/DatabaseAction.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Database; - -/** - * Route: /metadata/database/{id} - * {id}: Database id - * - * This action is used to manage one database - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class DatabaseAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * The key needed to encrypt the password of the database - * - * @var string - */ - private $encryptionKey; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param EntityManagerInterface $memf - * @param string $encryptionKey - */ - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf, string $encryptionKey) - { - $this->logger = $logger; - $this->memf = $memf; - $this->encryptionKey = $encryptionKey; - } - - /** - * `GET` Returns the database found - * `PUT` Full update the database and returns the new version - * `DELETE` Delete the database found and return a confirmation message - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Database action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $this->memf->createMetaEntityManager($args['instance']); - - // Search the correct database with primary key - $database = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Database', $args['id']); - - // If database is not found 404 - if (is_null($database)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Database with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($database); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // If mandatories empty fields 400 - foreach (array('label', 'dbname', 'dbtype', 'dbhost', 'dbport', 'dblogin', 'dbpassword') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the database' - ); - } - } - - $this->editDatabase($database, $parsedBody); - $newResponse = $response->withJson($database); - } - - if ($request->isDelete()) { - $id = $database->getId(); - $this->memf->getMetaEntityManager()->remove($database); - $this->memf->getMetaEntityManager()->flush(); - $newResponse = $response->withJson(array('message' => 'Database with id ' . $id . ' is removed!')); - } - - return $newResponse; - } - - /** - * Update database object with setters - * - * @param Database $database The database to update - * @param string[] $parsedBody Contains the new values ​​of the database sent by the user - */ - private function editDatabase(Database $database, array $parsedBody): void - { - $database->setLabel($parsedBody['label']); - $database->setDbName($parsedBody['dbname']); - $database->setType($parsedBody['dbtype']); - $database->setHost($parsedBody['dbhost']); - $database->setPort($parsedBody['dbport']); - $database->setLogin($parsedBody['dblogin']); - $database->setPassword($this->encryptData($parsedBody['dbpassword'])); - $this->memf->getMetaEntityManager()->flush(); - } -} diff --git a/src/Action/Meta/DatabaseListAction.php b/src/Action/Meta/DatabaseListAction.php deleted file mode 100644 index d68be7f..0000000 --- a/src/Action/Meta/DatabaseListAction.php +++ /dev/null @@ -1,137 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Database; - -/** - * Route: /metadata/database - * - * This action returns a list of all databases listed in the metamodel database - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class DatabaseListAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * The key needed to encrypt the password of the database - * - * @var string - */ - private $encryptionKey; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param EntityManagerInterface $memf - * @param string $encryptionKey - */ - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf, string $encryptionKey) - { - $this->logger = $logger; - $this->memf = $memf; - $this->encryptionKey = $encryptionKey; - } - - /** - * `GET` Returns a list of all databases listed in the metamodel database - * `POST` Add a new database - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Database list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - $this->memf->createMetaEntityManager($args['instance']); - - if ($request->isGet()) { - $databases = $this->memf->getMetaEntityManager()->getRepository('App\Entity\Metamodel\Database')->findAll(); - $newResponse = $response->withJson($databases); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // If mandatories empty fields 400 - foreach (array('label', 'dbname', 'dbtype', 'dbhost', 'dbport', 'dblogin', 'dbpassword') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new database' - ); - } - } - - $database = $this->postDatabase($parsedBody); - $newResponse = $response->withJson($database, 201); - } - - return $newResponse; - } - - /** - * Add a new database into the metamodel - * - * @param string[] $parsedBody Contains the values ​​of the new database sent by the user - */ - private function postDatabase(array $parsedBody): Database - { - $database = new Database(); - $database->setLabel($parsedBody['label']); - $database->setDbName($parsedBody['dbname']); - $database->setType($parsedBody['dbtype']); - $database->setHost($parsedBody['dbhost']); - $database->setPort($parsedBody['dbport']); - $database->setLogin($parsedBody['dblogin']); - $database->setPassword($this->encryptData($parsedBody['dbpassword'])); - - $this->memf->getMetaEntityManager()->persist($database); - $this->memf->getMetaEntityManager()->flush(); - - return $database; - } -} diff --git a/src/Action/Meta/DatasetAction.php b/src/Action/Meta/DatasetAction.php deleted file mode 100644 index b36b42c..0000000 --- a/src/Action/Meta/DatasetAction.php +++ /dev/null @@ -1,173 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Dataset; -use App\Entity\Metamodel\DatasetFamily; - -/** - * Route: /metadata/dataset/{name} - * {name}: Dataset name - * - * This action is used to manage one dataset - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class DatasetAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param MetaEntityManagerFactory $memf - */ - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - /** - * `GET` Returns the dataset found - * `PUT` Full update the dataset and returns the new version - * `DELETE` Delete the dataset found and return a confirmation message - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Dataset action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - // Search the correct dataset with primary key - $dataset = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Dataset', $args['name']); - - // If dataset is not found 404 - if (is_null($dataset)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset with name ' . $args['name'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($dataset); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // If mandatories empty fields 400 - $fields = array( - 'label', - 'description', - 'display', - 'count', - 'vo', - 'data_path', - 'selectable_row', - 'id_dataset_family' - ); - foreach ($fields as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the dataset' - ); - } - } - - // Search the correct dataset family with primary key - $family = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\DatasetFamily', - $parsedBody['id_dataset_family'] - ); - - // Dataset family is mandatory. If is null 404 - if (is_null($family)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset family with id ' . $parsedBody['id_dataset_family'] . ' is not found' - )->withStatus(404); - } - - $this->editDataset($dataset, $parsedBody, $family); - $newResponse = $response->withJson($dataset); - } - - if ($request->isDelete()) { - $name = $dataset->getName(); - $this->memf->getMetaEntityManager()->remove($dataset); - $this->memf->getMetaEntityManager()->flush(); - $newResponse = $response->withJson(array('message' => 'Dataset with name ' . $name . ' is removed!')); - } - - return $newResponse; - } - - /** - * Update dataset object with setters - * - * @param Dataset $dataset The dataset to update - * @param string[] $parsedBody Contains the new values ​​of the dataset sent by the user - * @param DatasetFamily $family Contains the dataset family doctrine object - */ - private function editDataset(Dataset $dataset, array $parsedBody, DatasetFamily $family): void - { - $dataset->setLabel($parsedBody['label']); - $dataset->setDescription($parsedBody['description']); - $dataset->setDatasetFamily($family); - $dataset->setDisplay($parsedBody['display']); - $dataset->setCount($parsedBody['count']); - $dataset->setVo($parsedBody['vo']); - $dataset->setDataPath($parsedBody['data_path']); - $dataset->setSelectableRow($parsedBody['selectable_row']); - - $this->memf->getMetaEntityManager()->flush(); - } -} diff --git a/src/Action/Meta/DatasetListAction.php b/src/Action/Meta/DatasetListAction.php deleted file mode 100644 index 42af7ef..0000000 --- a/src/Action/Meta/DatasetListAction.php +++ /dev/null @@ -1,240 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\DBALConnectionFactory; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Dataset; -use App\Entity\Metamodel\Attribute; -use App\Entity\Metamodel\Project; -use App\Entity\Metamodel\DatasetFamily; - -/** - * Route: /metadata/dataset - * - * This action is used to: - * - Get all datasets - * - Add a new dataset - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class DatasetListAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * Factory that provides a connection to the business database - * - * @var DBALConnectionFactory - */ - private $dcf; - - /** - * The key needed to decrypt the password of the database - * - * @var string - */ - private $encryptionKey; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param MetaEntityManagerFactory $memf - * @param DBALConnectionFactory $dcf - * @param string $encryptionKey - */ - public function __construct( - LoggerInterface $logger, - MetaEntityManagerFactory $memf, - DBALConnectionFactory $dcf, - string $encryptionKey - ) { - $this->logger = $logger; - $this->memf = $memf; - $this->dcf = $dcf; - $this->encryptionKey = $encryptionKey; - } - - /** - * `GET` Returns the list of datasets - * `POST` Add a new dataset. The program will search the business database for - * the columns of the table (table_ref) to transform them into attributes - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Dataset list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - if ($request->isGet()) { - $datasets = $this->memf->getMetaEntityManager()->getRepository('App\Entity\Metamodel\Dataset')->findBy( - array(), - array('display' => 'ASC') - ); - $newResponse = $response->withJson($datasets); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // If mandatories empty fields 400 - $fields = array( - 'name', - 'table_ref', - 'label', - 'description', - 'display', - 'count', - 'vo', - 'data_path', - 'selectable_row', - 'project_name', - 'id_dataset_family' - ); - foreach ($fields as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new dataset' - ); - } - } - - $project = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\Project', - $parsedBody['project_name'] - ); - if (is_null($project)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Project with name ' . $parsedBody['project_name'] . ' is not found' - )->withStatus(404); - } - - $family = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\DatasetFamily', - $parsedBody['id_dataset_family'] - ); - if (is_null($family)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset family with id ' . $parsedBody['id_dataset_family'] . ' is not found' - )->withStatus(404); - } - - $dataset = $this->postDataset($parsedBody, $project, $family); - $newResponse = $response->withJson($dataset); - } - - return $newResponse; - } - - /** - * Create a new dataset doctrine object and save it - * - * @param string[] $parsedBody Contains the values ​​of the new dataset sent by the user - * @param Project $project Contains the project doctrine object - * @param DatasetFamily $datasetFamily Contains the dataset family doctrine object - * - * @return Dataset - */ - private function postDataset(array $parsedBody, Project $project, DatasetFamily $family): Dataset - { - $dataset = new Dataset($parsedBody['name']); - $dataset->setTableRef($parsedBody['table_ref']); - $dataset->setLabel($parsedBody['label']); - $dataset->setDescription($parsedBody['description']); - $dataset->setDisplay($parsedBody['display']); - $dataset->setCount($parsedBody['count']); - $dataset->setVo($parsedBody['vo']); - $dataset->setDataPath($parsedBody['data_path']); - $dataset->setSelectableRow($parsedBody['selectable_row']); - $dataset->setProject($project); - $dataset->setDatasetFamily($family); - - $this->memf->getMetaEntityManager()->persist($dataset); - $this->postAttributes($dataset); - $this->memf->getMetaEntityManager()->flush(); - - return $dataset; - } - - /** - * Access to the business database dans get all the columns of the table (table_ref) - * and makes the corresponding doctrine attribute objects - * - * @param Dataset $dataset Contains the new dataset doctrine object - */ - private function postAttributes(Dataset $dataset): void - { - $database = $dataset->getProject()->getDatabase(); - $decryptedPassword = $this->decryptData($database->getPassword()); - $connection = $this->dcf->create($database, $decryptedPassword); - $sm = $connection->getSchemaManager(); - $columns = $sm->listTableColumns($dataset->getTableRef()); - $i = 10; - $id = 1; - foreach ($columns as $column) { - $columnName = $column->getName(); - $columnType = $column->getType()->getName(); - $attribute = new Attribute($id, $dataset); - $attribute->setName($columnName); - $attribute->setTableName($dataset->getTableRef()); - $attribute->setLabel($columnName); - $attribute->setFormLabel($columnName); - $attribute->setType($columnType); - $attribute->setCriteriaDisplay($i); - $attribute->setOutputDisplay($i); - $attribute->setDisplayDetail($i); - $attribute->setOrderDisplay($i); - $attribute->setSelected(true); - $this->memf->getMetaEntityManager()->persist($attribute); - $i = $i + 10; - $id++; - } - } -} diff --git a/src/Action/Meta/FamilyAction.php b/src/Action/Meta/FamilyAction.php deleted file mode 100644 index f0f5172..0000000 --- a/src/Action/Meta/FamilyAction.php +++ /dev/null @@ -1,133 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; - -final class FamilyAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Family action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $type = $this->verifyType($args['type']); - - if (empty($type)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Type ' . $args['type'] . ' is not defined' - ); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - $entityClass = $this->getEntityClass($type); - $family = $this->memf->getMetaEntityManager()->find($entityClass, $args['id']); - - if (is_null($family)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - ucfirst($type) . ' family with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($family); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - $fields = array('label', 'display'); - foreach ($fields as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the ' . $type . ' family' - ); - } - } - - $this->editFamily($family, $parsedBody); - $newResponse = $response->withJson($family); - } - - if ($request->isDelete()) { - $id = $family->getId(); - $this->memf->getMetaEntityManager()->remove($family); - $this->memf->getMetaEntityManager()->flush(); - $newResponse = $response->withJson(array( - 'message' => ucfirst($type) . ' family with id ' . $id . ' is removed!' - )); - } - - return $newResponse; - } - - private function getType(object $family): string - { - if ($family instanceof \App\Entity\Metamodel\DatasetFamily) { - return 'dataset'; - } elseif ($family instanceof \App\Entity\Metamodel\CriteriaFamily) { - return 'criteria'; - } else { - return 'output'; - } - } - - private function verifyType(string $type): string - { - $t = strtolower($type); - - if ($t == 'dataset' || $t == 'output' || $t == 'criteria') { - return $t; - } else { - return ''; - } - } - - private function getEntityClass(string $type): string - { - return 'App\Entity\Metamodel\\' . ucfirst($type) . 'Family'; - } - - private function editFamily(Object $family, array $parsedBody): void - { - $family->setLabel($parsedBody['label']); - $family->setDisplay($parsedBody['display']); - $this->memf->getMetaEntityManager()->flush(); - } -} diff --git a/src/Action/Meta/FamilyListAction.php b/src/Action/Meta/FamilyListAction.php deleted file mode 100644 index 8237225..0000000 --- a/src/Action/Meta/FamilyListAction.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; - -final class FamilyListAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Family list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - $type = $this->verifyType($args['type']); - - if (empty($type)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Type ' . $args['type'] . ' is not defined' - ); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - if ($request->isGet()) { - $sql = 'SELECT f FROM ' . $this->getEntityClass($type) . ' f'; - if ($type == 'dataset') { - $sql .= ' WHERE f.display > 0'; - } - $families = $this->memf->getMetaEntityManager()->createQuery($sql)->getResult(); - $newResponse = $response->withJson($families); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - $fields = array('label', 'display'); - foreach ($fields as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new family' - ); - } - } - - $family = $this->postFamily($parsedBody, $this->getEntityClass($type)); - $newResponse = $response->withJson($family, 201); - } - - return $newResponse; - } - - private function verifyType(string $type): string - { - $t = strtolower($type); - - if ($t == 'dataset' || $t == 'output' || $t == 'criteria') { - return $t; - } else { - return ''; - } - } - - private function getEntityClass(string $type): string - { - return 'App\Entity\Metamodel\\' . ucfirst($type) . 'Family'; - } - - private function postFamily(array $parsedBody, string $class): Object - { - $family = new $class; - $family->setLabel($parsedBody['label']); - $family->setDisplay($parsedBody['display']); - - $this->memf->getMetaEntityManager()->persist($family); - $this->memf->getMetaEntityManager()->flush(); - - return $family; - } -} diff --git a/src/Action/Meta/FileAction.php b/src/Action/Meta/FileAction.php deleted file mode 100644 index f1c2c23..0000000 --- a/src/Action/Meta/FileAction.php +++ /dev/null @@ -1,93 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\File; - -final class FileAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('File action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $file = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\File', $args['id']); - - if (is_null($file)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'File with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($file); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('label', 'file_loc', 'type', 'display', 'visible') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the file' - ); - } - } - - $this->editFile($file, $parsedBody); - $newResponse = $response->withJson($file); - } - - if ($request->isDelete()) { - $id = $file->getId(); - $this->memf->getMetaEntityManager()->remove($file); - $this->memf->getMetaEntityManager()->flush(); - $newResponse = $response->withJson(array('message' => 'File with id ' . $id . ' is removed!')); - } - - return $newResponse; - } - - private function editFile(File $file, array $parsedBody): void - { - $file->setLabel($parsedBody['label']); - $file->setFileLoc($parsedBody['file_loc']); - $file->setType($parsedBody['type']); - $file->setDisplay($parsedBody['display']); - $file->setVisible($parsedBody['visible']); - $this->memf->getMetaEntityManager()->flush(); - } -} diff --git a/src/Action/Meta/FileListAction.php b/src/Action/Meta/FileListAction.php deleted file mode 100644 index 39efba3..0000000 --- a/src/Action/Meta/FileListAction.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\File; -use App\Entity\Metamodel\Dataset; - -final class FileListAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('File list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - $dataset = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Dataset', $args['name']); - if (array_key_exists('type', $request->getQueryParams())) { - $type = $request->getQueryParams()['type']; - } - - if (is_null($dataset)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset with name ' . $args['name'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $types = array( - 'dataset' => $dataset - ); - if (isset($type)) { - $types['type'] = $type; - } - $files = $this->memf->getMetaEntityManager()->getRepository('App\Entity\Metamodel\File')->findBy($types); - $newResponse = $response->withJson($files); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('label', 'file_loc', 'type', 'display', 'visible') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new file' - ); - } - } - - $file = $this->postFile($parsedBody, $dataset); - $newResponse = $response->withJson($file, 201); - } - - return $newResponse; - } - - private function postFile(array $parsedBody, Dataset $dataset): File - { - $file = new File($dataset); - $file->setLabel($parsedBody['label']); - $file->setFileLoc($parsedBody['file_loc']); - $file->setType($parsedBody['type']); - $file->setDisplay($parsedBody['display']); - $file->setVisible($parsedBody['visible']); - - $this->memf->getMetaEntityManager()->persist($file); - $this->memf->getMetaEntityManager()->flush(); - - return $file; - } -} diff --git a/src/Action/Meta/FileProxyAction.php b/src/Action/Meta/FileProxyAction.php deleted file mode 100644 index 0fc847a..0000000 --- a/src/Action/Meta/FileProxyAction.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; - -final class FileProxyAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('File proxy action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - $dataset = $this->em->find('App\Entity\Metamodel\Dataset', $args['name']); - if (is_null($dataset)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset with name ' . $args['name'] . ' is not found' - )->withStatus(404); - } - - $path = $args['path']; - - if ($request->isGet()) { - $dataPath = rtrim($dataset->getDataPath(), "\\/" . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - $url = $dataPath . $path; - if (!file_exists($url)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'File with dataset name ' . $args['name'] . ' and path ' . $args['path'] . ' is not found' - )->withStatus(404); - } - $mime = mime_content_type($url); - $newResponse = $response->withHeader('Content-type', $mime)->write(file_get_contents($url)); - } - - return $newResponse; - } -} diff --git a/src/Action/Meta/GroupAction.php b/src/Action/Meta/GroupAction.php deleted file mode 100644 index 1f699bc..0000000 --- a/src/Action/Meta/GroupAction.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Group; - -final class GroupAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Group action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - $group = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Group', $args['id']); - - if (is_null($group)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Group with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($group); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('label') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the group' - ); - } - } - - $this->editGroup($group, $parsedBody); - $newResponse = $response->withJson($group); - } - - if ($request->isDelete()) { - $id = $group->getId(); - $this->memf->getMetaEntityManager()->remove($group); - $this->memf->getMetaEntityManager()->flush(); - $newResponse = $response->withJson(array('message' => 'Group with id ' . $id . ' is removed!')); - } - - return $newResponse; - } - - private function editGroup(Group $group, array $parsedBody): void - { - $group->setLabel($parsedBody['label']); - $this->memf->getMetaEntityManager()->flush(); - } -} diff --git a/src/Action/Meta/GroupListAction.php b/src/Action/Meta/GroupListAction.php deleted file mode 100644 index c5f2f61..0000000 --- a/src/Action/Meta/GroupListAction.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Group; - -final class GroupListAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Group list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - if ($request->isGet()) { - $groups = $this->memf->getMetaEntityManager()->getRepository('App\Entity\Metamodel\Group')->findAll(); - $newResponse = $response->withJson($groups); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('label') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new group' - ); - } - } - - $group = $this->postGroup($parsedBody); - $newResponse = $response->withJson($group, 201); - } - - return $newResponse; - } - - private function postGroup(array $parsedBody): Group - { - $group = new Group(); - $group->setLabel($parsedBody['label']); - - $this->memf->getMetaEntityManager()->persist($group); - $this->memf->getMetaEntityManager()->flush(); - - return $group; - } -} diff --git a/src/Action/Meta/OutputCategoryAction.php b/src/Action/Meta/OutputCategoryAction.php deleted file mode 100644 index f9c5003..0000000 --- a/src/Action/Meta/OutputCategoryAction.php +++ /dev/null @@ -1,160 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\OutputCategory; -use App\Entity\Metamodel\OutputFamily; - -/** - * Route: /metadata/output-category/{id} - * {id}: Output category id - * - * This action is used to manage one output category - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class OutputCategoryAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param MetaEntityManagerFactory $memf - */ - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - /** - * `GET` Returns the output category found - * `PUT` Full update the output category and returns the new version - * `DELETE` Delete the output category found and returns a confirmation message - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, $args): Response - { - $this->logger->info('Output category action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - // Search the correct output category with primary key (id) - $outputCategory = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\OutputCategory', $args['id']); - - // If output category is not found 404 - if (is_null($outputCategory)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Output category with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($outputCategory); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // If mandatories empty fields 400 - foreach (array('label', 'display', 'id_output_family') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the output category' - ); - } - } - - // Search the correct output family with primary key - $outputFamily = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\OutputFamily', - $parsedBody['id_output_family'] - ); - - // Output family is mandatory. If is null 404 - if (is_null($outputFamily)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Output family with id ' . $parsedBody['id_output_family'] . ' is not found' - )->withStatus(404); - } - - $this->editOutputCategory($outputCategory, $parsedBody, $outputFamily); - $newResponse = $response->withJson($outputCategory); - } - - if ($request->isDelete()) { - $id = $outputCategory->getId(); - $this->memf->getMetaEntityManager()->remove($outputCategory); - $this->memf->getMetaEntityManager()->flush(); - $newResponse = $response->withJson(array('message' => 'Output category with id ' . $id . ' is removed!')); - } - - return $newResponse; - } - - /** - * Update output category object with setters - * - * @param OutputCategory $outputCategory The output category to update - * @param string[] $parsedBody Contains the new values ​​of the output category sent by the user - * @param OutputFamily $outputFamily Contains the output family doctrine object to set to the output category - */ - private function editOutputCategory( - OutputCategory $outputCategory, - array $parsedBody, - OutputFamily $outputFamily - ): void { - $outputCategory->setLabel($parsedBody['label']); - $outputCategory->setDisplay($parsedBody['display']); - $outputCategory->setOutputFamily($outputFamily); - $this->memf->getMetaEntityManager()->flush(); - } -} diff --git a/src/Action/Meta/OutputCategoryListAction.php b/src/Action/Meta/OutputCategoryListAction.php deleted file mode 100644 index f172fbd..0000000 --- a/src/Action/Meta/OutputCategoryListAction.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\OutputCategory; -use App\Entity\Metamodel\OutputFamily; - -final class OutputCategoryListAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Output category list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - if ($request->isGet()) { - $outputCategories = $this->memf->getMetaEntityManager()->getRepository( - 'App\Entity\Metamodel\OutputCategory' - )->findAll(); - $newResponse = $response->withJson($outputCategories); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('label', 'display', 'id_output_family') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new output category' - ); - } - } - - // Vérification de l'existence de la output family - $outputFamily = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\OutputFamily', - $parsedBody['id_output_family'] - ); - if (is_null($outputFamily)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Output family with id ' . $parsedBody['id_output_family'] . ' is not found' - )->withStatus(404); - } - - $outputCategory = $this->postOutputCategory($parsedBody, $outputFamily); - $newResponse = $response->withJson($outputCategory, 201); - } - - return $newResponse; - } - - private function postOutputCategory(array $parsedBody, OutputFamily $outputFamily): OutputCategory - { - $outputCategory = new OutputCategory(); - $outputCategory->setLabel($parsedBody['label']); - $outputCategory->setDisplay($parsedBody['display']); - $outputCategory->setOutputFamily($outputFamily); - - $this->memf->getMetaEntityManager()->persist($outputCategory); - $this->memf->getMetaEntityManager()->flush(); - - return $outputCategory; - } -} diff --git a/src/Action/Meta/ProjectAction.php b/src/Action/Meta/ProjectAction.php deleted file mode 100644 index ec2d3b8..0000000 --- a/src/Action/Meta/ProjectAction.php +++ /dev/null @@ -1,159 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Project; -use App\Entity\Metamodel\Database; - -/** - * Route: /metadata/project/{name} - * {name}: Project name - * - * This action is used to manage one project - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Meta - */ -final class ProjectAction -{ - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - * @param MetaEntityManagerFactory $memf - */ - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - /** - * `GET` Returns the project found - * `PUT` Full update the project and returns the new version - * `DELETE` Delete the project found and return a confirmation message - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Project action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - // Search the correct project with primary key - $project = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Project', $args['name']); - - // If project is not found 404 - if (is_null($project)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Project with id ' . $args['name'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $newResponse = $response->withJson($project); - } - - if ($request->isPut()) { - $parsedBody = $request->getParsedBody(); - - // If mandatories empty fields 400 - foreach (array('label', 'description', 'link', 'manager', 'id_database') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to edit the project' - ); - } - } - - // Search the correct database with primary key - $database = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\Database', - $parsedBody['id_database'] - ); - - // Database is mandatory. If is null 404 - if (is_null($database)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Database with id ' . $parsedBody['id_database'] . ' is not found' - )->withStatus(404); - } - - $this->editProject($project, $parsedBody, $database); - $newResponse = $response->withJson($project); - } - - if ($request->isDelete()) { - $name = $project->getName(); - $this->memf->getMetaEntityManager()->remove($project); - $this->memf->getMetaEntityManager()->flush(); - $newResponse = $response->withJson(array('message' => 'Project ' . $name . ' is removed!')); - } - - return $newResponse; - } - - /** - * Update project object with setters - * - * @param Project $project The project to update - * @param string[] $parsedBody Contains the new values ​​of the project sent by the user - * @param Database $database Contains the database doctrine object to set to the project - */ - private function editProject(Project $project, array $parsedBody, Database $database): void - { - $project->setLabel($parsedBody['label']); - $project->setDescription($parsedBody['description']); - $project->setLink($parsedBody['link']); - $project->setManager($parsedBody['manager']); - $project->setDatabase($database); - $this->memf->getMetaEntityManager()->flush(); - } -} diff --git a/src/Action/Meta/ProjectListAction.php b/src/Action/Meta/ProjectListAction.php deleted file mode 100644 index 5721870..0000000 --- a/src/Action/Meta/ProjectListAction.php +++ /dev/null @@ -1,98 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Entity\Metamodel\Project; -use App\Entity\Metamodel\Database; - -final class ProjectListAction -{ - use ActionTrait; - - private $logger; - private $memf; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf) - { - $this->logger = $logger; - $this->memf = $memf; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Project list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - $this->memf->createMetaEntityManager($args['instance']); - - if ($request->isGet()) { - $projects = $this->memf->getMetaEntityManager()->getRepository('App\Entity\Metamodel\Project')->findAll(); - $newResponse = $response->withJson($projects, 200, JSON_UNESCAPED_SLASHES); - } - - if ($request->isPost()) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - foreach (array('name', 'label', 'description', 'link', 'manager', 'id_database') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param ' . $a . ' needed to add a new project' - ); - } - } - - // Vérification de l'existence de la database - $database = $this->memf->getMetaEntityManager()->find( - 'App\Entity\Metamodel\Database', - $parsedBody['id_database'] - ); - if (is_null($database)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Database with id ' . $parsedBody['id_database'] . ' is not found' - )->withStatus(404); - } - - $project = $this->postProject($parsedBody, $database); - $newResponse = $response->withJson($project, 201, JSON_UNESCAPED_SLASHES); - } - - return $newResponse; - } - - private function postProject(array $parsedBody, Database $database): Project - { - $project = new Project($parsedBody['name']); - $project->setLabel($parsedBody['label']); - $project->setDescription($parsedBody['description']); - $project->setLink($parsedBody['link']); - $project->setManager($parsedBody['manager']); - $project->setDatabase($database); - - $this->memf->getMetaEntityManager()->persist($project); - $this->memf->getMetaEntityManager()->flush(); - - return $project; - } -} diff --git a/src/Action/Meta/TableListAction.php b/src/Action/Meta/TableListAction.php deleted file mode 100644 index 7fdcb6b..0000000 --- a/src/Action/Meta/TableListAction.php +++ /dev/null @@ -1,90 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Meta; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; -use App\Utils\DBALConnectionFactory; - -final class TableListAction -{ - use ActionTrait; - - private $logger; - private $memf; - private $dcf; - - /** - * The encryption key used by anis to encrypt and decrypt sensitive data like passwords - * This key is provided by configuration (see the config file) - * - * @var string - */ - private $encryptionKey; - - public function __construct( - LoggerInterface $logger, - MetaEntityManagerFactory $memf, - DBALConnectionFactory $dcf, - string $encryptionKey - ) { - $this->logger = $logger; - $this->memf = $memf; - $this->dcf = $dcf; - $this->encryptionKey = $encryptionKey; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Table list action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - $this->memf->createMetaEntityManager($args['instance']); - $database = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Database', $args['id']); - - if (is_null($database)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Database with id ' . $args['id'] . ' is not found' - )->withStatus(404); - } - - if ($request->isGet()) { - $decryptedPassword = $this->decryptData($database->getPassword()); - $connection = $this->dcf->create($database, $decryptedPassword); - $sm = $connection->getSchemaManager(); - $f = function ($o) { - return $o->getName(); - }; - $tables = array_merge(array_map($f, $sm->listTables()), $this->getViews($sm->listViews())); - $newReponse = $response->withJson($tables); - } - - return $newReponse; - } - - private function getViews($listViews) - { - $views = array(); - foreach ($listViews as $v) { - $views[] = $v->getName(); - } - return $views; - } -} diff --git a/src/Action/OutputCategoryAction.php b/src/Action/OutputCategoryAction.php new file mode 100644 index 0000000..273c509 --- /dev/null +++ b/src/Action/OutputCategoryAction.php @@ -0,0 +1,112 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\OutputCategory; +use App\Entity\OutputFamily; + +final class OutputCategoryAction extends AbstractAction +{ + /** + * `GET` Returns the output category found + * `PUT` Full update the output category and returns the new version + * `DELETE` Delete the output category found and return a confirmation message + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct output category with primary key (id) + $outputCategory = $this->em->find('App\Entity\OutputCategory', $args['id']); + + // If output category is not found 404 + if (is_null($outputCategory)) { + throw new HttpNotFoundException( + $request, + 'Output category with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($outputCategory); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + // If mandatories empty fields 400 + foreach (array('label', 'display', 'id_output_family') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the output category' + ); + } + } + + // Search the correct output family with primary key + $outputFamily = $this->em->find('App\Entity\OutputFamily', $parsedBody['id_output_family']); + + // Output family is mandatory. If is null 400 + if (is_null($outputFamily)) { + throw new HttpBadRequestException( + $request, + 'Output family with id ' . $parsedBody['id_output_family'] . ' is not found' + ); + } + + $this->editOutputCategory($outputCategory, $parsedBody, $outputFamily); + $payload = json_encode($outputCategory); + } + + if ($request->getMethod() === DELETE) { + $id = $outputCategory->getId(); + $this->em->remove($outputCategory); + $this->em->flush(); + $payload = json_encode(array('message' => 'Output category with id ' . $id . ' is removed!')); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Update output category object with setters + * + * @param OutputCategory $outputCategory The output category to update + * @param array $parsedBody Contains the new values ​​of the output category sent by the user + * @param OutputFamily $outputFamily Contains the output family doctrine object to set to the output category + */ + private function editOutputCategory( + OutputCategory $outputCategory, + array $parsedBody, + OutputFamily $outputFamily + ): void { + $outputCategory->setLabel($parsedBody['label']); + $outputCategory->setDisplay($parsedBody['display']); + $outputCategory->setOutputFamily($outputFamily); + $this->em->flush(); + } +} diff --git a/src/Action/OutputCategoryListAction.php b/src/Action/OutputCategoryListAction.php new file mode 100644 index 0000000..d818322 --- /dev/null +++ b/src/Action/OutputCategoryListAction.php @@ -0,0 +1,87 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use App\Entity\OutputCategory; +use App\Entity\OutputFamily; + +final class OutputCategoryListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all output categories listed in the metamodel database + * `POST` Add a new output category + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + if ($request->getMethod() === GET) { + $outputCategories = $this->em->getRepository('App\Entity\OutputCategory')->findAll(); + $payload = json_encode($outputCategories); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // Verif mandatories fields + foreach (array('label', 'display', 'id_output_family') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new output category' + ); + } + } + + // Output family is mandatory + $outputFamily = $this->em->find('App\Entity\OutputFamily', $parsedBody['id_output_family']); + if (is_null($outputFamily)) { + throw new HttpBadRequestException( + $request, + 'Output family with id ' . $parsedBody['id_output_family'] . ' is not found' + ); + } + + $outputCategory = $this->postOutputCategory($parsedBody, $outputFamily); + $payload = json_encode($outputCategory); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + private function postOutputCategory(array $parsedBody, OutputFamily $outputFamily): OutputCategory + { + $outputCategory = new OutputCategory(); + $outputCategory->setLabel($parsedBody['label']); + $outputCategory->setDisplay($parsedBody['display']); + $outputCategory->setOutputFamily($outputFamily); + + $this->em->persist($outputCategory); + $this->em->flush(); + + return $outputCategory; + } +} diff --git a/src/Action/ProjectAction.php b/src/Action/ProjectAction.php new file mode 100644 index 0000000..138b1cd --- /dev/null +++ b/src/Action/ProjectAction.php @@ -0,0 +1,109 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Database; +use App\Entity\Project; + +final class ProjectAction extends AbstractAction +{ + /** + * `GET` Returns the project found + * `PUT` Full update the project and returns the new version + * `DELETE` Delete the project found and return a confirmation message + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct project with primary key + $project = $this->em->find('App\Entity\Project', $args['name']); + + // If project is not found 404 + if (is_null($project)) { + throw new HttpNotFoundException( + $request, + 'Project with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($project, JSON_UNESCAPED_SLASHES); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + // If mandatories empty fields 400 + foreach (array('label', 'description', 'link', 'manager', 'id_database') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the project' + ); + } + } + + // Database exists ? + $database = $this->em->find('App\Entity\Database', $parsedBody['id_database']); + if (is_null($database)) { + throw new HttpBadRequestException( + $request, + 'Database with id ' . $parsedBody['id_database'] . ' is not found' + ); + } + + $this->editProject($project, $parsedBody, $database); + $payload = json_encode($project, JSON_UNESCAPED_SLASHES); + } + + if ($request->getMethod() === DELETE) { + $name = $project->getName(); + $this->em->remove($project); + $this->em->flush(); + $payload = json_encode(array('message' => 'Project ' . $name . ' is removed!')); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Update project object with setters + * + * @param Project $project The project to update + * @param string[] $parsedBody Contains the new values ​​of the project sent by the user + * @param Database $database Contains the database doctrine object to set to the project + */ + private function editProject(Project $project, array $parsedBody, Database $database): void + { + $project->setLabel($parsedBody['label']); + $project->setDescription($parsedBody['description']); + $project->setLink($parsedBody['link']); + $project->setManager($parsedBody['manager']); + $project->setDatabase($database); + $this->em->flush(); + } +} diff --git a/src/Action/ProjectListAction.php b/src/Action/ProjectListAction.php new file mode 100644 index 0000000..b35a8b8 --- /dev/null +++ b/src/Action/ProjectListAction.php @@ -0,0 +1,89 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use App\Entity\Database; +use App\Entity\Project; + +final class ProjectListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all projects listed in the metamodel database + * `POST` Add a new project + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + if ($request->getMethod() === GET) { + $projects = $this->em->getRepository('App\Entity\Project')->findAll(); + $payload = json_encode($projects, JSON_UNESCAPED_SLASHES); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs information to update + foreach (array('name', 'label', 'description', 'link', 'manager', 'id_database') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new project' + ); + } + } + + // Database exists ? + $database = $this->em->find('App\Entity\Database', $parsedBody['id_database']); + if (is_null($database)) { + throw new HttpBadRequestException( + $request, + 'Database with id ' . $parsedBody['id_database'] . ' is not found' + ); + } + + $project = $this->postProject($parsedBody, $database); + $payload = json_encode($project, JSON_UNESCAPED_SLASHES); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + private function postProject(array $parsedBody, Database $database): Project + { + $project = new Project($parsedBody['name']); + $project->setLabel($parsedBody['label']); + $project->setDescription($parsedBody['description']); + $project->setLink($parsedBody['link']); + $project->setManager($parsedBody['manager']); + $project->setDatabase($database); + + $this->em->persist($project); + $this->em->flush(); + + return $project; + } +} diff --git a/src/Action/Root/OpenApiAction.php b/src/Action/Root/OpenApiAction.php deleted file mode 100644 index 3130e39..0000000 --- a/src/Action/Root/OpenApiAction.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Root; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use file_get_contents; - -/** - * Route: /open-api - * - * This action returns the open api description for the anis service. (GET) - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Root - */ -final class OpenApiAction -{ - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * This action returns the open api description for the anis service - * The open API description is available at the root of the project (anis-server.yaml) - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Open api action dispatched'); - $filePath = __DIR__ . '/../../../anis-server.yaml'; - $openApi = file_get_contents($filePath); - return $response->write($openApi)->withHeader('Content-Type', 'text/yaml'); - } -} diff --git a/src/Action/Root/RootAction.php b/src/Action/Root/RootAction.php deleted file mode 100644 index 369c911..0000000 --- a/src/Action/Root/RootAction.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Root; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -/** - * Route: / - * - * With this action user can make sure that - * the service is responding. (GET) - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Root - */ -final class RootAction -{ - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * Create the classe before call __invoke to execute the action - * - * @param LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * This action indicates that the service is responding - * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Root action dispatched'); - return $response->withJson(array('message' => 'it works!')); - } -} diff --git a/src/Action/RootAction.php b/src/Action/RootAction.php new file mode 100644 index 0000000..f563a7c --- /dev/null +++ b/src/Action/RootAction.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Root action + * + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +final class RootAction +{ + /** + * This action indicates that the service is responding + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + $payload = json_encode(array('message' => 'it works!')); + $response->getBody()->write($payload); + return $response; + } +} diff --git a/src/Action/Search/ServiceAction.php b/src/Action/Search/ServiceAction.php deleted file mode 100644 index 5b7ba1f..0000000 --- a/src/Action/Search/ServiceAction.php +++ /dev/null @@ -1,86 +0,0 @@ -<?php -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Action\Search; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use PhpAmqpLib\Connection\AMQPStreamConnection; -use PhpAmqpLib\Message\AMQPMessage; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; - -final class ServiceAction -{ - use ActionTrait; - - private $logger; - private $memf; - private $amqp; - - public function __construct(LoggerInterface $logger, MetaEntityManagerFactory $memf, AMQPStreamConnection $amqp) - { - $this->logger = $logger; - $this->memf = $memf; - $this->amqp = $amqp; - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $this->logger->info('Service action dispatched'); - - if ($request->isOptions()) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - $dataset = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Dataset', $args['dname']); - - if (is_null($dataset)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset with id ' . $args['dname'] . ' is not found' - )->withStatus(404); - } - - $queryParams = $request->getQueryParams(); - if (!array_key_exists('a', $queryParams)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param a is required for this request' - )->withStatus(400); - } - - $searchUri = '/search/' . $args['instance'] . '/data/' . $args['dname']; - $searchUri .= '?a=' . $queryParams['a']; - - if (array_key_exists('c', $queryParams)) { - $searchUri .= '&c=' . $queryParams['c']; - } - - $uniqId = uniqid(); - - $channel = $this->amqp->channel(); - $channel->queue_declare('csv', false, false, false, false); - $msg = new AMQPMessage(json_encode(array('search' => $searchUri, 'uniqid' => $uniqId))); - $channel->basic_publish($msg, '', 'csv'); - - $channel->close(); - $this->amqp->close(); - - return $response->withJson(array('message' => $uniqId)); - } -} diff --git a/src/Action/Search/SearchAction.php b/src/Action/SearchAction.php similarity index 53% rename from src/Action/Search/SearchAction.php rename to src/Action/SearchAction.php index b9d5e00..3905ec3 100644 --- a/src/Action/Search/SearchAction.php +++ b/src/Action/SearchAction.php @@ -1,211 +1,132 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Action\Search; +declare(strict_types=1); + +namespace App\Action; -use Psr\Log\LoggerInterface; -use Doctrine\DBAL\Platforms\PostgreSQL94Platform; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Query\Expression\CompositeExpression; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use PDO; - -use App\Utils\ActionTrait; -use App\Utils\MetaEntityManagerFactory; use App\Utils\DBALConnectionFactory; +use App\Utils\Operator\OperatorFactory; use App\Utils\SearchException; -use App\Utils\Operator\IOperatorFactory; -use App\Entity\Metamodel\Dataset; -use App\Entity\Metamodel\Attribute; +use App\Entity\Dataset; +use App\Entity\Attribute; /** - * Get anis data or meta search (GET) + * Search action * * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action\Search + * @package App\Action */ -final class SearchAction +final class SearchAction extends AbstractAction { - use ActionTrait; - - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * The MetaEntityManagerFactory create the doctrine entity manager for the metamodel database. - * The entity manager is the central access point to Doctrine ORM functionality (metadata database) - * - * @var MetaEntityManagerFactory - */ - private $memf; - - /** - * Factory method used to retrieve a PDO connection object to the business database - * - * @var DBALConnectionFactory - */ - private $dcf; - - /** - * - * @var IOperatorFactory - */ + private $connectionFactory; private $operatorFactory; /** - * The encryption key used by anis to encrypt and decrypt sensitive data like passwords - * This key is provided by configuration (see the config file) - * - * @var string - */ - private $encryptionKey; - - /** - * This class is creates before call __invoke to execute the action + * Create the classe before call __invoke to execute the action * - * @param LoggerInterface $logger - * @param MetaEntityManagerFactory $memf - * @param DBALConnectionFactory $dcf - * @param IOperatorFactory $operatorFactory - * @param string $encryptionKey + * @param EntityManagerInterface $em Doctrine Entity Manager Interface + * @param DBALConnectionFactory $connectionFactory Factory used to construct connection to business database */ - public function __construct( - LoggerInterface $logger, - MetaEntityManagerFactory $memf, - DBALConnectionFactory $dcf, - IOperatorFactory $operatorFactory, - string $encryptionKey - ) { - $this->logger = $logger; - $this->memf = $memf; - $this->dcf = $dcf; + public function __construct(EntityManagerInterface $em, DBALConnectionFactory $connectionFactory, OperatorFactory $operatorFactory) + { + parent::__construct($em); + $this->connectionFactory = $connectionFactory; $this->operatorFactory = $operatorFactory; - $this->encryptionKey = $encryptionKey; } /** - * This action returns data from an anis request + * This action indicates that the service is responding * - * @param ServerRequestInterface $request This object contains the HTTP request - * @param ResponseInterface $response This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) * * @return ResponseInterface */ public function __invoke(Request $request, Response $response, array $args): Response { - $this->logger->info('Search meta action dispatched'); - - if ($request->isOptions()) { + if ($request->getMethod() === OPTIONS) { return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); } - // Create the doctrine Entity Manager object for the instance metamodel database - $this->memf->createMetaEntityManager($args['instance']); - - $dataset = $this->memf->getMetaEntityManager()->find('App\Entity\Metamodel\Dataset', $args['dname']); + // Search the correct dataset with primary key + $dataset = $this->em->find('App\Entity\Dataset', $args['dname']); + // If dataset is not found 404 if (is_null($dataset)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Dataset with id ' . $args['dname'] . ' is not found' - )->withStatus(404); - } - - $queryParams = $request->getQueryParams(); - if (!array_key_exists('a', $queryParams)) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - 'Param a is required for this request' - )->withStatus(400); + throw new HttpNotFoundException( + $request, + 'Dataset with name ' . $args['name'] . ' is not found' + ); } - $database = $dataset->getProject()->getDatabase(); - $decryptedPassword = $this->decryptData($database->getPassword()); - $connection = $this->dcf->create($database, $decryptedPassword); - $this->operatorFactory->setDatabasePlatform($connection->getDatabasePlatform()); - + // Create query builder with from clause using dataset information + $connection = $this->connectionFactory->create($dataset->getProject()->getDatabase()); $queryBuilder = $connection->createQueryBuilder(); $queryBuilder->from($dataset->getTableRef()); + + if ($request->getMethod() === GET) { + $queryParams = $request->getQueryParams(); - try { - $searchType = $this->getAndVerifySearchType($args['type']); - - if (array_key_exists('c', $queryParams)) { - $this->where($queryBuilder, $dataset, explode(';', $queryParams['c'])); + // The parameter "a" is mandatory + if (!array_key_exists('a', $queryParams)) { + throw new HttpBadRequestException( + $request, + 'Param a is required for this request' + ); } - - $listOfIds = explode(';', $queryParams['a']); - - if ($searchType === 'data') { - $attributes = $this->select($queryBuilder, $dataset, $listOfIds); - + + try { + // The parameter a represents the SQL select clause + if ($queryParams['a'] === 'count') { + $attributes = array(); + $queryBuilder->select('COUNT(*) as nb'); + } else { + $attributes = $this->select($queryBuilder, $dataset, explode(';', $queryParams['a'])); + } + + // The parameter c is not mandatory and represents the SQL where clause + if (array_key_exists('c', $queryParams)) { + $this->where($queryBuilder, $dataset, explode(';', $queryParams['c'])); + } + + // The parameter o is not mandatory and represents the SQL order clause if (array_key_exists('o', $queryParams)) { $this->order($queryBuilder, $dataset, explode(';', $queryParams['o'])); } - + + // The parameter p is not mandatory and represents the SQL limit clause if (array_key_exists('p', $queryParams)) { $this->limit($queryBuilder, $queryParams['p']); } - - $result = $this->fetchAll($queryBuilder, $attributes); - } elseif ($searchType === 'meta') { - $queryBuilder->select('COUNT(*) as nb'); - $stmt = $queryBuilder->execute(); - $count = $stmt->fetchAll(); - $result = array(); - $result['dataset_selected'] = $dataset->getLabel(); - $attributesSelected = array(); - foreach ($listOfIds as $id) { - $attribute = $this->getAttribute($dataset, (int) $id); - $attributesSelected[] = array( - 'name' => $attribute->getName(), - 'label' => $attribute->getLabel() - ); - } - $result['attributes_selected'] = $attributesSelected; - $result['total_items'] = $count[0]['nb']; + } catch (SearchException $e) { + throw new HttpBadRequestException( + $request, + $e->getMessage() + ); } - } catch (SearchException $e) { - return $this->dispatchHttpError( - $response, - 'Invalid request', - $e->getMessage() - )->withStatus(400); - } - - $this->logger->info('SQL: ' . $queryBuilder->getSQL()); - - return $response->withJson($result, 200, JSON_NUMERIC_CHECK | JSON_UNESCAPED_SLASHES); - } - /** - * Returns one of two accepted search types (meta or data) - * Or throw an Exception - * - * @throws SearchException Bad type of search - * @param string $type Type of search - */ - private function getAndVerifySearchType(string $type): string - { - if ($type !== 'data' && $type !== 'meta') { - throw SearchException::typeOfSearchDoesNotExist($type); + $result = $this->fetchAll($queryBuilder, $attributes); + $payload = json_encode($result, JSON_UNESCAPED_SLASHES); } - return $type; + + $response->getBody()->write($payload); + return $response; } /** @@ -225,7 +146,6 @@ final class SearchAction $attributes[] = $attribute; } $queryBuilder->select($columns); - return $attributes; } @@ -306,6 +226,23 @@ final class SearchAction ->setMaxResults($limit); } + /** + * Returns the Attribute object of a dataset based on its ID (primary key) + * + * @throws SearchException Attribute with ID not found into the selected dataset + * @return Attribute Returns the attribute found + */ + private function getAttribute(Dataset $dataset, int $id): Attribute + { + $attributes = $dataset->getAttributes(); + foreach ($attributes as $attribute) { + if ($attribute->getId() === $id) { + return $attribute; + } + } + throw SearchException::attributeNotFound($id, $dataset->getLabel()); + } + private function fetchAll(QueryBuilder $queryBuilder, array $attributes): array { $jsonAttributes = $this->getAttributesOfTypeJson($attributes); @@ -331,21 +268,4 @@ final class SearchAction return $attribute->getType() === 'json'; })); } - - /** - * Returns the Attribute object of a dataset based on its ID (primary key) - * - * @throws SearchException Attribute with ID not found into the selected dataset - * @return Attribute - */ - private function getAttribute(Dataset $dataset, int $id): Attribute - { - $attributes = $dataset->getAttributes(); - foreach ($attributes as $attribute) { - if ($attribute->getId() === $id) { - return $attribute; - } - } - throw SearchException::attributeNotFound($id, $dataset->getLabel()); - } } diff --git a/src/Action/TableListAction.php b/src/Action/TableListAction.php new file mode 100644 index 0000000..227647f --- /dev/null +++ b/src/Action/TableListAction.php @@ -0,0 +1,86 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpNotFoundException; +use Doctrine\ORM\EntityManagerInterface; +use App\Utils\DBALConnectionFactory; +use App\Entity\Database; + +final class TableListAction extends AbstractAction +{ + private $connectionFactory; + + /** + * Create the classe before call __invoke to execute the action + * + * @param EntityManagerInterface $em Doctrine Entity Manager Interface + * @param DBALConnectionFactory $connectionFactory Factory used to construct connection to business database + */ + public function __construct(EntityManagerInterface $em, DBALConnectionFactory $connectionFactory) + { + parent::__construct($em); + $this->connectionFactory = $connectionFactory; + } + + /** + * `GET` Returns a list of all tables and views available in the database + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + } + + // Search the correct database with primary key + $database = $this->em->find('App\Entity\Database', $args['id']); + + // If database is not found 404 + if (is_null($database)) { + throw new HttpNotFoundException( + $request, + 'Database with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $connection = $this->connectionFactory->create($database); + $sm = $connection->getSchemaManager(); + $f = function ($o) { + return $o->getName(); + }; + $tables = array_merge(array_map($f, $sm->listTables()), $this->getViews($sm->listViews())); + $payload = json_encode($tables); + } + + $response->getBody()->write($payload); + return $response; + } + + private function getViews($listViews) + { + $views = array(); + foreach ($listViews as $v) { + $views[] = $v->getName(); + } + return $views; + } +} diff --git a/src/Entity/Admin/Instance.php b/src/Entity/Admin/Instance.php deleted file mode 100644 index 5ee052e..0000000 --- a/src/Entity/Admin/Instance.php +++ /dev/null @@ -1,336 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Entity\Admin; - -/** - * @Entity - * @Table(name="instance") - */ -class Instance implements \JsonSerializable -{ - /** - * @var string - * - * @Id - * @Column(type="string", nullable=false) - */ - protected $name; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $label; - - /** - * @var string - * - * @Column(type="string", name="path_proxy", nullable=false) - */ - protected $pathProxy; - - /** - * @var bool - * - * @Column(type="boolean", name="dev_mode", nullable=false) - */ - protected $devMode; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $driver; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $path; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $host; - - /** - * @var integer - * - * @Column(type="integer", nullable=true) - */ - protected $port; - - /** - * @var string - * - * @Column(type="string", name="dbname", nullable=false) - */ - protected $dbname; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $login; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $password; - - /** - * Create the classe with the property name (primary key) - */ - public function __construct(string $name) - { - $this->name = $name; - } - - /** - * Getter property name - * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * Getter property label - * - * @return string - */ - public function getLabel(): string - { - return $this->label; - } - - /** - * Setter property label - * - * @param string $label - */ - public function setLabel(string $label): void - { - $this->label = $label; - } - - /** - * Getter property pathProxy - * - * @return string - */ - public function getPathProxy(): string - { - return $this->pathProxy; - } - - /** - * Setter property pathProxy - * - * @param string $pathProxy - */ - public function setPathProxy(string $pathProxy): string - { - return $this->pathProxy = $pathProxy; - } - - /** - * Getter property devMode - * - * @return bool - */ - public function getDevMode(): bool - { - return $this->devMode; - } - - /** - * @Setter property devMode - * - * @param bool $devMode - */ - public function setDevMode(bool $devMode): void - { - $this->devMode = $devMode; - } - - /** - * Getter property driver - * - * @return string - */ - public function getDriver(): string - { - return $this->driver; - } - - /** - * Setter property driver - * - * @param string $driver - */ - public function setDriver(string $driver): void - { - $this->driver = $driver; - } - - /** - * Getter property path - * - * @return string - */ - public function getPath(): string - { - return $this->path; - } - - /** - * Setter property path - * - * @param string $path - */ - public function setPath(string $path): void - { - $this->path = $path; - } - - /** - * Getter property host - * - * @return string - */ - public function getHost(): string - { - return $this->host; - } - - /** - * Setter property host - * - * @param string $host - */ - public function setHost(string $host): void - { - $this->host = $host; - } - - /** - * Getter property port - * - * @return int - */ - public function getPort(): int - { - return $this->port; - } - - /** - * Setter property port - * - * @param int $port - */ - public function setPort(int $port): void - { - $this->port = $port; - } - - /** - * Getter property dbname - * - * @return string - */ - public function getDbName(): string - { - return $this->dbname; - } - - /** - * Setter property dbname - * - * @param string $dbname - */ - public function setDbName(string $dbname): void - { - $this->dbname = $dbname; - } - - /** - * Getter property login - * - * @return string - */ - public function getLogin(): string - { - return $this->login; - } - - /** - * Setter property login - * - * @param string $login - */ - public function setLogin(string $login): void - { - $this->login = $login; - } - - /** - * Getter property password - * - * @return string - */ - public function getPassword(): string - { - return $this->password; - } - - /** - * Setter property password - * - * @param string $password - */ - public function setPassword(string $password): void - { - $this->password = $password; - } - - /** - * Return objet to array - * - * @return array - */ - public function jsonSerialize(): array - { - return [ - 'name' => $this->getName(), - 'label' => $this->getLabel(), - 'path_proxy' => $this->getPathProxy(), - 'dev_mode' => $this->getDevMode(), - 'driver' => $this->getDriver(), - 'path' => $this->getPath(), - 'host' => $this->getHost(), - 'port' => $this->getPort(), - 'dbname' => $this->getDbName(), - 'login' => $this->getLogin(), - 'password' => $this->getPassword() - ]; - } -} diff --git a/src/Entity/Admin/SettingsOption.php b/src/Entity/Admin/SettingsOption.php deleted file mode 100644 index 9341515..0000000 --- a/src/Entity/Admin/SettingsOption.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Entity\Admin; - -/** -* @Entity -* @Table(name="settings_option") -*/ -class SettingsOption implements \JsonSerializable -{ - /** - * @var integer - * - * @Id - * @Column(type="integer", nullable=false) - * @GeneratedValue - */ - protected $id; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $label; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $value; - - /** - * @var integer - * - * @Column(type="integer", nullable=false) - */ - protected $display; - - /** - * @var Anis\Entity\SettingsSelect - * - * @ManyToOne(targetEntity="SettingsSelect", inversedBy="settingsOptions") - * @JoinColumn(name="settings_select_id", referencedColumnName="id", nullable=false) - */ - protected $settingsSelect; - - public function __construct(SettingsSelect $settingsSelect) - { - $this->settingsSelect = $settingsSelect; - } - - public function getId() - { - return $this->id; - } - - public function getLabel() - { - return $this->label; - } - - public function setLabel($label) - { - $this->label = $label; - } - - public function getValue() - { - return $this->value; - } - - public function setValue($value) - { - $this->value = $value; - } - - public function getDisplay() - { - return $this->display; - } - - public function setDisplay($display) - { - $this->display = (int) $display; - } - - public function getSettingsSelect() - { - return $this->settingsSelect; - } - - public function jsonSerialize() - { - return [ - 'id' => $this->getId(), - 'label' => $this->getLabel(), - 'value' => $this->getValue(), - 'display' => $this->getDisplay(), - 'id_settings_select' => $this->getSettingsSelect()->getId() - ]; - } -} diff --git a/src/Entity/Admin/SettingsSelect.php b/src/Entity/Admin/SettingsSelect.php deleted file mode 100644 index 93054fd..0000000 --- a/src/Entity/Admin/SettingsSelect.php +++ /dev/null @@ -1,82 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Entity\Admin; - -/** -* @Entity -* @Table(name="settings_select") -*/ -class SettingsSelect implements \JsonSerializable -{ - /** - * @var integer - * - * @Id - * @Column(type="integer", nullable=false) - * @GeneratedValue - */ - protected $id; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $name; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $label; - - /** - * @var Anis\Entity\SettingsOption[] - * - * @OneToMany(targetEntity="SettingsOption", mappedBy="settingsSelect", cascade={"remove"}) - */ - protected $settingsOptions; - - public function getId() - { - return $this->id; - } - - public function getName() - { - return $this->name; - } - - public function setName($name) - { - $this->name = $name; - } - - public function getLabel() - { - return $this->label; - } - - public function setLabel($label) - { - $this->label = $label; - } - - public function jsonSerialize() - { - return [ - 'id' => $this->id, - 'name' => $this->getName(), - 'label' => $this->getLabel() - ]; - } -} diff --git a/src/Entity/Admin/User.php b/src/Entity/Admin/User.php deleted file mode 100644 index cd3fb54..0000000 --- a/src/Entity/Admin/User.php +++ /dev/null @@ -1,131 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Entity\Admin; - -/** -* @Entity -* @Table(name="anis_user") -*/ -class User implements \JsonSerializable -{ - /** - * @var string - * - * @Id - * @Column(type="string", nullable=false) - */ - protected $email; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $password; - - /** - * @var string - * - * @Column(type="string", name="activation_key", nullable=false) - */ - protected $activationKey; - - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $activated; - - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $adminsi; - - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $superuser; - - public function __construct($email) - { - return $this->email = $email; - } - - public function getEmail() - { - return $this->email; - } - - public function getPassword() - { - return $this->password; - } - - public function setPassword($password) - { - $this->password = $password; - } - - public function getActivationKey() - { - return $this->activationKey; - } - - public function setActivationKey($activationKey) - { - $this->activationKey = $activationKey; - } - - public function getActivated() - { - return $this->activated; - } - - public function setActivated($activated) - { - $this->activated = $activated; - } - - public function getAdminsi() - { - return $this->adminsi; - } - - public function setAdminsi($adminsi) - { - $this->adminsi = $adminsi; - } - - public function getSuperuser() - { - return $this->superuser; - } - - public function setSuperuser($superuser) - { - $this->superuser = $superuser; - } - - public function jsonSerialize() - { - return [ - 'email' => $this->getEmail(), - 'activated' => $this->getActivated(), - 'adminsi' => $this->getAdminsi(), - 'superuser' => $this->getSuperuser() - ]; - } -} diff --git a/src/Entity/Metamodel/Attribute.php b/src/Entity/Attribute.php similarity index 98% rename from src/Entity/Metamodel/Attribute.php rename to src/Entity/Attribute.php index 0864960..30ecbea 100644 --- a/src/Entity/Metamodel/Attribute.php +++ b/src/Entity/Attribute.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; diff --git a/src/Entity/Metamodel/CriteriaFamily.php b/src/Entity/CriteriaFamily.php similarity index 87% rename from src/Entity/Metamodel/CriteriaFamily.php rename to src/Entity/CriteriaFamily.php index de056b5..1646654 100644 --- a/src/Entity/Metamodel/CriteriaFamily.php +++ b/src/Entity/CriteriaFamily.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/Database.php b/src/Entity/Database.php similarity index 93% rename from src/Entity/Metamodel/Database.php rename to src/Entity/Database.php index b0c18db..1cf4c5d 100644 --- a/src/Entity/Metamodel/Database.php +++ b/src/Entity/Database.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/Dataset.php b/src/Entity/Dataset.php similarity index 95% rename from src/Entity/Metamodel/Dataset.php rename to src/Entity/Dataset.php index 2abe888..b2205f8 100644 --- a/src/Entity/Metamodel/Dataset.php +++ b/src/Entity/Dataset.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; @@ -122,7 +124,7 @@ class Dataset implements \JsonSerializable public function __construct($name) { $this->name = $name; - $this->attributes = new ArrayCollection; + $this->attributes = new ArrayCollection(); } public function getName() diff --git a/src/Entity/Metamodel/DatasetFamily.php b/src/Entity/DatasetFamily.php similarity index 87% rename from src/Entity/Metamodel/DatasetFamily.php rename to src/Entity/DatasetFamily.php index 0f5a8cc..1137e75 100644 --- a/src/Entity/Metamodel/DatasetFamily.php +++ b/src/Entity/DatasetFamily.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; @@ -50,7 +52,7 @@ class DatasetFamily implements \JsonSerializable public function __construct() { - $this->datasets = new ArrayCollection; + $this->datasets = new ArrayCollection(); } public function getId() diff --git a/src/Entity/Metamodel/DatasetPrivileges.php b/src/Entity/DatasetPrivileges.php similarity index 86% rename from src/Entity/Metamodel/DatasetPrivileges.php rename to src/Entity/DatasetPrivileges.php index 8570cf1..d7c4361 100644 --- a/src/Entity/Metamodel/DatasetPrivileges.php +++ b/src/Entity/DatasetPrivileges.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/File.php b/src/Entity/File.php similarity index 92% rename from src/Entity/Metamodel/File.php rename to src/Entity/File.php index ab92709..1065603 100644 --- a/src/Entity/Metamodel/File.php +++ b/src/Entity/File.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/Group.php b/src/Entity/Group.php similarity index 86% rename from src/Entity/Metamodel/Group.php rename to src/Entity/Group.php index 999cef7..f4fe223 100644 --- a/src/Entity/Metamodel/Group.php +++ b/src/Entity/Group.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/OutputCategory.php b/src/Entity/OutputCategory.php similarity index 90% rename from src/Entity/Metamodel/OutputCategory.php rename to src/Entity/OutputCategory.php index f139573..9fdff31 100644 --- a/src/Entity/Metamodel/OutputCategory.php +++ b/src/Entity/OutputCategory.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/OutputFamily.php b/src/Entity/OutputFamily.php similarity index 88% rename from src/Entity/Metamodel/OutputFamily.php rename to src/Entity/OutputFamily.php index be79b16..d768a47 100644 --- a/src/Entity/Metamodel/OutputFamily.php +++ b/src/Entity/OutputFamily.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/Project.php b/src/Entity/Project.php similarity index 92% rename from src/Entity/Metamodel/Project.php rename to src/Entity/Project.php index 4e27d37..e4f642f 100644 --- a/src/Entity/Metamodel/Project.php +++ b/src/Entity/Project.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Entity/Metamodel/User.php b/src/Entity/User.php similarity index 92% rename from src/Entity/Metamodel/User.php rename to src/Entity/User.php index d0d9779..17a5294 100644 --- a/src/Entity/Metamodel/User.php +++ b/src/Entity/User.php @@ -1,14 +1,16 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace App\Entity\Metamodel; +declare(strict_types=1); + +namespace App\Entity; /** * @Entity diff --git a/src/Handlers/LogErrorHandler.php b/src/Handlers/LogErrorHandler.php new file mode 100644 index 0000000..61b0d76 --- /dev/null +++ b/src/Handlers/LogErrorHandler.php @@ -0,0 +1,52 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Handlers; + +use Slim\Handlers\ErrorHandler; +use Psr\Log\LoggerInterface; + +/** + * Handler to log eror information with psr-3 logger system + * + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Handlers + */ +class LogErrorHandler extends ErrorHandler +{ + /** + * The logger interface is the central access point to log information + * + * @var LoggerInterface + */ + private $logger; + + /** + * Set the concrete logger api based on psr-3 logger interface + * + * @param LoggerInterface $logger PSR-3 logger interface + */ + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + /** + * Log error information + * + * @param string $error Error information + */ + protected function logError(string $error): void + { + $this->logger->info($error); + } +} diff --git a/src/Middleware/AuthorizationMiddleware.php b/src/Middleware/AuthorizationMiddleware.php new file mode 100644 index 0000000..739dd60 --- /dev/null +++ b/src/Middleware/AuthorizationMiddleware.php @@ -0,0 +1,137 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Middleware; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use Psr\Http\Server\MiddlewareInterface; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpUnauthorizedException; +use Doctrine\ORM\EntityManagerInterface; +use Lcobucci\JWT\ValidationData; +use Lcobucci\JWT\Parser; +use Lcobucci\JWT\Signer\Hmac\Sha256; + +/** + * Middleware to handle authentication jwt + * + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Middleware + */ +final class AuthorizationMiddleware implements MiddlewareInterface +{ + /** + * The EntityManager is the central access point to Doctrine ORM functionality + * + * @var EntityManagerInterface + */ + protected $em; + + /** + * This object contains token options from settings file + * + * @var array + */ + private $tokenOptions; + + /** + * Create the classe before call process to execute the middleware + * + * @param array $tokenOptions Options to create the token + */ + public function __construct(EntityManagerInterface $em, array $tokenOptions) + { + $this->em = $em; + $this->tokenOptions = $tokenOptions; + } + + /** + * Handle jwt authentication + * + * @param ServerRequest $request PSR-7 request + * @param RequestHandler $handler PSR-15 request handler + * + * @return Response + */ + public function process(Request $request, RequestHandler $handler): Response + { + if (!$request->hasHeader('Authorization')) { + throw new HttpBadRequestException( + $request, + 'Header Authorization needed for this route' + ); + } + + $authorization = $request->getHeaderLine('Authorization'); + + list($title, $jwt) = explode(' ', $authorization); + if ($title !== 'Bearer') { + throw new HttpBadRequestException( + $request, + 'The format of the header Authorization must be Bearer jwt' + ); + } + + $token = (new Parser())->parse((string) $jwt); + + if (!$token->validate($this->getValidationData())) { + throw new HttpUnauthorizedException( + $request, + 'The jwt token is not validate' + ); + } + + $key = file_get_contents($this->tokenOptions['key']); + if (!$token->verify(new Sha256(), $key)) { + throw new HttpUnauthorizedException( + $request, + 'The jwt token signature is not verify' + ); + } + + $jti = $token->getClaim('jti'); + if ($this->getRevoked($jti)) { + throw new HttpUnauthorizedException( + $request, + 'The jwt token is revoked' + ); + } + + return $handler->handle($request->withAttribute('superuser', $token->getClaim('superuser'))); + } + + /** + * Create and return the object used to validate a token + * + * @return ValidationData The object to validate a token + */ + private function getValidationData(): ValidationData + { + $validationData = new ValidationData(); + $validationData->setIssuer($this->tokenOptions['iss']); + $validationData->setAudience($this->tokenOptions['aud']); + return $validationData; + } + + /** + * Return the state of the token (revoked or not) + * + * @return bool The state of the token + */ + private function getRevoked($jti): bool + { + $entityToken = $this->em->find('App\Entity\Token', $jti); + return $entityToken->getRevoked(); + } +} diff --git a/src/Middleware/ContentTypeJsonMiddleware.php b/src/Middleware/ContentTypeJsonMiddleware.php new file mode 100644 index 0000000..1739227 --- /dev/null +++ b/src/Middleware/ContentTypeJsonMiddleware.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Middleware; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use Psr\Http\Server\MiddlewareInterface; + +/** + * Middleware to force content type to application/json eror + * + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Middleware + */ +final class ContentTypeJsonMiddleware implements MiddlewareInterface +{ + /** + * Force return response content type to application/json + * + * @param ServerRequest $request PSR-7 request + * @param RequestHandler $handler PSR-15 request handler + * + * @return Response + */ + public function process(Request $request, RequestHandler $handler): Response + { + $response = $handler->handle($request); + return $response->withHeader('Content-Type', 'application/json'); + } +} diff --git a/src/Middleware/CorsMiddleware.php b/src/Middleware/CorsMiddleware.php deleted file mode 100644 index e3b48ea..0000000 --- a/src/Middleware/CorsMiddleware.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Middleware; - -//use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; - -class CorsMiddleware -{ - public function __invoke(Request $req, Response $res, $next) - { - if ($req->isOptions()) { - $acah = 'X-Requested-With, Content-Type, Accept, Origin, Authorization'; - $response = $res - ->withHeader('Access-Control-Allow-Origin', '*') - ->withHeader('Access-Control-Allow-Headers', $acah); - } else { - $response = $res - ->withHeader('Access-Control-Allow-Origin', '*') - ->withHeader('Content-Type', 'application/json'); - } - - return $next($req, $response); - } -} diff --git a/src/Middleware/JsonBodyParserMiddleware.php b/src/Middleware/JsonBodyParserMiddleware.php new file mode 100644 index 0000000..2628f7d --- /dev/null +++ b/src/Middleware/JsonBodyParserMiddleware.php @@ -0,0 +1,50 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Middleware; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use Psr\Http\Server\MiddlewareInterface; + +/** + * Middleware to handle request parsed body + * + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Middleware + */ +final class JsonBodyParserMiddleware implements MiddlewareInterface +{ + /** + * Read body http request data if request content-type is application/json + * And set parsed body with json decoded information + * + * @param ServerRequest $request PSR-7 request + * @param RequestHandler $handler PSR-15 request handler + * + * @return Response + */ + public function process(Request $request, RequestHandler $handler): Response + { + $contentType = $request->getHeaderLine('Content-Type'); + + if (strstr($contentType, 'application/json')) { + $contents = json_decode(file_get_contents('php://input'), true); + if (json_last_error() === JSON_ERROR_NONE) { + $request = $request->withParsedBody($contents); + } + } + + return $handler->handle($request); + } +} diff --git a/src/Middleware/TokenMiddleware.php b/src/Middleware/TokenMiddleware.php deleted file mode 100644 index 42be44b..0000000 --- a/src/Middleware/TokenMiddleware.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Middleware; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Lcobucci\JWT\Parser; -use Lcobucci\JWT\ValidationData; -use Lcobucci\JWT\Signer\Hmac\Sha256; - -class TokenMiddleware -{ - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - public function __invoke(Request $request, Response $response, $next) - { - $this->logger->info("Token middleware dispatched"); - - if ($request->isOptions()) { - $response = $next($request, $response); - return $response; - } - - if (!$request->hasHeader('Authorization')) { - return $response->withStatus(401); - } - - // Récupération du token string dans le header de la requete HTTP - $bearer = $request->getHeader('Authorization'); - $data = explode(' ', $bearer[0]); - if ($data[0] !== 'Bearer') { - return $response->withStatus(401); - } - - // Conversion string vers objet - $token = (new Parser())->parse((string) $data[1]); - - // Validation du token - $data = new ValidationData(); - $data->setIssuer('http://anis.lam.fr'); - $data->setAudience('http://anis.lam.fr'); - if (!$token->validate($data)) { - return $response->withStatus(401); - } - - // Verification de la signature du token - $signer = new Sha256(); - if (!$token->verify($signer, 'testing')) { - return $response->withStatus(401); - } - - $response = $next($request->withAttribute('email', $token->getClaim('email')), $response); - - return $response; - } -} diff --git a/src/Utils/ActionTrait.php b/src/Utils/ActionTrait.php deleted file mode 100644 index 7b5a535..0000000 --- a/src/Utils/ActionTrait.php +++ /dev/null @@ -1,84 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Utils; - -use Psr\Http\Message\ResponseInterface as Response; - -/** - * Shared functions used by actions classes - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Utils - */ -trait ActionTrait -{ - /** - * @param string $field - * @param array $parsedBody - * - * @return string true if field is empty or false else - */ - protected function isEmptyField(string $field, array $parsedBody): bool - { - // TODO: Vérifier le type, si le type est string verifier champ vide - if (!isset($parsedBody[$field])) { - return true; - } else { - return false; - } - } - - /** - * Centralizes client error responses (400) - * - * @param ResponseInterface $response - * @param string $error - * @param string $errorDescription - * - * @return ResponseInterface The http client error response - */ - protected function dispatchHttpError(Response $response, string $error, string $errorDescription): Response - { - $this->logger->info($errorDescription); - return $response->withStatus(400)->write(json_encode(array( - 'error' => $error, - 'error_description' => $errorDescription - ))); - } - - /** - * @param string $data The string to encrypt - * - * @return string The string encrypted - */ - protected function encryptData(string $data): string - { - // Generate an initialization vector - $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc')); - // Encrypt the data using AES 256 encryption in CBC mode using our encryption key and initialization vector. - $encrypted = openssl_encrypt($data, 'aes-256-cbc', $this->encryptionKey, 0, $iv); - // The $iv is just as important as the key for decrypting - // So save it with our encrypted data using a unique separator (::) - return base64_encode($encrypted . '::' . $iv); - } - - /** - * @param string $data The string to decrypt - * - * @return string The string decrypted - */ - protected function decryptData(string $data): string - { - // To decrypt, split the encrypted data from our IV - our unique separator used was "::" - list($encryptedData, $iv) = explode('::', base64_decode($data), 2); - return openssl_decrypt($encryptedData, 'aes-256-cbc', $this->encryptionKey, 0, $iv); - } -} diff --git a/src/Utils/AnisErrorHandler.php b/src/Utils/AnisErrorHandler.php deleted file mode 100644 index 31f1c00..0000000 --- a/src/Utils/AnisErrorHandler.php +++ /dev/null @@ -1,106 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Utils; - -use Psr\Log\LoggerInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Swift_Mailer; -use Throwable; - -/** - * Allows to manage anis server error and in particular returns the error by mail to the administrator - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Utils - */ -class AnisErrorHandler extends \Slim\Handlers\PhpError -{ - /** - * The logger interface is the central access point to log anis information - * - * @var LoggerInterface - */ - private $logger; - - /** - * Swift Mailer class is used here to send error by email - * - * @var Swift_Mailer - */ - private $mailer; - - /** - * Create the classe before call __invoke to execute error handler - * - * @param LoggerInterface $logger - * @param Swift_Mailer $mailer - * @param bool $displayErrorDetails - */ - public function __construct(LoggerInterface $logger, Swift_Mailer $mailer, bool $displayErrorDetails) - { - parent::__construct($displayErrorDetails); - $this->logger = $logger; - $this->mailer = $mailer; - } - - /** - * Sending an email to the administrator with details and invoke slim error handler - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Throwable $error The caught Throwable object - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, Throwable $error): Response - { - $bool = $this->displayErrorDetails; - $this->displayErrorDetails = true; - $this->logger->error($this->getMessageAsText($error)); - $this->sendErrorByMail($error); - $this->displayErrorDetails = $bool; - return parent::__invoke($request, $response, $error); - } - - /** - * This method send the email to the administrator - * - * @param Throwable $error - */ - private function sendErrorByMail(Throwable $error): void - { - $message = new \Swift_Message(); - $message->setContentType('text/html'); - $message->setSubject('ANIS error'); - $message->setBody($this->renderHtmlErrorMessage($error)); - $message->setFrom('anis-v3-server@lam.fr'); - $message->setTo('cesamsi@lam.fr'); - $this->mailer->send($message); - } - - /** - * This method transform the error to the one line text message for the logger - * - * @param Throwable $error - */ - private function getMessageAsText(Throwable $error): string - { - $message = 'Slim Application Error:' . PHP_EOL; - $message .= $this->renderThrowableAsText($error); - while ($error = $error->getPrevious()) { - $message .= PHP_EOL . 'Previous error:' . PHP_EOL; - $message .= $this->renderThrowableAsText($error); - } - - return $message . PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; - } -} diff --git a/src/Utils/DBALConnectionFactory.php b/src/Utils/DBALConnectionFactory.php index 785079e..00a35d8 100644 --- a/src/Utils/DBALConnectionFactory.php +++ b/src/Utils/DBALConnectionFactory.php @@ -1,19 +1,20 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Connection; - -use App\Entity\Metamodel\Database; +use App\Entity\Database; /** * Factory used to create a Doctrine DBAL connection to a business database @@ -24,24 +25,23 @@ use App\Entity\Metamodel\Database; class DBALConnectionFactory { /** - * This static method create a connection to a business database. + * This method create a connection to a business database. * It retrieves connection information from the Database object. * - * Databae object stores information about a business database in + * Database object stores information about a business database in * the metamodel * - * @param Database $database This object contains the database connection parameters - * @param string $decryptedPassword This string contains the decrypted password for the database connection + * @param Database $database This object contains the database connection parameters * * @return Connection */ - public static function create(Database $database, string $decryptedPassword): Connection + public function create(Database $database): Connection { $config = new \Doctrine\DBAL\Configuration(); $connectionParams = array( 'dbname' => $database->getDbName(), 'user' => $database->getLogin(), - 'password' => $decryptedPassword, + 'password' => $database->getPassword(), 'host' => $database->getHost(), 'port' => $database->getPort(), 'driver' => $database->getType(), diff --git a/src/Utils/MetaEntityManagerFactory.php b/src/Utils/MetaEntityManagerFactory.php deleted file mode 100644 index eefa78c..0000000 --- a/src/Utils/MetaEntityManagerFactory.php +++ /dev/null @@ -1,98 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace App\Utils; - -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityManager; - -use App\Entity\Admin\Instance; - -/** - * Factory used to create a Doctrine Entity Manager to the project metamodel database - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Utils - */ -class MetaEntityManagerFactory -{ - /** - * Doctrine Entity Manager to the anis admin database - * - * @var EntityManagerInterface - */ - private $aem; - - /** - * Doctrine Entity Manager to the instance metamodel database - */ - private $mem; - - /** - * Create the classe with the Admin Entity Manager object - */ - public function __construct(EntityManagerInterface $aem) - { - $this->aem = $aem; - } - - /** - * Returns the instance metamodel doctrine entity manager - * - * @return EntityManagerInterface - */ - public function getMetaEntityManager(): EntityManagerInterface - { - return $this->mem; - } - - /** - * Create and return an Entity Manager to the correct metamodel database (databases, datasets, attributes...) - * - * @param string $instanceName Instance name necessary to find the correct metamodel database - */ - public function createMetaEntityManager(string $instanceName): void - { - // Get instance information from anis admin database - $instance = $this->aem->find('App\Entity\Admin\Instance', $instanceName); - - // Create the entity manager to drive the project metamodel database (databases, datasets, attributes...) - $c = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration( - array('../src/Entity/Metamodel'), - $instance->getDevMode() - ); - $c->setProxyDir($instance->getPathProxy()); - if ($instance->getDevMode()) { - $c->setAutoGenerateProxyClasses(true); - } else { - $c->setAutoGenerateProxyClasses(false); - } - $this->mem = EntityManager::create($this->getConnectionOptions($instance), $c); - } - - /** - * Returns the doctrine connection options array created with the $instance row object - * - * @param Instance $instance The doctrine object instance find with the $instanceName - * @return array - */ - private function getConnectionOptions(Instance $instance): array - { - return [ - 'driver' => $instance->getDriver(), - 'path' => $instance->getPath(), - 'host' => $instance->getHost(), - 'port' => $instance->getPort(), - 'dbname' => $instance->getDbName(), - 'user' => $instance->getLogin(), - 'password' => $instance->getPassword() - ]; - } -} diff --git a/src/Utils/Operator/Between.php b/src/Utils/Operator/Between.php index 9aad059..cc73891 100644 --- a/src/Utils/Operator/Between.php +++ b/src/Utils/Operator/Between.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/Equal.php b/src/Utils/Operator/Equal.php index 4a448b9..a076a67 100644 --- a/src/Utils/Operator/Equal.php +++ b/src/Utils/Operator/Equal.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/GreaterThan.php b/src/Utils/Operator/GreaterThan.php index 9f329ef..d4761d3 100644 --- a/src/Utils/Operator/GreaterThan.php +++ b/src/Utils/Operator/GreaterThan.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; @@ -47,7 +49,7 @@ class GreaterThan extends Operator * * @return string */ - public function getExpression() : string + public function getExpression(): string { return $this->expr->gt($this->column, $this->getSqlValue($this->value)); } diff --git a/src/Utils/Operator/GreaterThanEqual.php b/src/Utils/Operator/GreaterThanEqual.php index bb604a4..3cca25e 100644 --- a/src/Utils/Operator/GreaterThanEqual.php +++ b/src/Utils/Operator/GreaterThanEqual.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; @@ -47,7 +49,7 @@ class GreaterThanEqual extends Operator * * @return string */ - public function getExpression() : string + public function getExpression(): string { return $this->expr->gte($this->column, $this->getSqlValue($this->value)); } diff --git a/src/Utils/Operator/IOperator.php b/src/Utils/Operator/IOperator.php index 0de9c86..321ddc2 100644 --- a/src/Utils/Operator/IOperator.php +++ b/src/Utils/Operator/IOperator.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; /** diff --git a/src/Utils/Operator/IOperatorFactory.php b/src/Utils/Operator/IOperatorFactory.php index b07819c..1a31370 100644 --- a/src/Utils/Operator/IOperatorFactory.php +++ b/src/Utils/Operator/IOperatorFactory.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/In.php b/src/Utils/Operator/In.php index 5bf3a58..1e60529 100644 --- a/src/Utils/Operator/In.php +++ b/src/Utils/Operator/In.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/JsonPostgres.php b/src/Utils/Operator/JsonPostgres.php index 4a1c13e..94adbb0 100644 --- a/src/Utils/Operator/JsonPostgres.php +++ b/src/Utils/Operator/JsonPostgres.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/LessThan.php b/src/Utils/Operator/LessThan.php index b17a803..ea62bf3 100644 --- a/src/Utils/Operator/LessThan.php +++ b/src/Utils/Operator/LessThan.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/LessThanEqual.php b/src/Utils/Operator/LessThanEqual.php index 4ed4085..ec0315d 100644 --- a/src/Utils/Operator/LessThanEqual.php +++ b/src/Utils/Operator/LessThanEqual.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/Like.php b/src/Utils/Operator/Like.php index fe5f26a..6909a53 100644 --- a/src/Utils/Operator/Like.php +++ b/src/Utils/Operator/Like.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/NotEqual.php b/src/Utils/Operator/NotEqual.php index 6e6bb6f..26aaa68 100644 --- a/src/Utils/Operator/NotEqual.php +++ b/src/Utils/Operator/NotEqual.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/NotIn.php b/src/Utils/Operator/NotIn.php index ad67ef6..10ad192 100644 --- a/src/Utils/Operator/NotIn.php +++ b/src/Utils/Operator/NotIn.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/NotLike.php b/src/Utils/Operator/NotLike.php index 02895ec..bae1ce4 100644 --- a/src/Utils/Operator/NotLike.php +++ b/src/Utils/Operator/NotLike.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/Operator.php b/src/Utils/Operator/Operator.php index 3e6cf57..7ebd1f2 100644 --- a/src/Utils/Operator/Operator.php +++ b/src/Utils/Operator/Operator.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/OperatorException.php b/src/Utils/Operator/OperatorException.php index f381c3d..6695b1a 100644 --- a/src/Utils/Operator/OperatorException.php +++ b/src/Utils/Operator/OperatorException.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use App\Utils\SearchException; diff --git a/src/Utils/Operator/OperatorFactory.php b/src/Utils/Operator/OperatorFactory.php index 1afe7f0..f99612a 100644 --- a/src/Utils/Operator/OperatorFactory.php +++ b/src/Utils/Operator/OperatorFactory.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Platforms\AbstractPlatform; diff --git a/src/Utils/Operator/OperatorNotNull.php b/src/Utils/Operator/OperatorNotNull.php index 5a60312..4d58171 100644 --- a/src/Utils/Operator/OperatorNotNull.php +++ b/src/Utils/Operator/OperatorNotNull.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; diff --git a/src/Utils/Operator/OperatorNull.php b/src/Utils/Operator/OperatorNull.php index 817d541..de07b1d 100644 --- a/src/Utils/Operator/OperatorNull.php +++ b/src/Utils/Operator/OperatorNull.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils\Operator; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; @@ -20,18 +22,6 @@ use Doctrine\DBAL\Query\Expression\ExpressionBuilder; */ class OperatorNull extends Operator { - /** - * Create the class before call getExpression method to execute this operator - * - * @param ExpressionBuilder $expr - * @param string $column - * @param string $columnType - */ - public function __construct(ExpressionBuilder $expr, string $column, string $columnType) - { - parent::__construct($expr, $column, $columnType); - } - /** * This method returns the null expression for this criterion * diff --git a/src/Utils/SearchException.php b/src/Utils/SearchException.php index 74fcd62..a1380e8 100644 --- a/src/Utils/SearchException.php +++ b/src/Utils/SearchException.php @@ -1,13 +1,15 @@ -<?php declare(strict_types=1); +<?php + /* - * This file is part of ANIS SERVER API. + * This file is part of Anis Server. * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> + * (c) Laboratoire d'Astrophysique de Marseille / CNRS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +declare(strict_types=1); + namespace App\Utils; use Exception; diff --git a/src/dependencies.php b/src/dependencies.php deleted file mode 100644 index 1cd43f7..0000000 --- a/src/dependencies.php +++ /dev/null @@ -1,269 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -// DIC configuration -$container = $app->getContainer(); - -// Anis Error Handler -$container['errorHandler'] = function ($c) { - return new App\Utils\AnisErrorHandler( - $c->get('logger'), - $c->get('mailer'), - (bool) $c->get('settings')['displayErrorDetails'] - ); -}; - -// Admin database Doctrine 2 Entity Manager -$container['aem'] = function ($c) { - $settings = $c->get('settings'); - $adminDb = $settings['admin_db']; - $c = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration( - array('src/Entity/Admin'), - $adminDb['dev_mode'] - ); - $c->setProxyDir($adminDb['path_proxy']); - if ($adminDb['dev_mode']) { - $c->setAutoGenerateProxyClasses(true); - } else { - $c->setAutoGenerateProxyClasses(false); - } - $em = \Doctrine\ORM\EntityManager::create($adminDb['connection_options'], $c); - return $em; -}; - -// Monolog -$container['logger'] = function ($c) { - $settings = $c->get('settings'); - $logger = new \Monolog\Logger($settings['logger']['name']); - $logger->pushProcessor(new \Monolog\Processor\UidProcessor()); - $logger->pushHandler(new \Monolog\Handler\StreamHandler($settings['logger']['path'], $settings['logger']['level'])); - return $logger; -}; - -// swiftmailer -$container['mailer'] = function ($c) { - $settings = $c->get('settings'); - $transport = new \Swift_SmtpTransport($settings['mailer']['host'], $settings['mailer']['port']); - $mailer = new \Swift_Mailer($transport); - return $mailer; -}; - -// RabbitMQ -$container['amqp'] = function ($c) { - $settings = $c->get('settings'); - $amqp = new \PhpAmqpLib\Connection\AMQPStreamConnection( - $settings['amqp']['host'], - $settings['amqp']['port'], - $settings['amqp']['user'], - $settings['amqp']['password'] - ); - return $amqp; -}; - -// MetaEntityManagerFactory -$container['memf'] = function ($c) { - $adminEntityManager = $c->get('aem'); - return new App\Utils\MetaEntityManagerFactory($adminEntityManager); -}; - -// OperatorFactory -$container['of'] = function ($c) { - return new App\Utils\Operator\OperatorFactory(); -}; - -// DBAL Connection Factory -$container['dcf'] = function ($c) { - return new App\Utils\DBALConnectionFactory(); -}; - -// Middleware -$container['App\Middleware\CorsMiddleware'] = function ($c) { - return new App\Middleware\CorsMiddleware(); -}; - -// Root actions -$container['App\Action\Root\RootAction'] = function ($c) { - return new App\Action\Root\RootAction($c->get('logger')); -}; - -$container['App\Action\Root\OpenApiAction'] = function ($c) { - return new App\Action\Root\OpenApiAction($c->get('logger')); -}; - -// Login actions -$container['App\Action\Login\RegisterAction'] = function ($c) { - return new App\Action\Login\RegisterAction($c->get('logger'), $c->get('aem'), $c->get('mailer')); -}; - -$container['App\Action\Login\ActivateAccountAction'] = function ($c) { - return new App\Action\Login\ActivateAccountAction($c->get('logger'), $c->get('aem'), $c->get('mailer')); -}; - -$container['App\Action\Login\TokenAction'] = function ($c) { - // TODO: Augmenter la force de la signature du jeton - // On instancie le jwtBuilder à envoyer dans l'action login - $jwtBuilder = (new \Lcobucci\JWT\Builder()) - ->setIssuer('http://anis.lam.fr') // Emetteur du jeton - ->setAudience('http://anis.lam.fr') // Recepteur du jeton - ->setIssuedAt(time()) // Date à laquelle le jeton a été généré - ->setNotBefore(time()) // Date à laquelle le jeton pourra être utilisé - ->setExpiration(time() + 36000); // Date d'éxpiration du jeton - $jwtSigner = new \Lcobucci\JWT\Signer\Hmac\Sha256(); - return new App\Action\Login\TokenAction($c->get('logger'), $c->get('aem'), $jwtBuilder, $jwtSigner); -}; - -$container['App\Action\Login\NewPasswordAction'] = function ($c) { - return new App\Action\Login\NewPasswordAction($c->get('logger'), $c->get('aem'), $c->get('mailer')); -}; - -$container['App\Action\Login\ChangePasswordAction'] = function ($c) { - return new App\Action\Login\ChangePasswordAction($c->get('logger'), $c->get('aem'), $c->get('mailer')); -}; - -// Admin actions -$container['App\Action\Admin\UserListAction'] = function ($c) { - return new App\Action\Admin\UserListAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\UserAction'] = function ($c) { - return new App\Action\Admin\UserAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\SelectListAction'] = function ($c) { - return new App\Action\Admin\SelectListAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\SelectAction'] = function ($c) { - return new App\Action\Admin\SelectAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\OptionListAction'] = function ($c) { - return new App\Action\Admin\OptionListAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\OptionAction'] = function ($c) { - return new App\Action\Admin\OptionAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\InstanceListAction'] = function ($c) { - return new App\Action\Admin\InstanceListAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\InstanceAction'] = function ($c) { - return new App\Action\Admin\InstanceAction($c->get('logger'), $c->get('aem')); -}; - -$container['App\Action\Admin\MetamodelAction'] = function ($c) { - return new App\Action\Admin\MetamodelAction($c->get('logger'), $c->get('aem'), $c->get('memf')); -}; - -// Metamodel actions -$container['App\Action\Meta\DatabaseListAction'] = function ($c) { - $settings = $c->get('settings'); - return new App\Action\Meta\DatabaseListAction($c->get('logger'), $c->get('memf'), $settings['encryption_key']); -}; - -$container['App\Action\Meta\DatabaseAction'] = function ($c) { - $settings = $c->get('settings'); - return new App\Action\Meta\DatabaseAction($c->get('logger'), $c->get('memf'), $settings['encryption_key']); -}; - -$container['App\Action\Meta\TableListAction'] = function ($c) { - $settings = $c->get('settings'); - return new App\Action\Meta\TableListAction( - $c->get('logger'), - $c->get('memf'), - $c->get('dcf'), - $settings['encryption_key'] - ); -}; - -$container['App\Action\Meta\ProjectListAction'] = function ($c) { - return new App\Action\Meta\ProjectListAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\ProjectAction'] = function ($c) { - return new App\Action\Meta\ProjectAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\FamilyListAction'] = function ($c) { - return new App\Action\Meta\FamilyListAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\FamilyAction'] = function ($c) { - return new App\Action\Meta\FamilyAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\OutputCategoryListAction'] = function ($c) { - return new App\Action\Meta\OutputCategoryListAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\OutputCategoryAction'] = function ($c) { - return new App\Action\Meta\OutputCategoryAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\DatasetListAction'] = function ($c) { - $settings = $c->get('settings'); - return new App\Action\Meta\DatasetListAction( - $c->get('logger'), - $c->get('memf'), - $c->get('dcf'), - $settings['encryption_key'] - ); -}; - -$container['App\Action\Meta\DatasetAction'] = function ($c) { - return new App\Action\Meta\DatasetAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\AttributeAction'] = function ($c) { - return new App\Action\Meta\AttributeAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\AttributeListAction'] = function ($c) { - return new App\Action\Meta\AttributeListAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\FileProxyAction'] = function ($c) { - return new App\Action\Meta\FileProxyAction($c->get('memf')); -}; - -$container['App\Action\Meta\FileListAction'] = function ($c) { - return new App\Action\Meta\FileListAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\FileAction'] = function ($c) { - return new App\Action\Meta\FileAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\GroupListAction'] = function ($c) { - return new App\Action\Meta\GroupListAction($c->get('logger'), $c->get('memf')); -}; - -$container['App\Action\Meta\GroupAction'] = function ($c) { - return new App\Action\Meta\GroupAction($c->get('logger'), $c->get('memf')); -}; - -// Search actions -$container['App\Action\Search\SearchAction'] = function ($c) { - $settings = $c->get('settings'); - return new App\Action\Search\SearchAction( - $c->get('logger'), - $c->get('memf'), - $c->get('dcf'), - $c->get('of'), - $settings['encryption_key'] - ); -}; - -$container['App\Action\Search\ServiceAction'] = function ($c) { - return new App\Action\Search\ServiceAction($c->get('logger'), $c->get('memf'), $c->get('amqp')); -}; diff --git a/src/middleware.php b/src/middleware.php deleted file mode 100644 index a5ee05b..0000000 --- a/src/middleware.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -$app->add(App\Middleware\CorsMiddleware::class); diff --git a/src/routes.php b/src/routes.php deleted file mode 100644 index 83d3ed8..0000000 --- a/src/routes.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -$app->get('/', App\Action\Root\RootAction::class); -$app->get('/open-api', App\Action\Root\OpenApiAction::class); - -$app->group('/login', function (Slim\App $app) { - $app->map(['OPTIONS', 'POST'], '/register', App\Action\Login\RegisterAction::class); - $app->map(['OPTIONS', 'GET'], '/activate-account', App\Action\Login\ActivateAccountAction::class); - $app->map(['OPTIONS', 'POST'], '/token', App\Action\Login\TokenAction::class); - $app->map(['OPTIONS', 'POST'], '/new-password', App\Action\Login\NewPasswordAction::class); - $app->map(['OPTIONS', 'POST'], '/change-password', App\Action\Login\ChangePasswordAction::class); -}); - -$app->group('/admin', function (Slim\App $app) { - $app->map(['OPTIONS', 'GET'], '/user', App\Action\Admin\UserListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/user/{email}', App\Action\Admin\UserAction::class); - $app->group('/settings', function (Slim\App $app) { - $app->map(['OPTIONS', 'GET', 'POST'], '/select', App\Action\Admin\SelectListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/select/{id}', App\Action\Admin\SelectAction::class); - $app->map(['OPTIONS', 'GET', 'POST'], '/option', App\Action\Admin\OptionListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/option/{id}', App\Action\Admin\OptionAction::class); - }); - $app->map(['OPTIONS', 'GET', 'POST'], '/instance', App\Action\Admin\InstanceListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/instance/{name}', App\Action\Admin\InstanceAction::class); - $app->map(['OPTIONS', 'GET'], '/instance/{name}/{action}', App\Action\Admin\MetamodelAction::class); -}); - -$app->group('/metadata', function (Slim\App $app) { - $app->group('/{instance}', function (Slim\App $app) { - $app->map(['OPTIONS', 'GET', 'POST'], '/database', App\Action\Meta\DatabaseListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/database/{id}', App\Action\Meta\DatabaseAction::class); - $app->map(['OPTIONS', 'GET'], '/database/{id}/table', App\Action\Meta\TableListAction::class); - $app->map(['OPTIONS', 'GET', 'POST'], '/project', App\Action\Meta\ProjectListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/project/{name}', App\Action\Meta\ProjectAction::class); - $app->map(['OPTIONS', 'GET', 'POST'], '/family/{type}', App\Action\Meta\FamilyListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/family/{type}/{id}', App\Action\Meta\FamilyAction::class); - $app->map(['OPTIONS', 'GET', 'POST'], '/output-category', App\Action\Meta\OutputCategoryListAction::class); - $app->map( - ['OPTIONS', 'GET', 'PUT', 'DELETE'], - '/output-category/{id}', - App\Action\Meta\OutputCategoryAction::class - ); - $app->map(['OPTIONS', 'GET', 'POST'], '/dataset', App\Action\Meta\DatasetListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/dataset/{name}', App\Action\Meta\DatasetAction::class); - $app->map(['OPTIONS', 'GET'], '/dataset/{name}/attribute', App\Action\Meta\AttributeListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT'], '/dataset/{name}/attribute/{id}', App\Action\Meta\AttributeAction::class); - $app->map(['OPTIONS', 'GET'], '/dataset/{name}/get-file/{path}', App\Action\Meta\FileProxyAction::class); - $app->map(['OPTIONS', 'GET', 'POST'], '/dataset/{name}/file', App\Action\Meta\FileListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/file/{id}', App\Action\Meta\FileAction::class); - $app->map(['OPTIONS', 'GET', 'POST'], '/group', App\Action\Meta\GroupListAction::class); - $app->map(['OPTIONS', 'GET', 'PUT', 'DELETE'], '/group/{id}', App\Action\Meta\GroupAction::class); - }); -}); - -$app->map(['OPTIONS', 'GET'], '/search/{instance}/{type}/{dname}', App\Action\Search\SearchAction::class); -$app->map(['OPTIONS', 'GET'], '/service/{instance}/{dname}', App\Action\Search\ServiceAction::class); diff --git a/src/settings.php b/src/settings.php deleted file mode 100644 index 4d4e029..0000000 --- a/src/settings.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php declare(strict_types=1); -/* - * This file is part of ANIS SERVER API. - * - * (c) François Agneray <francois.agneray@lam.fr> - * (c) Chrystel Moreau <chrystel.moreau@lam.fr> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -return [ - 'settings' => [ - // app config - 'encryption_key' => 'r3Q8C7LgIrRcTtI8I6EPzFwrDXJ4adgnGQ9V/pWVI8M=', - // slim framework settings - 'displayErrorDetails' => (bool) getenv('SLIM_DISPLAY_ERROR_DETAILS'), - 'determineRouteBeforeAppMiddleware' => false, - 'addContentLengthHeader' => false, // Allow the web server to send the content-length header - // metadata settings (doctrine 2) - 'admin_db' => [ - 'path_proxy' => getenv('METADATA_DOCTRINE_PATH_PROXY'), - 'dev_mode' => getenv('METADATA_DOCTRINE_DEV_MODE'), - 'connection_options' => [ - 'driver' => getenv('METADATA_DB_DRIVER'), - 'path' => getenv('METADATA_DB_PATH'), - 'host' => getenv('METADATA_DB_HOST'), - 'port' => getenv('METADATA_DB_PORT'), - 'dbname' => getenv('METADATA_DB_DBNAME'), - 'user' => getenv('METADATA_DB_USER'), - 'password' => getenv('METADATA_DB_PASSWORD') - ] - ], - // monolog settings - 'logger' => [ - 'name' => getenv('LOGGER_NAME'), - 'path' => getenv('LOGGER_PATH'), - 'level' => getenv('LOGGER_LEVEL') - ], - // Swiftmailer settings - 'mailer' => [ - 'host' => getenv('MAILER_HOST'), - 'port' => getenv('MAILER_PORT') - ], - // RabbitMQ settings - 'amqp' => [ - 'host' => getenv('AMQP_HOST'), - 'port' => getenv('AMQP_PORT'), - 'user' => getenv('AMQP_USER'), - 'password' => getenv('AMQP_PASSWORD') - ] - ] -]; diff --git a/tests/AbstractActionAdminTestCase.php b/tests/AbstractActionAdminTestCase.php deleted file mode 100644 index 3bc0967..0000000 --- a/tests/AbstractActionAdminTestCase.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests; - -use PHPUnit\DbUnit\TestCase; -use PHPUnit\DbUnit\DataSet\YamlDataSet; -use Doctrine\ORM\Tools\Setup; -use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Tools\SchemaTool; - -abstract class AbstractActionAdminTestCase extends TestCase -{ - protected $entityManager; - protected $mailer; - protected $action; - - protected function setUp(): void - { - parent::setUp(); - $transport = new \Swift_Transport_SpoolTransport( - new \Swift_Events_SimpleEventDispatcher(), - new \Swift_MemorySpool - ); - $this->mailer = new \Swift_Mailer($transport); - } - - protected function getConnection() - { - $isDevMode = true; - $config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/../src/Entity/Admin"), $isDevMode); - $conn = array( - 'driver' => 'pdo_sqlite', - 'memory' => true - ); - $this->entityManager = EntityManager::create($conn, $config); - $schemaTool = new SchemaTool($this->entityManager); - $schemaTool->createSchema($this->entityManager->getMetadataFactory()->getAllMetadata()); - $pdo = $this->entityManager->getConnection()->getWrappedConnection(); - return $this->createDefaultDBConnection($pdo, ':memory'); - } - - protected function getDataSet() - { - return new YamlDataset(__DIR__ . "/admin.yaml"); - } -} diff --git a/tests/AbstractActionMetaTestCase.php b/tests/AbstractActionMetaTestCase.php deleted file mode 100644 index 52cd5d2..0000000 --- a/tests/AbstractActionMetaTestCase.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests; - -use PHPUnit\DbUnit\TestCase; -use PHPUnit\DbUnit\DataSet\YamlDataSet; -use Doctrine\ORM\Tools\Setup; -use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Tools\SchemaTool; - -use App\Utils\MetaEntityManagerFactory; - -abstract class AbstractActionMetaTestCase extends TestCase -{ - protected $metaEntityManagerFactory; - protected $entityManager; - protected $mailer; - protected $action; - - protected function setUp(): void - { - parent::setUp(); - $transport = new \Swift_Transport_SpoolTransport( - new \Swift_Events_SimpleEventDispatcher(), - new \Swift_MemorySpool - ); - $this->mailer = new \Swift_Mailer($transport); - $this->metaEntityManagerFactory = $this->getMockBuilder(MetaEntityManagerFactory::class) - ->disableOriginalConstructor() - ->setMethods(['getMetaEntityManager', 'createMetaEntityManager']) - ->getMock(); - - $this->metaEntityManagerFactory->method('getMetaEntityManager')->willReturn($this->entityManager); - } - - protected function getConnection() - { - $isDevMode = true; - $config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/../src/Entity/Metamodel"), $isDevMode); - $conn = array( - 'driver' => 'pdo_sqlite', - 'memory' => true - ); - $this->entityManager = EntityManager::create($conn, $config); - $schemaTool = new SchemaTool($this->entityManager); - $schemaTool->createSchema($this->entityManager->getMetadataFactory()->getAllMetadata()); - $pdo = $this->entityManager->getConnection()->getWrappedConnection(); - return $this->createDefaultDBConnection($pdo, ':memory'); - } - - protected function getDataSet() - { - return new YamlDataset(__DIR__ . "/database.yaml"); - } -} diff --git a/tests/Action/AttributeActionTest.php b/tests/Action/AttributeActionTest.php new file mode 100644 index 0000000..4402516 --- /dev/null +++ b/tests/Action/AttributeActionTest.php @@ -0,0 +1,250 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\DatasetFamily; +use App\Entity\CriteriaFamily; +use App\Entity\OutputFamily; +use App\Entity\OutputCategory; +use App\Entity\Dataset; +use App\Entity\Attribute; + +final class AttributeActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\AttributeAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, OPTIONS'); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + public function testAttributeIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Attribute with dataset name obs_cat and attribute id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat', 'id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAnAttributeById(): void + { + $attribute = $this->addAnAttribute(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat', 'id' => 1)); + $this->assertSame(json_encode($attribute), (string) $response->getBody()); + } + + public function testEditADatabase(): void + { + $this->addAnAttribute(); + $this->addCriteriaFamily(); + $this->addOutputCategory(); + $fields = $this->getEditAttributeFields(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat', 'id' => 1)); + $this->assertSame( + json_encode(array_merge(['id' => 1, 'name' => 'id', 'table_name' => 'v_obs_cat'], $fields)), + (string) $response->getBody() + ); + } + + public function testEditADatabaseWithCriteriaFamilyAndOutputCategoryNull(): void + { + $this->addAnAttribute(); + $fields = $this->getEditAttributeFields(); + $fields['id_criteria_family'] = null; + $fields['id_output_category'] = null; + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat', 'id' => 1)); + $this->assertSame( + json_encode(array_merge(['id' => 1, 'name' => 'id', 'table_name' => 'v_obs_cat'], $fields)), + (string) $response->getBody() + ); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset/obs_cat/attribute/1', array( + 'Content-Type' => 'application/json' + )); + } + + private function getEditAttributeFields(): array + { + return array( + 'label' => 'ID', + 'form_label' => 'ID', + 'description' => 'ID description', + 'output_display' => 20, + 'criteria_display' => 10, + 'search_flag' => 'ID', + 'search_type' => null, + 'type' => 'integer', + 'operator' => null, + 'min' => null, + 'max' => null, + 'placeholder_min' => null, + 'placeholder_max' => null, + 'uri_action' => null, + 'renderer' => null, + 'display_detail' => 10, + 'selected' => false, + 'order_by' => false, + 'order_display' => null, + 'detail' => false, + 'renderer_detail' => null, + 'options' => null, + 'vo_utype' => null, + 'vo_ucd' => null, + 'vo_unit' => null, + 'vo_description' => null, + 'vo_datatype' => null, + 'vo_size' => 0, + 'id_criteria_family' => 1, + 'id_output_category' => 1 + ); + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + return $project; + } + + private function addDatasetFamily(): DatasetFamily + { + $family = new DatasetFamily(); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + return $family; + } + + private function addCriteriaFamily(): CriteriaFamily + { + $family = new CriteriaFamily(); + $family->setLabel('Default criteria'); + $family->setDisplay(10); + $this->entityManager->persist($family); + $this->entityManager->flush(); + + return $family; + } + + private function addOutputCategory(): OutputCategory + { + $outputFamily = $this->addOutputFamily(); + + $outputCategory = new OutputCategory(); + $outputCategory->setLabel('Default output category'); + $outputCategory->setDisplay(10); + $outputCategory->setOutputFamily($outputFamily); + $this->entityManager->persist($outputCategory); + $this->entityManager->flush(); + return $outputCategory; + } + + private function addOutputFamily(): OutputFamily + { + $family = new OutputFamily(); + $family->setLabel('Default output'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + + return $dataset; + } + + private function addAnAttribute(): Attribute + { + $dataset = $this->addADataset(); + + $attribute = new Attribute(1, $dataset); + $attribute->setName('id'); + $attribute->setTableName($dataset->getTableRef()); + $attribute->setLabel('ID'); + $attribute->setFormLabel('ID'); + $attribute->setType('integer'); + $attribute->setCriteriaDisplay(10); + $attribute->setOutputDisplay(10); + $attribute->setDisplayDetail(10); + $attribute->setOrderDisplay(10); + $attribute->setSelected(true); + $this->entityManager->persist($attribute); + $this->entityManager->flush(); + + return $attribute; + } +} diff --git a/tests/Action/AttributeListActionTest.php b/tests/Action/AttributeListActionTest.php new file mode 100644 index 0000000..f49816c --- /dev/null +++ b/tests/Action/AttributeListActionTest.php @@ -0,0 +1,165 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; +use App\Entity\Attribute; + +final class AttributeListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\AttributeListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, OPTIONS'); + } + + public function testDatasetIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Dataset with name obs_cat is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAllAttributesOfADataset(): void + { + $attributes = $this->addAttributes(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame( + json_encode($attributes), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset/obs_cat/attribute', array( + 'Content-Type' => 'application/json' + )); + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + return $project; + } + + private function addDatasetFamily(): DatasetFamily + { + $family = new DatasetFamily(); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + + return $dataset; + } + + private function addAttributes(): array + { + $dataset = $this->addADataset(); + + $attribute1 = new Attribute(1, $dataset); + $attribute1->setName('id'); + $attribute1->setTableName($dataset->getTableRef()); + $attribute1->setLabel('ID'); + $attribute1->setFormLabel('ID'); + $attribute1->setType('integer'); + $attribute1->setCriteriaDisplay(10); + $attribute1->setOutputDisplay(10); + $attribute1->setDisplayDetail(10); + $attribute1->setOrderDisplay(10); + $attribute1->setSelected(true); + $this->entityManager->persist($attribute1); + + $attribute2 = new Attribute(2, $dataset); + $attribute2->setName('ra'); + $attribute2->setTableName($dataset->getTableRef()); + $attribute2->setLabel('RA'); + $attribute2->setFormLabel('RA'); + $attribute2->setType('float'); + $attribute2->setCriteriaDisplay(20); + $attribute2->setOutputDisplay(20); + $attribute2->setDisplayDetail(20); + $attribute2->setOrderDisplay(20); + $attribute2->setSelected(true); + $this->entityManager->persist($attribute2); + + $this->entityManager->flush(); + return array($attribute1, $attribute2); + } +} diff --git a/tests/Action/DatabaseActionTest.php b/tests/Action/DatabaseActionTest.php new file mode 100644 index 0000000..ffb140d --- /dev/null +++ b/tests/Action/DatabaseActionTest.php @@ -0,0 +1,122 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; + +final class DatabaseActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\DatabaseAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testDatabaseIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Database with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetADatabaseById(): void + { + $database = $this->addADatabase(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode($database), (string) $response->getBody()); + } + + public function testEditADatabaseEmptyLabelField(): void + { + $this->addADatabase(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the database'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditADatabase(): void + { + $fields = array( + 'label' => 'New_label', + 'dbname' => 'test2', + 'dbtype' => 'pgsql', + 'dbhost' => 'db', + 'dbport' => 5432, + 'dblogin' => 'test', + 'dbpassword' => 'test' + ); + $this->addADatabase(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode(array_merge(['id' => 1], $fields)), (string) $response->getBody()); + } + + public function testDeleteADatabase(): void + { + $this->addADatabase(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame( + json_encode(array('message' => 'Database with id 1 is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/database/1', array( + 'Content-Type' => 'application/json' + )); + } + + private function addADatabase(): Database + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + $this->entityManager->flush(); + return $database; + } +} diff --git a/tests/Action/DatabaseListActionTest.php b/tests/Action/DatabaseListActionTest.php new file mode 100644 index 0000000..9344073 --- /dev/null +++ b/tests/Action/DatabaseListActionTest.php @@ -0,0 +1,117 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; + +final class DatabaseListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\DatabaseListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testGetAllDatabases(): void + { + $databases = $this->addDatabases(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($databases), + (string) $response->getBody() + ); + } + + public function testAddANewDatabaseEmptyLabelField(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to add a new database'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewDatabase(): void + { + $fields = array( + 'label' => 'Test1', + 'dbname' => 'test1', + 'dbtype' => 'pgsql', + 'dbhost' => 'db', + 'dbport' => 5432, + 'dblogin' => 'test', + 'dbpassword' => 'test' + ); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields)), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/database', array( + 'Content-Type' => 'application/json' + )); + } + + private function addDatabases(): array + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $database2 = new Database(); + $database2->setLabel('Test2'); + $database2->setDbName('test2'); + $database2->setType('pgsql'); + $database2->setHost('db'); + $database2->setPort(5432); + $database2->setLogin('test'); + $database2->setPassword('test'); + $this->entityManager->persist($database2); + + $this->entityManager->flush(); + return array($database, $database2); + } +} diff --git a/tests/Action/DatasetActionTest.php b/tests/Action/DatasetActionTest.php new file mode 100644 index 0000000..d3f4f54 --- /dev/null +++ b/tests/Action/DatasetActionTest.php @@ -0,0 +1,196 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; + +final class DatasetActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\DatasetAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testDatasetIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Dataset with name obs_cat is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetADatasetByName(): void + { + $dataset = $this->addADataset(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame(json_encode($dataset, JSON_UNESCAPED_SLASHES), (string) $response->getBody()); + } + + public function testEditADatasetEmptyLabelField(): void + { + $dataset = $this->addADataset(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the dataset'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditADatasetFamilyNotFound(): void + { + $dataset = $this->addADataset(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Dataset family with id 2 is not found'); + $fields = $this->getEditDatasetFields(); + $fields['id_dataset_family'] = 2; + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditADataset(): void + { + $this->addADataset(); + $fields = $this->getEditDatasetFields(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + unset($fields['id_dataset_family']); + $this->assertSame( + json_encode( + array_merge( + ['name' => 'obs_cat', 'table_ref' => 'v_obs_cat'], + $fields, + ['project_name' => 'anis_project', 'id_dataset_family' => 1] + ), + JSON_UNESCAPED_SLASHES + ), + (string) $response->getBody() + ); + $this->assertEquals(200, (int) $response->getStatusCode()); + } + + public function testDeleteADatabase(): void + { + $this->addADataset(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame( + json_encode(array('message' => 'Dataset with name obs_cat is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset/obs_cat', array( + 'Content-Type' => 'application/json' + )); + } + + private function getEditDatasetFields(): array + { + return array( + 'label' => 'Dataset1 label modified', + 'description' => 'Dataset1 description', + 'display' => 20, + 'count' => 5000, + 'vo' => false, + 'data_path' => '/mnt/dataset1', + 'selectable_row' => false, + 'id_dataset_family' => 1 + ); + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addDatasetFamily(): DatasetFamily + { + $family = new DatasetFamily(); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } +} diff --git a/tests/Action/DatasetListActionTest.php b/tests/Action/DatasetListActionTest.php new file mode 100644 index 0000000..dfcea82 --- /dev/null +++ b/tests/Action/DatasetListActionTest.php @@ -0,0 +1,234 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\SqliteSchemaManager; +use Doctrine\DBAL\Schema\Column; +use App\Utils\DBALConnectionFactory; +use Doctrine\DBAL\Types\Type; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; + +final class DatasetListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\DatasetListAction($this->entityManager, $this->getConnectionFactory()); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testGetAllDatasets(): void + { + $datasets = $this->addDatasets(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($datasets), + (string) $response->getBody() + ); + } + + public function testAddANewDatasetEmptyNameField(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param name needed to add a new dataset'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewDatasetProjectNotFound(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Project with name anis_project is not found'); + $fields = $this->getNewDatasetFields(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewDatasetFamilyNotFound(): void + { + $this->addProject(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Dataset family with id 1 is not found'); + $fields = $this->getNewDatasetFields(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewDataset(): void + { + $this->addProject(); + $this->addDatasetFamily(); + $fields = $this->getNewDatasetFields(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($fields), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset', array( + 'Content-Type' => 'application/json' + )); + } + + private function getNewDatasetFields(): array + { + return array( + 'name' => 'dataset1', + 'table_ref' => 'table1', + 'label' => 'Dataset1 label', + 'description' => 'Dataset1 description', + 'display' => 10, + 'count' => 10000, + 'vo' => false, + 'data_path' => '/mnt/dataset1', + 'selectable_row' => false, + 'project_name' => 'anis_project', + 'id_dataset_family' => 1 + ); + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addDatasetFamily(): DatasetFamily + { + $family = new DatasetFamily(); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addDatasets(): array + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset1 = new Dataset('dataset1'); + $dataset1->setTableRef('table1'); + $dataset1->setLabel('Dataset1 label'); + $dataset1->setDescription('Dataset1 description'); + $dataset1->setDisplay(10); + $dataset1->setCount(10000); + $dataset1->setVo(false); + $dataset1->setDataPath('/mnt/dataset1'); + $dataset1->setSelectableRow(false); + $dataset1->setProject($project); + $dataset1->setDatasetFamily($family); + $this->entityManager->persist($dataset1); + + $dataset2 = new Dataset('dataset2'); + $dataset2->setTableRef('table2'); + $dataset2->setLabel('Dataset2 label'); + $dataset2->setDescription('Dataset2 description'); + $dataset2->setDisplay(20); + $dataset2->setCount(5000); + $dataset2->setVo(false); + $dataset2->setDataPath('/mnt/dataset2'); + $dataset2->setSelectableRow(false); + $dataset2->setProject($project); + $dataset2->setDatasetFamily($family); + $this->entityManager->persist($dataset2); + + $this->entityManager->flush(); + return array($dataset1, $dataset2); + } + + private function getConnectionFactory(): DBALConnectionFactory + { + $schemaManager = $this->getMockBuilder(SqliteSchemaManager::class) + ->disableOriginalConstructor() + ->setMethods(['listTableColumns']) + ->getMock(); + + $schemaManager->method('listTableColumns') + ->will($this->returnValue(array( + new Column('id', Type::getType(Type::INTEGER)), + new Column('ra', Type::getType(Type::FLOAT)), + new Column('dec', Type::getType(TYPE::FLOAT)) + ))); + + $connection = $this->getMockBuilder(Connection::class) + ->disableOriginalConstructor() + ->setMethods(['getSchemaManager']) + ->getMock(); + + $connection->method('getSchemaManager') + ->will($this->returnValue($schemaManager)); + + $connectionFactory = $this->getMockBuilder(DBALConnectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $connectionFactory->method('create') + ->will($this->returnValue($connection)); + + return $connectionFactory; + } +} diff --git a/tests/Action/FamilyActionTest.php b/tests/Action/FamilyActionTest.php new file mode 100644 index 0000000..313d9a2 --- /dev/null +++ b/tests/Action/FamilyActionTest.php @@ -0,0 +1,124 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\DatasetFamily; + +final class FamilyActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\FamilyAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testTypeIsNotDefined(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Type undifined is not defined'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('type' => 'undifined')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testDatasetFamilyIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Dataset family with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetADatasetFamilyById(): void + { + $family = $this->addADatasetFamily(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); + $this->assertSame(json_encode($family), (string) $response->getBody()); + } + + public function testEditADatasetFamilyEmptyLabelField(): void + { + $this->addADatasetFamily(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the dataset family'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditADatasetFamily(): void + { + $fields = array( + 'label' => 'New_label', + 'display' => 20 + ); + $this->addADatasetFamily(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset', 'datasets' => array()])), + (string) $response->getBody() + ); + } + + public function testDeleteADatasetFamily(): void + { + $this->addADatasetFamily(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); + $this->assertSame( + json_encode(array('message' => 'Dataset family with id 1 is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/family/dataset/1', array( + 'Content-Type' => 'application/json' + )); + } + + private function addADatasetFamily(): DatasetFamily + { + $family = new DatasetFamily(); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + $this->entityManager->flush(); + return $family; + } +} diff --git a/tests/Action/FamilyListActionTest.php b/tests/Action/FamilyListActionTest.php new file mode 100644 index 0000000..2c7ab2b --- /dev/null +++ b/tests/Action/FamilyListActionTest.php @@ -0,0 +1,111 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\DatasetFamily; + +final class FamilyListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\FamilyListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testTypeIsNotDefined(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Type undifined is not defined'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('type' => 'undifined')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testGetAllDatasetFamilies(): void + { + $families = $this->addDatasetFamilies(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('type' => 'dataset')); + $this->assertSame( + json_encode($families), + (string) $response->getBody() + ); + } + + public function testAddANewFamilyEmptyLabelField(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to add a new family'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('type' => 'dataset')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewDatabase(): void + { + $fields = array( + 'label' => 'Default family', + 'display' => 10 + ); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('type' => 'dataset')); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset', 'datasets' => array()])), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/family/dataset', array( + 'Content-Type' => 'application/json' + )); + } + + private function addDatasetFamilies(): array + { + $family1 = new DatasetFamily(); + $family1->setLabel('Default dataset'); + $family1->setDisplay(10); + $this->entityManager->persist($family1); + + $family2 = new DatasetFamily(); + $family2->setLabel('My family dataset'); + $family2->setDisplay(20); + $this->entityManager->persist($family2); + + $this->entityManager->flush(); + return array($family1, $family2); + } +} diff --git a/tests/Action/Login/ActivateAccountActionTest.php b/tests/Action/Login/ActivateAccountActionTest.php deleted file mode 100644 index 31640b4..0000000 --- a/tests/Action/Login/ActivateAccountActionTest.php +++ /dev/null @@ -1,86 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Login; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionAdminTestCase; - -final class ActivateAccountActionTest extends AbstractActionAdminTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Login\ActivateAccountAction( - new \Psr\Log\NullLogger(), - $this->entityManager, - $this->mailer - ); - } - - protected function getRequest(string $queryString): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/activate-account', - 'QUERY_STRING' => $queryString - ]); - return \Slim\Http\Request::createFromEnvironment($environment); - } - - public function testEmailIsEmpty(): void - { - $request = $this->getRequest('activation_key=noioioi'); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param email needed to activate a new user account' - ))); - } - - public function testActivationKeyIsEmpty(): void - { - $request = $this->getRequest('email=user1@anis.fr'); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param activation_key needed to activate a new user account' - ))); - } - - public function testUserDoNotExist(): void - { - $request = $this->getRequest('email=user99@anis.fr&activation_key=noioioi'); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'No account is identified with this email address' - ))); - } - - public function testBadActivationKey(): void - { - $request = $this->getRequest('email=user1@anis.fr&activation_key=badkey'); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid activation key', - 'error_description' => 'Bad activation key; Unable to activate account' - ))); - } - - public function testActivateAccount(): void - { - $request = $this->getRequest('email=user1@anis.fr&activation_key=noioioi'); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - '{"email":"user1@anis.fr","activated":true,"adminsi":false,"superuser":false}' - ); - } -} diff --git a/tests/Action/Login/ChangePasswordActionTest.php b/tests/Action/Login/ChangePasswordActionTest.php deleted file mode 100644 index 37b73c8..0000000 --- a/tests/Action/Login/ChangePasswordActionTest.php +++ /dev/null @@ -1,121 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Login; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionAdminTestCase; - -final class ChangePasswordActionTest extends AbstractActionAdminTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Login\ChangePasswordAction( - new \Psr\Log\NullLogger(), - $this->entityManager, - $this->mailer - ); - } - - protected function getRequest(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/change-password', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testEmailIsEmpty(): void - { - $request = $this->getRequest(['password' => 'password', 'new_password' => 'new_password']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param email needed to change your password' - ))); - } - - public function testPasswordIsEmpty(): void - { - $request = $this->getRequest(['email' => 'user2@anis.fr', 'new_password' => 'new_password']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param password needed to change your password' - ))); - } - - public function testNewPasswordIsEmpty(): void - { - $request = $this->getRequest(['email' => 'user2@anis.fr', 'password' => 'password']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param new_password needed to change your password' - ))); - } - - public function testUserDoNotExist(): void - { - $request = $this->getRequest(array( - 'email' => 'nobody@anis.fr', - 'password' => 'password', - 'new_password' => 'new_password' - )); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'No account is identified with this email address' - ))); - } - - public function testAccountNotActivated(): void - { - $request = $this->getRequest(array( - 'email' => 'user1@anis.fr', - 'password' => 'password', - 'new_password' => 'new_password' - )); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'Account not yet activated' - ))); - } - - public function testBadPassword(): void - { - $request = $this->getRequest(array( - 'email' => 'user2@anis.fr', - 'password' => 'badpassword', - 'new_password' => 'new_password' - )); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid password', - 'error_description' => 'Bad password; unable to change the password' - ))); - } - - public function testChangePassword(): void - { - $request = $this->getRequest(array( - 'email' => 'user2@anis.fr', - 'password' => 'password', - 'new_password' => 'new_password' - )); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array('message' => 'Password changed!'))); - } -} diff --git a/tests/Action/Login/NewPasswordActionTest.php b/tests/Action/Login/NewPasswordActionTest.php deleted file mode 100644 index e0ef478..0000000 --- a/tests/Action/Login/NewPasswordActionTest.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Login; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionAdminTestCase; - -final class NewPasswordActionTest extends AbstractActionAdminTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Login\NewPasswordAction( - new \Psr\Log\NullLogger(), - $this->entityManager, - $this->mailer - ); - } - - protected function getRequest(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/new-password', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testEmailIsEmpty(): void - { - $request = $this->getRequest([]); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param email needed to generate a new password' - ))); - } - - public function testUserDoNotExist(): void - { - $request = $this->getRequest(['email' => 'nobody@anis.fr']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'No account is identified with this email address' - ))); - } - - public function testAccountNotActivated(): void - { - $request = $this->getRequest(['email' => 'user1@anis.fr']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'Account not yet activated' - ))); - } - - public function testNewPassword(): void - { - $request = $this->getRequest(['email' => 'user2@anis.fr']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array('message' => 'Password re-generated!'))); - } -} diff --git a/tests/Action/Login/RegisterActionTest.php b/tests/Action/Login/RegisterActionTest.php deleted file mode 100644 index 4ff7cad..0000000 --- a/tests/Action/Login/RegisterActionTest.php +++ /dev/null @@ -1,98 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Login; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionAdminTestCase; - -final class RegisterActionTest extends AbstractActionAdminTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Login\RegisterAction( - new \Psr\Log\NullLogger(), - $this->entityManager, - $this->mailer - ); - } - - protected function getRequest(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/register', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testEmailIsEmpty(): void - { - $request = $this->getRequest(['password' => 'test']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param email needed to register a new user' - ))); - } - - public function testPasswordIsEmpty(): void - { - $request = $this->getRequest(['email' => 'test@anis.fr']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param password needed to register a new user' - ))); - } - - public function testExistUser(): void - { - $request = $this->getRequest([ - 'email' => 'user1@anis.fr', - 'password' => 'test' - ]); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'A user with the email address user1@anis.fr already exists' - ))); - } - - public function testInvalidEmail(): void - { - $request = $this->getRequest([ - 'email' => 'user1@anis', - 'password' => 'test' - ]); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $error_description = 'Bad email adress; a well-defined email address is needed to finalize the registration'; - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid email', - 'error_description' => $error_description - ))); - } - - public function testRegisterPost(): void - { - $this->assertSame(2, $this->getDatabaseTester()->getConnection()->getRowCount('anis_user')); - $request = $this->getRequest([ - 'email' => 'test@anis.fr', - 'password' => 'test' - ]); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - '{"email":"test@anis.fr","activated":false,"adminsi":false,"superuser":false}' - ); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('anis_user')); - } -} diff --git a/tests/Action/Login/TokenActionTest.php b/tests/Action/Login/TokenActionTest.php deleted file mode 100644 index fc0a71c..0000000 --- a/tests/Action/Login/TokenActionTest.php +++ /dev/null @@ -1,120 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Login; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Lcobucci\JWT\Builder as JwtBuilder; -use Lcobucci\JWT\Signer as JwtSigner; -use Lcobucci\JWT\Token; - -use App\Tests\AbstractActionAdminTestCase; - -final class TokenActionTest extends AbstractActionAdminTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Login\TokenAction( - new \Psr\Log\NullLogger(), - $this->entityManager, - $this->getMockJwtBuilder(), - $this->createMock(JwtSigner::class) - ); - } - - protected function getMockJwtBuilder(): JwtBuilder - { - $mockJwtBuilder = $this->createMock(JwtBuilder::class); - $mockToken = $this->createMock(Token::class); - $mockToken - ->method('__toString') - ->willReturn('supertoken'); - $mockJwtBuilder - ->method('set') - ->willReturn($mockJwtBuilder); - $mockJwtBuilder - ->method('sign') - ->willReturn($mockJwtBuilder); - $mockJwtBuilder - ->method('getToken') - ->willReturn($mockToken); - return $mockJwtBuilder; - } - - protected function getRequest(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/login', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testEmailIsEmpty(): void - { - $request = $this->getRequest(['password' => 'password']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param email needed to login' - ))); - } - - public function testPasswordIsEmpty(): void - { - $request = $this->getRequest(['email' => 'user2@anis.fr']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param password needed to login' - ))); - } - - public function testUserDoNotExist(): void - { - $request = $this->getRequest(['email' => 'nobody@anis.fr', 'password' => 'pass']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'No account is identified with this email address' - ))); - } - - public function testAccountNotActivated(): void - { - $request = $this->getRequest(['email' => 'user1@anis.fr', 'password' => 'sfpsdfpsdmsdlsdfdf']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid user', - 'error_description' => 'Account not yet activated' - ))); - } - - public function testBadPassword(): void - { - $request = $this->getRequest(['email' => 'user2@anis.fr', 'password' => 'badpassword']); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid password', - 'error_description' => 'Bad password; unable to login' - ))); - } - - public function testLogin(): void - { - $request = $this->getRequest([ - 'email' => 'user2@anis.fr', - 'password' => 'password' - ]); - $response = ($this->action)($request, new \Slim\Http\Response(), array()); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), '{"email":"user2@anis.fr","token":"supertoken"}'); - } -} diff --git a/tests/Action/Meta/DatabaseActionTest.php b/tests/Action/Meta/DatabaseActionTest.php deleted file mode 100644 index 3abe5e9..0000000 --- a/tests/Action/Meta/DatabaseActionTest.php +++ /dev/null @@ -1,158 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class DatabaseActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\DatabaseAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory, - base64_decode('r3Q8C7LgIrRcTtI8I6EPzFwrDXJ4adgnGQ9V/pWVI8M=') - ); - } - - protected function getRequestForPut(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'PUT', - 'REQUEST_URI' => '/metadata/database/1', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testDatabaseIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/database/15' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 15, 'instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Database with id 15 is not found' - ))); - } - - public function testGetDatabase(): void - { - $database = array( - 'id' => 1, - 'label' => 'Cosmology', - 'dbname' => 'cosmologydb', - 'dbtype' => 'pgsql', - 'dbhost' => 'pgserver', - 'dbport' => 5432, - 'dblogin' => 'consult', - 'dbpassword' => 'password' - ); - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/database/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($database)); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDbNameIsEmpty(): void - { - $this->fieldIsEmpty('dbname'); - } - - public function testDbTypeIsEmpty(): void - { - $this->fieldIsEmpty('dbtype'); - } - - public function testDbHostIsEmpty(): void - { - $this->fieldIsEmpty('dbhost'); - } - - public function testDbPortIsEmpty(): void - { - $this->fieldIsEmpty('dbport'); - } - - public function testDbLoginIsEmpty(): void - { - $this->fieldIsEmpty('dblogin'); - } - - public function testDbPasswordIsEmpty(): void - { - $this->fieldIsEmpty('dbpassword'); - } - - public function testEditDatabase(): void - { - $editedDatabase = $this->getEditedDatabase(); - $request = $this->getRequestForPut($editedDatabase); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $arrayResponse = json_decode((string) $response->getBody(), true); - $arrayResponse['dbpassword'] = 'password'; - $this->assertSame($arrayResponse, $editedDatabase); - $this->assertSame(2, $this->getDatabaseTester()->getConnection()->getRowCount('database')); - } - - public function testRemoveDatabase(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'DELETE', - 'REQUEST_URI' => '/metadata/database/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - json_encode(array('message' => 'Database with id 1 is removed!')) - ); - $this->assertSame(1, $this->getDatabaseTester()->getConnection()->getRowCount('database')); - } - - private function fieldIsEmpty($field): void - { - $editedDatabase = $this->getEditedDatabase(); - unset($editedDatabase[$field]); - $request = $this->getRequestForPut($editedDatabase); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to edit the database' - ))); - } - - private function getEditedDatabase(): array - { - return array( - 'id' => 1, - 'label' => 'Edited Database', - 'dbname' => 'edited_database', - 'dbtype' => 'pgsql', - 'dbhost' => 'pgserver2', - 'dbport' => 5432, - 'dblogin' => 'login', - 'dbpassword' => 'password' - ); - } -} diff --git a/tests/Action/Meta/DatabaseListActionTest.php b/tests/Action/Meta/DatabaseListActionTest.php deleted file mode 100644 index 13159c9..0000000 --- a/tests/Action/Meta/DatabaseListActionTest.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class DatabaseListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\DatabaseListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory, - base64_decode('r3Q8C7LgIrRcTtI8I6EPzFwrDXJ4adgnGQ9V/pWVI8M=') - ); - } - - protected function getRequestForPost(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/metadata/database', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testDatabaseList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/database' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $json = array( - [ - "id" => 1, - "label" => "Cosmology", - "dbname" => "cosmologydb", - "dbtype" => "pgsql", - "dbhost" => "pgserver", - "dbport" => 5432, - "dblogin" => "consult", - "dbpassword" => "password" - ], - [ - "id" => 2, - "label" => "ExoDat", - "dbname" => "exodat_new", - "dbtype" => "pgsql", - "dbhost" => "pgserver", - "dbport" => 5432, - "dblogin" => "consult", - "dbpassword" => "exopassword" - ] - ); - $this->assertSame((string) $response->getBody(), json_encode($json)); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDbNameIsEmpty(): void - { - $this->fieldIsEmpty('dbname'); - } - - public function testDbTypeIsEmpty(): void - { - $this->fieldIsEmpty('dbtype'); - } - - public function testDbHostIsEmpty(): void - { - $this->fieldIsEmpty('dbhost'); - } - - public function testDbPortIsEmpty(): void - { - $this->fieldIsEmpty('dbport'); - } - - public function testDbLoginIsEmpty(): void - { - $this->fieldIsEmpty('dblogin'); - } - - public function testDbPasswordIsEmpty(): void - { - $this->fieldIsEmpty('dbpassword'); - } - - public function testAddDatabase(): void - { - $newDatabase = $this->getNewDatabase(); - $request = $this->getRequestForPost($newDatabase); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(201, (int) $response->getStatusCode()); - $arrayResponse = json_decode((string) $response->getBody(), true); - $arrayResponse['dbpassword'] = 'password'; - $this->assertSame($arrayResponse, array_merge(array('id' => 3), $newDatabase)); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('database')); - } - - private function fieldIsEmpty($field): void - { - $newDatabase = $this->getNewDatabase(); - unset($newDatabase[$field]); - $request = $this->getRequestForPost($newDatabase); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to add a new database' - ))); - } - - private function getNewDatabase(): array - { - return array( - 'label' => 'New Database', - 'dbname' => 'new_database', - 'dbtype' => 'pgsql', - 'dbhost' => 'pgserver2', - 'dbport' => 5432, - 'dblogin' => 'login', - 'dbpassword' => 'password' - ); - } -} diff --git a/tests/Action/Meta/DatasetActionTest.php b/tests/Action/Meta/DatasetActionTest.php deleted file mode 100644 index 4ba2a5f..0000000 --- a/tests/Action/Meta/DatasetActionTest.php +++ /dev/null @@ -1,180 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class DatasetActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\DatasetAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPut(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'PUT', - 'REQUEST_URI' => '/metadata/dataset/corot_targets', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testDatasetIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/dataset/undifined' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'undifined', 'instance' => 'default') - ); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Dataset with name undifined is not found' - ))); - } - - public function testGetDataset(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/dataset/corot_targets' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'corot_targets', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'name' => 'corot_targets', - 'table_ref' => 'v_corot_targets', - 'label' => 'CoRoT Targets', - 'description' => 'CoRoT Targets', - 'display' => 10, - 'count' => 100582, - 'vo' => true, - 'data_path' => '/tmp/data', - 'selectable_row' => false, - 'project_name' => 'corot', - 'id_dataset_family' => 1 - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDescriptionIsEmpty(): void - { - $this->fieldIsEmpty('description'); - } - - public function testIdDatasetFamilyIsEmpty(): void - { - $this->fieldIsEmpty('id_dataset_family'); - } - - public function testDisplayIsEmpty(): void - { - $this->fieldIsEmpty('display'); - } - - public function testCountIsEmpty(): void - { - $this->fieldIsEmpty('count'); - } - - public function testVoIsEmpty(): void - { - $this->fieldIsEmpty('vo'); - } - - public function testDataPathIsEmpty(): void - { - $this->fieldIsEmpty('data_path'); - } - - public function testEditDataset(): void - { - $editedDataset = $this->getEditedDataset(); - $request = $this->getRequestForPut($editedDataset); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'corot_targets', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($editedDataset)); - $this->assertSame(2, $this->getDatabaseTester()->getConnection()->getRowCount('dataset')); - } - - public function testRemoveDataset(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'DELETE', - 'REQUEST_URI' => '/metadata/dataset/corot_targets' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'corot_targets', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - json_encode(array('message' => 'Dataset with name corot_targets is removed!')) - ); - $this->assertSame(1, $this->getDatabaseTester()->getConnection()->getRowCount('dataset')); - } - - private function fieldIsEmpty($field): void - { - $editedDataset = $this->getEditedDataset(); - unset($editedDataset[$field]); - $request = $this->getRequestForPut($editedDataset); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'corot_targets', 'instance' => 'default') - ); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to edit the dataset' - ))); - } - - private function getEditedDataset(): array - { - return array( - 'name' => 'corot_targets', - 'table_ref' => 'v_corot_targets', - 'label' => 'Edited dataset', - 'description' => 'Edited dataset description', - 'display' => 50, - 'count' => 100583, - 'vo' => true, - 'data_path' => '/tmp/data', - 'selectable_row' => false, - 'project_name' => 'corot', - 'id_dataset_family' => 1 - ); - } -} diff --git a/tests/Action/Meta/DatasetListActionTest.php b/tests/Action/Meta/DatasetListActionTest.php deleted file mode 100644 index dbfa034..0000000 --- a/tests/Action/Meta/DatasetListActionTest.php +++ /dev/null @@ -1,189 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Doctrine\DBAL\Connection as DBALConnection; - -use App\Utils\DBALConnectionFactory; -use App\Tests\AbstractActionMetaTestCase; - -final class DatasetListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\DatasetListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory, - $this->getMockDBALConnectionFactory(), - base64_decode('r3Q8C7LgIrRcTtI8I6EPzFwrDXJ4adgnGQ9V/pWVI8M=') - ); - } - - protected function getMockDBALConnectionFactory(): DBALConnectionFactory - { - $mockDBALConnection = $this->createMock(DBALConnection::class); - $mockConnectionFactory = $this->createMock(DBALConnectionFactory::class); - $mockConnectionFactory - ->method('create') - ->willReturn($mockDBALConnection); - return $mockConnectionFactory; - } - - protected function getRequestForPost(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/metadata/dataset', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testGetDatasetList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/dataset' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - array( - 'name' => 'corot_targets', - 'table_ref' => 'v_corot_targets', - 'label' => 'CoRoT Targets', - 'description' => 'CoRoT Targets', - 'display' => 10, - 'count' => 100582, - 'vo' => true, - 'data_path' => '/tmp/data', - 'selectable_row' => false, - 'project_name' => 'corot', - 'id_dataset_family' => 1 - ), - array( - 'name' => 'vuds_targets', - 'table_ref' => 'v_vuds_targets', - 'label' => 'VUDS Targets', - 'description' => 'VUDS Targets display', - 'display' => 20, - 'count' => 784512, - 'vo' => false, - 'data_path' => '/tmp/data', - 'selectable_row' => false, - 'project_name' => 'vuds', - 'id_dataset_family' => 2 - ) - ))); - } - - public function testNameIsEmpty(): void - { - $this->fieldIsEmpty('name'); - } - - public function testTableRefIsEmpty(): void - { - $this->fieldIsEmpty('table_ref'); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDescriptionIsEmpty(): void - { - $this->fieldIsEmpty('description'); - } - - public function testDisplayIsEmpty(): void - { - $this->fieldIsEmpty('display'); - } - - public function testCountIsEmpty(): void - { - $this->fieldIsEmpty('count'); - } - - public function testVoIsEmpty(): void - { - $this->fieldIsEmpty('vo'); - } - - public function testDataPathIsEmpty(): void - { - $this->fieldIsEmpty('data_path'); - } - - public function testProjectNameIsEmpty(): void - { - $this->fieldIsEmpty('project_name'); - } - - public function testIdDatasetFamilyIsEmpty(): void - { - $this->fieldIsEmpty('id_dataset_family'); - } - - public function testProjectIsNotFound(): void - { - $newDataset = $this->getNewDataset(); - $newDataset['project_name'] = 'undifined'; - $request = $this->getRequestForPost($newDataset); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Project with name undifined is not found' - ))); - } - - public function testDatasetFamilyIsNotFound(): void - { - $newDataset = $this->getNewDataset(); - $newDataset['id_dataset_family'] = 15; - $request = $this->getRequestForPost($newDataset); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Dataset family with id 15 is not found' - ))); - } - - private function fieldIsEmpty($field): void - { - $newDataset = $this->getNewDataset(); - unset($newDataset[$field]); - $request = $this->getRequestForPost($newDataset); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to add a new dataset' - ))); - } - - private function getNewDataset(): array - { - return array( - 'name' => 'new_dataset', - 'table_ref' => 'v_new_dataset', - 'label' => 'New dataset', - 'description' => 'New Dataset description', - 'display' => 30, - 'count' => 987456, - 'vo' => true, - 'data_path' => '/tmp/data', - 'selectable_row' => true, - 'project_name' => 'corot', - 'id_dataset_family' => 1 - ); - } -} diff --git a/tests/Action/Meta/FamilyActionTest.php b/tests/Action/Meta/FamilyActionTest.php deleted file mode 100644 index 581875d..0000000 --- a/tests/Action/Meta/FamilyActionTest.php +++ /dev/null @@ -1,169 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class FamilyActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\FamilyAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPut(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'PUT', - 'REQUEST_URI' => '/metadata/family/dataset/1', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testTypeIsNotDefined(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/family/undifined/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'undifined', 'id' => 1, 'instance' => 'default') - ); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Type undifined is not defined' - ))); - } - - public function testFamilyIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/family/dataset/15' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'id' => 15, 'instance' => 'default') - ); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Dataset family with id 15 is not found' - ))); - } - - public function testGetDatasetFamily(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/family/dataset/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'id' => 1, 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'id' => 1, - 'label' => 'VUDS dataset collection', - 'display' => 10, - 'type' => 'dataset', - 'datasets' => array( - 'corot_targets' - ) - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDisplayIsEmpty(): void - { - $this->fieldIsEmpty('display'); - } - - public function testEditDatasetFamily(): void - { - $editedDatasetFamily = $this->getEditedDatasetFamily(); - $request = $this->getRequestForPut($editedDatasetFamily); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'id' => 1, 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'id' => 1, - 'label' => 'Edited Family', - 'display' => 40, - 'type' => 'dataset', - 'datasets' => array( - 'corot_targets' - ) - ))); - } - - public function testRemoveDatasetFamily(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'DELETE', - 'REQUEST_URI' => '/metadata/family/dataset/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'id' => 1, 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - json_encode(array('message' => 'Dataset family with id 1 is removed!')) - ); - $this->assertSame(1, $this->getDatabaseTester()->getConnection()->getRowCount('dataset_family')); - } - - private function fieldIsEmpty($field): void - { - $editedDatasetFamily = $this->getEditedDatasetFamily(); - unset($editedDatasetFamily[$field]); - $request = $this->getRequestForPut($editedDatasetFamily); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'id' => 1, 'instance' => 'default') - ); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to edit the dataset family' - ))); - } - - private function getEditedDatasetFamily(): array - { - return array( - 'id' => 1, - 'label' => 'Edited Family', - 'display' => 40 - ); - } -} diff --git a/tests/Action/Meta/FamilyListActionTest.php b/tests/Action/Meta/FamilyListActionTest.php deleted file mode 100644 index a1ccdf3..0000000 --- a/tests/Action/Meta/FamilyListActionTest.php +++ /dev/null @@ -1,236 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class FamilyListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\FamilyListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPost(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/metadata/family', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testDatasetFamilyList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/family/dataset' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $json = array( - [ - "id" => 1, - "label" => "VUDS dataset collection", - "display" => 10, - "type" => "dataset", - "datasets" => ["corot_targets"] - ], - [ - "id" => 2, - "label" => "VVDS dataset collection", - "display" => 20, - "type" => "dataset", - "datasets" => ["vuds_targets"] - ] - ); - $this->assertSame((string) $response->getBody(), json_encode($json)); - } - - public function testCriteriaFamilyList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/family/criteria' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'criteria', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $json = array( - [ - "id" => 1, - "label" => "Criteria by default", - "display" => 10, - "type" => "criteria" - ], - [ - "id" => 2, - "label" => "Photometry", - "display" => 20, - "type" => "criteria" - ] - ); - $this->assertSame((string) $response->getBody(), json_encode($json)); - } - - public function testOutputFamilyList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/family/family' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'output', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $json = array( - [ - "id" => 1, - "label" => "Parameters by default", - "display" => 10, - "type" => "output" - ], - [ - "id" => 2, - "label" => "Others catalog ID", - "display" => 20, - "type" => "output" - ] - ); - $this->assertSame((string) $response->getBody(), json_encode($json)); - } - - public function testFamilyTypeIsNotDefined(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/family/undifined' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'undifined', 'instance' => 'default') - ); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Type undifined is not defined' - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDisplayIsEmpty(): void - { - $this->fieldIsEmpty('display'); - } - - public function testAddDatasetFamily(): void - { - $newDatasetFamily = $this->getNewDatasetFamily(); - $request = $this->getRequestForPost($newDatasetFamily); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'instance' => 'default') - ); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode( - array_merge( - array('id' => 3), - $newDatasetFamily, - array('type' => 'dataset', 'datasets' => array()) - ) - )); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('dataset_family')); - } - - public function testAddCriteriaFamily(): void - { - $newCriteriaFamily = array('label' => 'New criteria family', 'display' => 30); - $request = $this->getRequestForPost($newCriteriaFamily); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'criteria', 'instance' => 'default') - ); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode( - array_merge( - array('id' => 3), - $newCriteriaFamily, - array('type' => 'criteria') - ) - )); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('criteria_family')); - } - - public function testAddOutputFamily(): void - { - $newOutputFamily = array('label' => 'New output family', 'display' => 30); - $request = $this->getRequestForPost($newOutputFamily); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'output', 'instance' => 'default') - ); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode( - array_merge( - array('id' => 3), - $newOutputFamily, - array('type' => 'output') - ) - )); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('output_family')); - } - - private function fieldIsEmpty($field): void - { - $newDatasetFamily = $this->getNewDatasetFamily(); - unset($newDatasetFamily[$field]); - $request = $this->getRequestForPost($newDatasetFamily); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('type' => 'dataset', 'instance' => 'default') - ); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to add a new family' - ))); - } - - private function getNewDatasetFamily(): array - { - return array( - 'label' => 'New dataset family', - 'display' => 30 - ); - } -} diff --git a/tests/Action/Meta/FileActionTest.php b/tests/Action/Meta/FileActionTest.php deleted file mode 100644 index 6c866f8..0000000 --- a/tests/Action/Meta/FileActionTest.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class FileActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\FileAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPut(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'PUT', - 'REQUEST_URI' => '/metadata/file/1', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testGetFile(): void - { - $file = array( - 'id' => 1, - 'label' => 'My file', - 'file_loc' => 'my_file.txt', - 'type' => 'txt', - 'display' => 10, - 'visible' => true - ); - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/file/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($file)); - } - - public function testFileIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/file/15' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 15, 'instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'File with id 15 is not found' - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testFileLocIsEmpty(): void - { - $this->fieldIsEmpty('file_loc'); - } - - public function testTypeIsEmpty(): void - { - $this->fieldIsEmpty('type'); - } - - public function testDisplayIsEmpty(): void - { - $this->fieldIsEmpty('display'); - } - - public function testVisibleIsEmpty(): void - { - $this->fieldIsEmpty('visible'); - } - - public function testEditFile(): void - { - $editedFile = $this->getEditedFile(); - $request = $this->getRequestForPut($editedFile); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($editedFile)); - $this->assertSame(2, $this->getDatabaseTester()->getConnection()->getRowCount('file')); - } - - public function testRemoveFile(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'DELETE', - 'REQUEST_URI' => '/metadata/file/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array('message' => 'File with id 1 is removed!'))); - $this->assertSame(1, $this->getDatabaseTester()->getConnection()->getRowCount('file')); - } - - private function fieldIsEmpty($field): void - { - $editedFile = $this->getEditedFile(); - unset($editedFile[$field]); - $request = $this->getRequestForPut($editedFile); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to edit the file' - ))); - } - - private function getEditedFile(): array - { - return array( - 'id' => 1, - 'label' => 'Edited File', - 'file_loc' => 'my_edited_file.txt', - 'type' => 'txt', - 'display' => 20, - 'visible' => true - ); - } -} diff --git a/tests/Action/Meta/FileListActionTest.php b/tests/Action/Meta/FileListActionTest.php deleted file mode 100644 index b98af73..0000000 --- a/tests/Action/Meta/FileListActionTest.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class FileListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\FileListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPost(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/metadata/dataset/corot_targets/file', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testDatasetIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/dataset/undifined/file' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'undifined', 'instance' => 'default') - ); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Dataset with name undifined is not found' - ))); - } - - public function testGetFileList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/dataset/corot_targets/file' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'corot_targets', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - array( - 'id' => 1, - 'label' => 'My file', - 'file_loc' => 'my_file.txt', - 'type' => 'txt', - 'display' => 10, - 'visible' => true - ), - array( - 'id' => 2, - 'label' => 'My fits', - 'file_loc' => 'file.fits', - 'type' => 'fits', - 'display' => 20, - 'visible' => true - ) - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testFileLocIsEmpty(): void - { - $this->fieldIsEmpty('file_loc'); - } - - public function testTypeIsEmpty(): void - { - $this->fieldIsEmpty('type'); - } - - public function testDisplayIsEmpty(): void - { - $this->fieldIsEmpty('display'); - } - - public function testVisibleIsEmpty(): void - { - $this->fieldIsEmpty('visible'); - } - - public function testAddFile(): void - { - $newFile = $this->getNewFile(); - $request = $this->getRequestForPost($newFile); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'corot_targets', 'instance' => 'default') - ); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array_merge(array('id' => 3), $newFile))); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('file')); - } - - private function fieldIsEmpty($field): void - { - $newFile = $this->getNewFile(); - unset($newFile[$field]); - $request = $this->getRequestForPost($newFile); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'corot_targets', 'instance' => 'default') - ); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to add a new file' - ))); - } - - private function getNewFile(): array - { - return array( - 'label' => 'My new file', - 'file_loc' => 'new_file.fits', - 'type' => 'fits', - 'display' => 30, - 'visible' => true - ); - } -} diff --git a/tests/Action/Meta/GroupActionTest.php b/tests/Action/Meta/GroupActionTest.php deleted file mode 100644 index 07302ac..0000000 --- a/tests/Action/Meta/GroupActionTest.php +++ /dev/null @@ -1,113 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class GroupActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\GroupAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPut(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'PUT', - 'REQUEST_URI' => '/metadata/group/1', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testGetGroup(): void - { - $group = array( - 'id' => 1, - 'label' => 'Default' - ); - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/group/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($group)); - } - - public function testGroupIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/group/15' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 15, 'instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Group with id 15 is not found' - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testEditGroup(): void - { - $editedGroup = $this->getEditedGroup(); - $request = $this->getRequestForPut($editedGroup); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($editedGroup)); - $this->assertSame(2, $this->getDatabaseTester()->getConnection()->getRowCount('anis_group')); - } - - public function testRemoveGroup(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'DELETE', - 'REQUEST_URI' => '/metadata/group/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - json_encode(array('message' => 'Group with id 1 is removed!')) - ); - $this->assertSame(1, $this->getDatabaseTester()->getConnection()->getRowCount('anis_group')); - } - - private function fieldIsEmpty($field): void - { - $editedGroup = $this->getEditedGroup(); - unset($editedGroup[$field]); - $request = $this->getRequestForPut($editedGroup); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to edit the group' - ))); - } - - private function getEditedGroup(): array - { - return array( - 'id' => 1, - 'label' => 'Edited Group' - ); - } -} diff --git a/tests/Action/Meta/GroupListActionTest.php b/tests/Action/Meta/GroupListActionTest.php deleted file mode 100644 index 9831650..0000000 --- a/tests/Action/Meta/GroupListActionTest.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class GroupListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\GroupListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPost(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/metadata/group', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testGetGroupList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/group' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), '[{"id":1,"label":"Default"},{"id":2,"label":"Admin"}]'); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testAddGroup(): void - { - $newGroup = $this->getNewGroup(); - $request = $this->getRequestForPost($newGroup); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array_merge(array('id' => 3), $newGroup))); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('anis_group')); - } - - private function fieldIsEmpty($field): void - { - $newGroup = $this->getNewGroup(); - unset($newGroup[$field]); - $request = $this->getRequestForPost($newGroup); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to add a new group' - ))); - } - - private function getNewGroup(): array - { - return array( - 'label' => 'New Group' - ); - } -} diff --git a/tests/Action/Meta/OutputCategoryActionTest.php b/tests/Action/Meta/OutputCategoryActionTest.php deleted file mode 100644 index 7b4829a..0000000 --- a/tests/Action/Meta/OutputCategoryActionTest.php +++ /dev/null @@ -1,116 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class OutputCategoryActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\OutputCategoryAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPut(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'PUT', - 'REQUEST_URI' => '/metadata/category/1', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testCategoryIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/category/15' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 15, 'instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Output category with id 15 is not found' - ))); - } - - public function testGetCategory(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/category/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'id' => 1, - 'label' => 'Default category', - 'display' => 10, - 'id_output_family' => 1 - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testEditCategory(): void - { - $editedCategory = $this->getEditedCategory(); - $request = $this->getRequestForPut($editedCategory); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($editedCategory)); - $this->assertSame(2, $this->getDatabaseTester()->getConnection()->getRowCount('output_category')); - } - - public function testRemoveCategory(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'DELETE', - 'REQUEST_URI' => '/metadata/category/1' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - json_encode(array('message' => 'Output category with id 1 is removed!')) - ); - $this->assertSame(1, $this->getDatabaseTester()->getConnection()->getRowCount('output_category')); - } - - private function fieldIsEmpty($field): void - { - $editedCategory = $this->getEditedCategory(); - unset($editedCategory[$field]); - $request = $this->getRequestForPut($editedCategory); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 1, 'instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to edit the output category' - ))); - } - - private function getEditedCategory(): array - { - return array( - 'id' => 1, - 'label' => 'Edited Category', - 'display' => 20, - 'id_output_family' => 1 - ); - } -} diff --git a/tests/Action/Meta/OutputCategoryListActionTest.php b/tests/Action/Meta/OutputCategoryListActionTest.php deleted file mode 100644 index 414b8fb..0000000 --- a/tests/Action/Meta/OutputCategoryListActionTest.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class OutputCategoryListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\OutputCategoryListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPost(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/metadata/category', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testGetCategoryList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/category' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - array( - 'id' => 1, - 'label' => 'Default category', - 'display' => 10, - 'id_output_family' => 1 - ), - array( - 'id' => 2, - 'label' => 'Photometry', - 'display' => 20, - 'id_output_family' => 1 - ) - ))); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testAddCategory(): void - { - $newCategory = $this->getNewCategory(); - $request = $this->getRequestForPost($newCategory); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array_merge(array('id' => 3), $newCategory))); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('output_category')); - } - - private function fieldIsEmpty($field): void - { - $newCategory = $this->getNewCategory(); - unset($newCategory[$field]); - $request = $this->getRequestForPost($newCategory); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to add a new output category' - ))); - } - - private function getNewCategory(): array - { - return array( - 'label' => 'New Category', - 'display' => 30, - 'id_output_family' => 1 - ); - } -} diff --git a/tests/Action/Meta/ProjectActionTest.php b/tests/Action/Meta/ProjectActionTest.php deleted file mode 100644 index 6f5aefc..0000000 --- a/tests/Action/Meta/ProjectActionTest.php +++ /dev/null @@ -1,159 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class ProjectActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\ProjectAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPut(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'PUT', - 'REQUEST_URI' => '/metadata/project/1', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testGetProject(): void - { - $project = array( - 'name' => 'vuds', - 'label' => 'VUDS', - 'description' => 'VUDS project', - 'link' => 'http://cesam.lam.fr/vuds', - 'manager' => 'Olivier Le Fèvre', - 'id_database' => 1 - ); - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/project/vuds' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'vuds', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($project)); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDescriptionIsEmpty(): void - { - $this->fieldIsEmpty('description'); - } - - public function testLinkIsEmpty(): void - { - $this->fieldIsEmpty('link'); - } - - public function testManagerIsEmpty(): void - { - $this->fieldIsEmpty('manager'); - } - - public function testIdDatabaseIsEmpty(): void - { - $this->fieldIsEmpty('id_database'); - } - - public function testDatabaseIsNotFound(): void - { - $editedProject = $this->getEditedProject(); - $editedProject['id_database'] = 15; - $request = $this->getRequestForPut($editedProject); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'vuds', 'instance' => 'default') - ); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Database with id 15 is not found' - ))); - } - - public function testEditProject(): void - { - $editedProject = $this->getEditedProject(); - $request = $this->getRequestForPut($editedProject); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'vuds', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($editedProject)); - $this->assertSame(2, $this->getDatabaseTester()->getConnection()->getRowCount('project')); - } - - public function testRemoveProject(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'DELETE', - 'REQUEST_URI' => '/metadata/project/vuds' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'vuds', 'instance' => 'default') - ); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame( - (string) $response->getBody(), - json_encode(array('message' => 'Project vuds is removed!')) - ); - $this->assertSame(1, $this->getDatabaseTester()->getConnection()->getRowCount('project')); - } - - private function fieldIsEmpty($field): void - { - $editedProject = $this->getEditedProject(); - unset($editedProject[$field]); - $request = $this->getRequestForPut($editedProject); - $response = ($this->action)( - $request, - new \Slim\Http\Response(), - array('name' => 'vuds', 'instance' => 'default') - ); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to edit the project' - ))); - } - - private function getEditedProject(): array - { - return array( - 'name' => 'vuds', - 'label' => 'Edited Project', - 'description' => 'Description project', - 'link' => 'http://link.fr', - 'manager' => 'My Manager', - 'id_database' => 1 - ); - } -} diff --git a/tests/Action/Meta/ProjectListActionTest.php b/tests/Action/Meta/ProjectListActionTest.php deleted file mode 100644 index 2c46f03..0000000 --- a/tests/Action/Meta/ProjectListActionTest.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; - -use App\Tests\AbstractActionMetaTestCase; - -final class ProjectListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\ProjectListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory - ); - } - - protected function getRequestForPost(array $parsedBody): Request - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/metadata/project', - 'CONTENT_TYPE' => 'application/json', - ]); - return \Slim\Http\Request::createFromEnvironment($environment)->withParsedBody($parsedBody); - } - - public function testProjectList(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/project' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(200, (int) $response->getStatusCode()); - $json = array( - [ - "name" => "vuds", - "label" => "VUDS", - "description" => "VUDS project", - "link" => "http://cesam.lam.fr/vuds", - "manager" => "Olivier Le Fèvre", - "id_database" => 1 - ], - [ - "name" => "corot", - "label" => "CoRoT", - "description" => "CoRoT project", - "link" => "http://corot.cnes.fr", - "manager" => "Magali Deleuil", - "id_database" => 2 - ] - ); - $this->assertSame((string) $response->getBody(), json_encode($json, JSON_UNESCAPED_SLASHES)); - } - - public function testNameIsEmpty(): void - { - $this->fieldIsEmpty('name'); - } - - public function testLabelIsEmpty(): void - { - $this->fieldIsEmpty('label'); - } - - public function testDescriptionIsEmpty(): void - { - $this->fieldIsEmpty('description'); - } - - public function testLinkIsEmpty(): void - { - $this->fieldIsEmpty('link'); - } - - public function testManagerIsEmpty(): void - { - $this->fieldIsEmpty('manager'); - } - - public function testIdDatabaseIsEmpty(): void - { - $this->fieldIsEmpty('id_database'); - } - - public function testDatabaseIsNotFound(): void - { - $newProject = $this->getNewProject(); - $newProject['id_database'] = 15; - $request = $this->getRequestForPost($newProject); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Database with id 15 is not found' - ))); - } - - public function testAddProject(): void - { - $newProject = $this->getNewProject(); - $request = $this->getRequestForPost($newProject); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(201, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode($newProject, JSON_UNESCAPED_SLASHES)); - $this->assertSame(3, $this->getDatabaseTester()->getConnection()->getRowCount('project')); - } - - private function fieldIsEmpty($field): void - { - $newProject = $this->getNewProject(); - unset($newProject[$field]); - $request = $this->getRequestForPost($newProject); - $response = ($this->action)($request, new \Slim\Http\Response(), array('instance' => 'default')); - $this->assertEquals(400, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Param ' . $field . ' needed to add a new project' - ))); - } - - private function getNewProject(): array - { - return array( - 'name' => 'project', - 'label' => 'My Project', - 'description' => 'Project description', - 'link' => 'http://www.project.com', - 'manager' => 'M. Nobody', - 'id_database' => 1 - ); - } -} diff --git a/tests/Action/Meta/TableListActionTest.php b/tests/Action/Meta/TableListActionTest.php deleted file mode 100644 index e09af09..0000000 --- a/tests/Action/Meta/TableListActionTest.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Action\Meta; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Doctrine\DBAL\Connection as DBALConnection; - -use App\Utils\DBALConnectionFactory; -use App\Tests\AbstractActionMetaTestCase; - -final class TableListActionTest extends AbstractActionMetaTestCase -{ - protected function setUp(): void - { - parent::setUp(); - $this->action = new \App\Action\Meta\TableListAction( - new \Psr\Log\NullLogger(), - $this->metaEntityManagerFactory, - $this->getMockDBALConnectionFactory(), - base64_decode('r3Q8C7LgIrRcTtI8I6EPzFwrDXJ4adgnGQ9V/pWVI8M=') - ); - } - - protected function getMockDBALConnectionFactory(): DBALConnectionFactory - { - $mockDBALConnection = $this->createMock(DBALConnection::class); - $mockConnectionFactory = $this->createMock(DBALConnectionFactory::class); - $mockConnectionFactory - ->method('create') - ->willReturn($mockDBALConnection); - return $mockConnectionFactory; - } - - public function testDatabaseIsNotFound(): void - { - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/metadata/database/15/table' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - $response = ($this->action)($request, new \Slim\Http\Response(), array('id' => 15, 'instance' => 'default')); - $this->assertEquals(404, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array( - 'error' => 'Invalid request', - 'error_description' => 'Database with id 15 is not found' - ))); - } -} diff --git a/tests/Action/OutputCategoryActionTest.php b/tests/Action/OutputCategoryActionTest.php new file mode 100644 index 0000000..277962e --- /dev/null +++ b/tests/Action/OutputCategoryActionTest.php @@ -0,0 +1,146 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\OutputCategory; +use App\Entity\OutputFamily; + +final class OutputCategoryActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\OutputCategoryAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testOutputCategoryIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Output category with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAnOutputCategoryById(): void + { + $outputCategory = $this->addAnOutputCategory(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode($outputCategory), (string) $response->getBody()); + } + + public function testEditAnOutputCategoryEmptyLabelField(): void + { + $this->addAnOutputCategory(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the output category'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditAnOutputCategoryOutputFamilyIsNotFound(): void + { + $this->addAnOutputCategory(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Output family with id 2 is not found'); + $fields = array( + 'label' => 'Default output category', + 'display' => 20, + 'id_output_family' => 2 + ); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditAnOutputCategory(): void + { + $this->addAnOutputCategory(); + $fields = array( + 'label' => 'Default output category', + 'display' => 20, + 'id_output_family' => 1 + ); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields)), + (string) $response->getBody() + ); + $this->assertEquals(200, (int) $response->getStatusCode()); + } + + public function testDeleteAnOutputCategory(): void + { + $this->addAnOutputCategory(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame( + json_encode(array('message' => 'Output category with id 1 is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/output-category/1', array( + 'Content-Type' => 'application/json' + )); + } + + private function addOutputFamily(): OutputFamily + { + $family = new OutputFamily(); + $family->setLabel('Default output family'); + $family->setDisplay(10); + $this->entityManager->persist($family); + $this->entityManager->flush(); + return $family; + } + + private function addAnOutputCategory(): OutputCategory + { + $outputFamily = $this->addOutputFamily(); + + $outputCategory = new OutputCategory(); + $outputCategory->setLabel('Default output category'); + $outputCategory->setDisplay(10); + $outputCategory->setOutputFamily($outputFamily); + $this->entityManager->persist($outputCategory); + $this->entityManager->flush(); + return $outputCategory; + } +} diff --git a/tests/Action/OutputCategoryListActionTest.php b/tests/Action/OutputCategoryListActionTest.php new file mode 100644 index 0000000..3c396e9 --- /dev/null +++ b/tests/Action/OutputCategoryListActionTest.php @@ -0,0 +1,133 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\OutputFamily; +use App\Entity\OutputCategory; + +final class OutputCategoryListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\OutputCategoryListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testGetAllOutputCategories(): void + { + $outputCategories = $this->addOutputCategories(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($outputCategories), + (string) $response->getBody() + ); + } + + public function testAddANewOutputCategoryEmptyLabelField(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to add a new output category'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewOutputCategoryOutputFamilyIsNotFound(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Output family with id 1 is not found'); + $fields = array( + 'label' => 'Default output category', + 'display' => 10, + 'id_output_family' => 1 + ); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewOutputCategory(): void + { + $this->addOutputFamily(); + $fields = array( + 'label' => 'Default output category', + 'display' => 10, + 'id_output_family' => 1 + ); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields)), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/output-category', array( + 'Content-Type' => 'application/json' + )); + } + + private function addOutputFamily(): OutputFamily + { + $family = new OutputFamily(); + $family->setLabel('Default output family'); + $family->setDisplay(10); + $this->entityManager->persist($family); + $this->entityManager->flush(); + return $family; + } + + private function addOutputCategories(): array + { + $outputFamily = $this->addOutputFamily(); + + $outputCategory1 = new OutputCategory(); + $outputCategory1->setLabel('Default output category'); + $outputCategory1->setDisplay(10); + $outputCategory1->setOutputFamily($outputFamily); + $this->entityManager->persist($outputCategory1); + + $outputCategory2 = new OutputCategory(); + $outputCategory2->setLabel('My output category'); + $outputCategory2->setDisplay(20); + $outputCategory2->setOutputFamily($outputFamily); + $this->entityManager->persist($outputCategory2); + + $this->entityManager->flush(); + return array($outputCategory1, $outputCategory2); + } +} diff --git a/tests/Action/ProjectActionTest.php b/tests/Action/ProjectActionTest.php new file mode 100644 index 0000000..5160a6c --- /dev/null +++ b/tests/Action/ProjectActionTest.php @@ -0,0 +1,156 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; + +final class ProjectActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\ProjectAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testProjectIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Project with name anis_project is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'anis_project')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAProjectByName(): void + { + $project = $this->addAProject(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'anis_project')); + $this->assertSame(json_encode($project, JSON_UNESCAPED_SLASHES), (string) $response->getBody()); + } + + public function testEditAProjectEmptyLabelField(): void + { + $this->addAProject(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the project'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('name' => 'anis_project')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditAProjectDatabaseNotFound(): void + { + $this->addAProject(); + $fields = $this->getEditProjectFields(); + $fields['id_database'] = 2; + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Database with id 2 is not found'); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'anis_project')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditAProject(): void + { + $this->addAProject(); + $fields = $this->getEditProjectFields(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'anis_project')); + $this->assertSame( + json_encode(array_merge(['name' => 'anis_project'], $fields), JSON_UNESCAPED_SLASHES), + (string) $response->getBody() + ); + } + + public function testDeleteAProject(): void + { + $this->addAProject(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('name' => 'anis_project')); + $this->assertSame( + json_encode(array('message' => 'Project anis_project is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/project/anis_project', array( + 'Content-Type' => 'application/json' + )); + } + + private function getEditProjectFields(): array + { + return array( + 'label' => 'New label project', + 'description' => 'Anis Project Test', + 'link' => 'http://project.com', + 'manager' => 'M. Durand', + 'id_database' => 1 + ); + } + + private function addDatabase(): Database + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + $this->entityManager->flush(); + return $database; + } + + private function addAProject(): Project + { + $database = $this->addDatabase(); + + $project = new Project('anis_project'); + $project->setLabel('Anis Project Test'); + $project->setDescription('Anis Project Test'); + $project->setLink('http://project.com'); + $project->setManager('M. Durand'); + $project->setDatabase($database); + $this->entityManager->persist($project); + $this->entityManager->flush(); + return $project; + } +} diff --git a/tests/Action/ProjectListActionTest.php b/tests/Action/ProjectListActionTest.php new file mode 100644 index 0000000..ef2a927 --- /dev/null +++ b/tests/Action/ProjectListActionTest.php @@ -0,0 +1,146 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; + +final class ProjectListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\ProjectListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testGetAllProjects(): void + { + $projects = $this->addProjects(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($projects, JSON_UNESCAPED_SLASHES), + (string) $response->getBody() + ); + } + + public function testAddANewProjectEmptyNameField(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param name needed to add a new project'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewProjectDatabaseNotFound(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Database with id 1 is not found'); + $fields = $this->getNewProjectFields(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewProject(): void + { + $this->addDatabase(); + $fields = $this->getNewProjectFields(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($fields, JSON_UNESCAPED_SLASHES), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/project', array( + 'Content-Type' => 'application/json' + )); + } + + private function getNewProjectFields(): array + { + return array( + 'name' => 'anis_project', + 'label' => 'Anis Project Test', + 'description' => 'Anis Project Test', + 'link' => 'http://project.com', + 'manager' => 'M. Durand', + 'id_database' => 1 + ); + } + + private function addDatabase(): Database + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + $this->entityManager->flush(); + return $database; + } + + private function addProjects(): array + { + $database = $this->addDatabase(); + + $project = new Project('test'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $project2 = new Project('test2'); + $project2->setLabel('Test2 project'); + $project2->setDescription('Test2 description'); + $project2->setLink('http://test2.com'); + $project2->setManager('User2'); + $project2->setDatabase($database); + $this->entityManager->persist($project2); + + $this->entityManager->flush(); + return array($project, $project2); + } +} diff --git a/tests/Action/Root/RootActionTest.php b/tests/Action/Root/RootActionTest.php deleted file mode 100644 index 84dde23..0000000 --- a/tests/Action/Root/RootActionTest.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -declare(strict_types=1); - -namespace App\Tests\Root; - -use PHPUnit\Framework\TestCase; - -final class RootActionTest extends TestCase -{ - public function testRootGet(): void - { - $action = new \App\Action\Root\RootAction(new \Psr\Log\NullLogger()); - - $environment = \Slim\Http\Environment::mock([ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/' - ]); - $request = \Slim\Http\Request::createFromEnvironment($environment); - - $response = $action($request, new \Slim\Http\Response(), array()); - $this->assertEquals(200, (int) $response->getStatusCode()); - $this->assertSame((string) $response->getBody(), json_encode(array('message' => 'it works!'))); - } -} diff --git a/tests/Action/RootActionTest.php b/tests/Action/RootActionTest.php new file mode 100644 index 0000000..46bd6f4 --- /dev/null +++ b/tests/Action/RootActionTest.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; + +final class RootActionTest extends TestCase +{ + private $action; + + protected function setUp(): void + { + $this->action = new \App\Action\RootAction(); + } + + public function testAction(): void + { + $request = new ServerRequest('GET', '/', array( + 'Content-Type' => 'application/json' + )); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame('{"message":"it works!"}', (string) $response->getBody()); + } +} diff --git a/tests/Action/TableListActionTest.php b/tests/Action/TableListActionTest.php new file mode 100644 index 0000000..767de4e --- /dev/null +++ b/tests/Action/TableListActionTest.php @@ -0,0 +1,120 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\SqliteSchemaManager; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\View; +use App\Utils\DBALConnectionFactory; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; + +final class TableListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\TableListAction($this->entityManager, $this->getConnectionFactory()); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, OPTIONS'); + } + + public function testDatabaseIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Database with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetTablesAndViewsList(): void + { + $this->addADatabase(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode(array('table1', 'table2', 'view1', 'view2')), (string) $response->getBody()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/database/1/table', array( + 'Content-Type' => 'application/json' + )); + } + + private function getConnectionFactory(): DBALConnectionFactory + { + $schemaManager = $this->getMockBuilder(SqliteSchemaManager::class) + ->disableOriginalConstructor() + ->setMethods(['listTables', 'listViews']) + ->getMock(); + + $schemaManager->method('listTables') + ->will($this->returnValue(array(new Table('table1'), new Table('table2')))); + + $schemaManager->method('listViews') + ->will($this->returnValue(array(new View('view1', ''), new View('view2', '')))); + + $connection = $this->getMockBuilder(Connection::class) + ->disableOriginalConstructor() + ->setMethods(['getSchemaManager']) + ->getMock(); + + $connection->method('getSchemaManager') + ->will($this->returnValue($schemaManager)); + + $connectionFactory = $this->getMockBuilder(DBALConnectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $connectionFactory->method('create') + ->will($this->returnValue($connection)); + + return $connectionFactory; + } + + private function addADatabase(): Database + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + $this->entityManager->flush(); + return $database; + } +} diff --git a/tests/EntityManagerBuilder.php b/tests/EntityManagerBuilder.php new file mode 100644 index 0000000..ecdb09b --- /dev/null +++ b/tests/EntityManagerBuilder.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests; + +use Doctrine\ORM\Tools\Setup; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Tools\SchemaTool; +use App\Entity\User; + +abstract class EntityManagerBuilder +{ + public static function getInstance() + { + $isDevMode = true; + $config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/../src/Entity"), $isDevMode); + $conn = array( + 'driver' => 'pdo_sqlite', + 'memory' => true + ); + $entityManager = EntityManager::create($conn, $config); + $schemaTool = new SchemaTool($entityManager); + $schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata()); + return $entityManager; + } +} diff --git a/tests/Handlers/LogErrorHandlerTest.php b/tests/Handlers/LogErrorHandlerTest.php new file mode 100644 index 0000000..110287b --- /dev/null +++ b/tests/Handlers/LogErrorHandlerTest.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Handlers; + +use PHPUnit\Framework\TestCase; +use Slim\Interfaces\CallableResolverInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Log\LoggerInterface; +use ReflectionClass; + +final class LogErrorHandlerTest extends TestCase +{ + private $logErrorHandler; + + protected function setUp(): void + { + $callableResolver = $this->createMock(CallableResolverInterface::class); + $responseFactory = $this->createMock(ResponseFactoryInterface::class); + $this->logErrorHandler = new \App\Handlers\LogErrorHandler($callableResolver, $responseFactory); + } + + protected static function getMethod($name) + { + $class = new ReflectionClass('App\Handlers\LogErrorHandler'); + $method = $class->getMethod($name); + $method->setAccessible(true); + return $method; + } + + public function testLogError(): void + { + $logError = self::getMethod('logError'); + $logger = $this->prophesize(LoggerInterface::class); + $logger->info('Log test')->shouldBeCalled(); + $this->logErrorHandler->setLogger($logger->reveal()); + $logError->invokeArgs($this->logErrorHandler, array('Log test')); + } +} diff --git a/tests/Middleware/ContentTypeJsonMiddlewareTest.php b/tests/Middleware/ContentTypeJsonMiddlewareTest.php new file mode 100644 index 0000000..82d3e9f --- /dev/null +++ b/tests/Middleware/ContentTypeJsonMiddlewareTest.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Middleware; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; + +final class ContentTypeJsonMiddlewareTest extends TestCase +{ + public function testContentTypeJson() + { + $request = new ServerRequest('GET', '/', array( + 'Content-Type' => 'application/json' + )); + + $requestHandler = $this->getMockBuilder(RequestHandler::class) + ->disableOriginalConstructor() + ->setMethods(['handle']) + ->getMock(); + + $requestHandler->method('handle') + ->with($this->identicalTo($request)) + ->will($this->returnValue(new Response())); + + $contentTypeJsonMiddleware = new \App\Middleware\ContentTypeJsonMiddleware(); + $response = $contentTypeJsonMiddleware->process($request, $requestHandler); + $this->assertSame((string) $response->getHeaderLine('Content-Type'), 'application/json'); + } +} diff --git a/tests/admin.yaml b/tests/admin.yaml deleted file mode 100644 index 9af83a7..0000000 --- a/tests/admin.yaml +++ /dev/null @@ -1,15 +0,0 @@ -anis_user: - - - email: 'user1@anis.fr' - password: 'sfpsdfpsdmsdlsdfdf' - adminsi: false - superuser: false - activation_key: 'noioioi' - activated: false - - - email: 'user2@anis.fr' - password: '$2y$10$TpRjQWm9KtwP7SDBqJbezemYXe02adGIturk.dVjKo86.I/IL.rNG' - adminsi: false - superuser: false - activation_key: 'fpsdf' - activated: true \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 8260685..1e9cf21 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,15 @@ <?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + require __DIR__ . '/../vendor/autoload.php'; -require_once('AbstractActionAdminTestCase.php'); -require_once('AbstractActionMetaTestCase.php'); +require_once(__DIR__ . '/../app/constants.php'); +require_once('EntityManagerBuilder.php'); diff --git a/tests/database.yaml b/tests/database.yaml deleted file mode 100644 index 0928fa1..0000000 --- a/tests/database.yaml +++ /dev/null @@ -1,121 +0,0 @@ -anis_group: - - - id: 1 - label: 'Default' - - - id: 2 - label: 'Admin' -database: - - - id: 1 - label: 'Cosmology' - dbname: 'cosmologydb' - type: 'pgsql' - host: 'pgserver' - port: 5432 - login: 'consult' - password: 'password' - - - id: 2 - label: 'ExoDat' - dbname: 'exodat_new' - type: 'pgsql' - host: 'pgserver' - port: 5432 - login: 'consult' - password: 'exopassword' -project: - - - name: 'vuds' - label: 'VUDS' - description: 'VUDS project' - link: 'http://cesam.lam.fr/vuds' - manager: 'Olivier Le Fèvre' - database_id: 1 - - - name: 'corot' - label: 'CoRoT' - description: 'CoRoT project' - link: 'http://corot.cnes.fr' - manager: 'Magali Deleuil' - database_id: 2 -output_family: - - - id: 1 - label: 'Parameters by default' - display: 10 - - - id: 2 - label: 'Others catalog ID' - display: 20 -criteria_family: - - - id: 1 - label: 'Criteria by default' - display: 10 - - - id: 2 - label: 'Photometry' - display: 20 -dataset_family: - - - id: 1 - label: 'VUDS dataset collection' - display: 10 - - - id: 2 - label: 'VVDS dataset collection' - display: 20 -output_category: - - - id: 1 - label: 'Default category' - display: 10 - output_family: 1 - - - id: 2 - label: 'Photometry' - display: 20 - output_family: 1 -dataset: - - - name: 'corot_targets' - table_ref: 'v_corot_targets' - label: 'CoRoT Targets' - description: 'CoRoT Targets' - display: 10 - count: 100582 - vo: true - data_path: '/tmp/data' - selectable_row: false - project_name: 'corot' - id_dataset_family: 1 - - - name: 'vuds_targets' - table_ref: 'v_vuds_targets' - label: 'VUDS Targets' - description: 'VUDS Targets display' - display: 20 - count: 784512 - vo: false - data_path: '/tmp/data' - selectable_row: false - project_name: 'vuds' - id_dataset_family: 2 -file: - - - id: 1 - label: 'My file' - file_loc: 'my_file.txt' - type: 'txt' - display: 10 - visible: true - dataset_name: 'corot_targets' - - - id: 2 - label: 'My fits' - file_loc: 'file.fits' - type: 'fits' - display: 20 - visible: true - dataset_name: 'corot_targets' -- GitLab From 82c73049a8babb01ab053c26cfd3f4a183aaec7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Mon, 9 Dec 2019 21:58:24 +0100 Subject: [PATCH 02/31] Add the open api documentation (not finished) --- anis-server.yaml | 778 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 778 insertions(+) create mode 100644 anis-server.yaml diff --git a/anis-server.yaml b/anis-server.yaml new file mode 100644 index 0000000..4fc1895 --- /dev/null +++ b/anis-server.yaml @@ -0,0 +1,778 @@ +openapi: "3.0.0" +info: + version: 3.1.0 + title: Anis Server + description: 'AstroNomical Information System is a generic web tool that aims to facilitate the provision of data (Astrophysics), accessible from a database, to a community of scientists.' + contact: + email: francois.agneray@lam.fr + license: + name: CeCILL 2.1 + url: http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.html +servers: + - url: http://localhost:8082 +tags: + - name: root + description: Root path + - name: metadata + description: Set of actions about metamodel database management + - name: search + description: Access to the data +paths: + /: + get: + tags: + - root + summary: Root path + description: Ensures that the service responds + operationId: root + responses: + 200: + description: The service responds + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + /database: + get: + tags: + - metadata + summary: List all available databases + description: Anis can connect to databases (PostgreSQL, MySQL, SQLite, Oracle ...). This action lists the databases already registered and where anis can connect. + operationId: getDatabaseList + responses: + 200: + description: Databases list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Database' + post: + tags: + - metadata + summary: Add a new database + description: Anis can connect to databases (PostgreSQL, MySQL, SQLite, Oracle ...). This action add a new data source. + operationId: addDatabase + requestBody: + description: Database form object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseForm' + required: true + responses: + 201: + description: Database added + content: + application/json: + schema: + $ref: '#/components/schemas/Database' + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /database/{id}: + get: + tags: + - metadata + summary: Find a database by ID + description: Returns a single database registered + operationId: getDatabase + parameters: + - name: id + in: path + description: ID of database to return + required: true + schema: + type: integer + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Database' + 404: + description: Database not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + tags: + - metadata + summary: Updates a database with form data + operationId: editDatabase + parameters: + - name: id + in: path + description: ID of database to return + required: true + schema: + type: integer + requestBody: + description: Database form object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseForm' + required: true + responses: + 200: + description: Database edited + content: + application/json: + schema: + $ref: '#/components/schemas/Database' + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 404: + description: Database not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - metadata + summary: Delete a registered database + operationId: deleteDatabase + parameters: + - name: id + in: path + description: ID of database to return + required: true + schema: + type: integer + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + 404: + description: Database not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /database/{id}/table: + get: + tags: + - metadata + summary: Get tables and views list + description: Get tables and views listed in the database identified by the ID parameter + operationId: tableListDatabase + parameters: + - name: id + in: path + description: ID of database to return + required: true + schema: + type: integer + responses: + 200: + description: Tables and views list + content: + application/json: + schema: + type: array + items: + type: string + 404: + description: Database not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /project: + get: + tags: + - metadata + summary: List all available projects + description: All searchable datasets are listed in one or more projects and a project is attached to a database + operationId: getProjectList + responses: + 200: + description: Project list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Project' + post: + tags: + - metadata + summary: Add a new project + description: All searchable datasets are listed in one or more projects and a project is attached to a database. This action add a new data project. + operationId: addProject + requestBody: + description: Project form object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + required: true + responses: + 201: + description: Project added + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /project/{name}: + get: + tags: + - metadata + summary: Find a project by name + description: Returns a single project registered + operationId: getProject + parameters: + - name: name + in: path + description: Name of project to return + required: true + schema: + type: string + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + 404: + description: Project not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + tags: + - metadata + summary: Updates a project with form data + operationId: editProject + parameters: + - name: name + in: path + description: Name of project to return + required: true + schema: + type: string + requestBody: + description: Project form object that needs to be edited to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + required: true + responses: + 200: + description: Project edited + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 404: + description: Project not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - metadata + summary: Delete a registered project + operationId: deleteProject + parameters: + - name: name + in: path + description: Name of project to return + required: true + schema: + type: string + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + 404: + description: Project not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /dataset: + get: + tags: + - metadata + summary: List all available datasets + description: Get all searchable datasets + operationId: getDatasetList + responses: + 200: + description: Dataset list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Dataset' + post: + tags: + - metadata + summary: Add a new dataset + description: Add a new dataset. This action add automatically the associated attributes (columns) + operationId: addDataset + requestBody: + description: Dataset form object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + required: true + responses: + 201: + description: Dataset added + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /dataset/{name}: + get: + tags: + - metadata + summary: Find a dataset by name + description: Returns a single dataset registered + operationId: getDataset + parameters: + - name: name + in: path + description: Name of dataset to return + required: true + schema: + type: string + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + 404: + description: Dataset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + tags: + - metadata + summary: Updates a dataset with form data + operationId: editDataset + parameters: + - name: name + in: path + description: Name of dataset to return + required: true + schema: + type: string + requestBody: + description: Dataset form object that needs to be edited to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + required: true + responses: + 200: + description: Dataset edited + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 404: + description: Dataset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - metadata + summary: Delete a registered dataset + operationId: deleteDataset + parameters: + - name: name + in: path + description: Name of dataset to return + required: true + schema: + type: string + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + 404: + description: Dataset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /dataset/{name}/attribute: + get: + tags: + - metadata + summary: Retrieve all attributes for a dataset + operationId: getAttributes + parameters: + - name: name + in: path + description: Name of dataset to return + required: true + schema: + type: string + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Attribute' + 404: + description: Dataset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /dataset/{name}/attribute/{id}: + get: + tags: + - metadata + summary: Find an attribute by dataset name and attribute id + description: Returns a single attribute registered + operationId: getAttribute + parameters: + - name: name + in: path + description: Name of dataset to return + required: true + schema: + type: string + - name: id + in: path + description: Id of attribute to return + required: true + schema: + type: integer + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + 404: + description: Dataset or attribute not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + tags: + - metadata + summary: Updates an attribute with form data + operationId: editAttribute + parameters: + - name: name + in: path + description: Name of dataset to return + required: true + schema: + type: string + - name: id + in: path + description: Id of attribute to return + required: true + schema: + type: integer + requestBody: + description: Attribute form object that needs to be edited to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + required: true + responses: + 200: + description: Attribute edited + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 404: + description: Dataset or attribute not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /search/{dname}: + get: + tags: + - search + summary: Retrieve all data of a search + parameters: + - name: dname + in: path + description: Name of dataset about the search + required: true + schema: + type: string + - name: c + in: query + description: Search criteria list separated by a semicolon + required: true + schema: + type: string + - name: a + in: query + description: Search id attributes output list separated by a semicolon + required: true + schema: + type: string + - name: o + in: query + description: Display order list of the search separated by a semicolon. This parameter is mandatory for pagination (p) + required: false + schema: + type: string + - name: p + in: query + description: Pagination settings separated by a colon + required: false + schema: + type: string + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: array + items: + type: object + 400: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 404: + description: Dataset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + DatabaseForm: + type: object + properties: + label: + type: string + dbname: + type: string + dbtype: + type: string + dbhost: + type: string + dbport: + type: integer + dblogin: + type: string + dbpassword: + type: string + Database: + allOf: + - type: object + properties: + id: + type: string + - $ref: '#/components/schemas/DatabaseForm' + Project: + type: object + properties: + name: + type: string + label: + type: string + description: + type: string + link: + type: string + manager: + type: string + id_database: + type: integer + Dataset: + type: object + properties: + name: + type: string + table_ref: + type: string + label: + type: string + description: + type: string + display: + type: integer + count: + type: integer + vo: + type: boolean + data_path: + type: string + project_name: + type: string + id_dataset_family: + type: integer + Attribute: + type: object + properties: + id: + type: integer + name: + type: string + table_name: + type: string + label: + type: string + form_label: + type: string + description: + type: string + output_display: + type: integer + criteria_display: + type: integer + search_flag: + type: string + search_type: + type: string + type: + type: string + operator: + type: string + min: + type: number + max: + type: number + placeholder_min: + type: string + placeholder_max: + type: string + uri_action: + type: string + renderer: + type: string + display_detail: + type: string + selected: + type: boolean + order_by: + type: boolean + order_display: + type: integer + detail: + type: boolean + renderer_detail: + type: string + vo_utype: + type: string + vo_ucd: + type: string + vo_unit: + type: string + vo_description: + type: string + vo_datatype: + type: string + vo_size: + type: integer + id_criteria_family: + type: integer + id_output_family: + type: integer + id_category: + type: integer + Message: + type: object + properties: + message: + type: string + Error: + type: object + properties: + message: + type: string + exception: + type: object + properties: + type: + type: string + code: + type: integer + message: + type: string + file: + type: string + line: + type: string \ No newline at end of file -- GitLab From 4d2bae166c293b455e9cebbd955590ad1af56fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Tue, 10 Dec 2019 15:13:29 +0100 Subject: [PATCH 03/31] Adding Instance into metamodel --- Makefile | 2 +- app/routes.php | 2 + src/Action/DatasetListAction.php | 22 ++++- src/Action/InstanceAction.php | 95 +++++++++++++++++++ src/Action/InstanceListAction.php | 81 +++++++++++++++++ src/Entity/Dataset.php | 19 ++++ src/Entity/Instance.php | 82 +++++++++++++++++ tests/Action/AttributeActionTest.php | 12 +++ tests/Action/AttributeListActionTest.php | 12 +++ tests/Action/DatasetActionTest.php | 16 +++- tests/Action/DatasetListActionTest.php | 27 ++++++ tests/Action/InstanceActionTest.php | 111 +++++++++++++++++++++++ tests/Action/InstanceListActionTest.php | 101 +++++++++++++++++++++ 13 files changed, 576 insertions(+), 6 deletions(-) create mode 100644 src/Action/InstanceAction.php create mode 100644 src/Action/InstanceListAction.php create mode 100644 src/Entity/Instance.php create mode 100644 tests/Action/InstanceActionTest.php create mode 100644 tests/Action/InstanceListActionTest.php diff --git a/Makefile b/Makefile index c5f2879..bf6867b 100644 --- a/Makefile +++ b/Makefile @@ -65,4 +65,4 @@ dev-meta: @docker-compose exec php sh ./conf-dev/dev-meta.sh remove-pgdata: - @docker volume rm anis-metamodel_pgdata + @docker volume rm anis-server_pgdata diff --git a/app/routes.php b/app/routes.php index 42c5ec3..b21d644 100644 --- a/app/routes.php +++ b/app/routes.php @@ -22,6 +22,8 @@ $app->map([OPTIONS, GET, POST], '/family/{type}', App\Action\FamilyListAction::c $app->map([OPTIONS, GET, PUT, DELETE], '/family/{type}/{id}', App\Action\FamilyAction::class); $app->map([OPTIONS, GET, POST], '/output-category', App\Action\OutputCategoryListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class); +$app->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/instance/{name}', App\Action\InstanceAction::class); $app->map([OPTIONS, GET, POST], '/dataset', App\Action\DatasetListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class); $app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class); diff --git a/src/Action/DatasetListAction.php b/src/Action/DatasetListAction.php index fe022a2..dfee85e 100644 --- a/src/Action/DatasetListAction.php +++ b/src/Action/DatasetListAction.php @@ -19,6 +19,7 @@ use Doctrine\ORM\EntityManagerInterface; use App\Utils\DBALConnectionFactory; use App\Entity\Database; use App\Entity\Project; +use App\Entity\Instance; use App\Entity\DatasetFamily; use App\Entity\Dataset; use App\Entity\Attribute; @@ -75,6 +76,7 @@ final class DatasetListAction extends AbstractAction 'data_path', 'selectable_row', 'project_name', + 'instance_name', 'id_dataset_family' ); foreach ($fields as $a) { @@ -95,6 +97,15 @@ final class DatasetListAction extends AbstractAction ); } + // Instance is mandatory to add a new dataset + $instance = $this->em->find('App\Entity\Instance', $parsedBody['instance_name']); + if (is_null($instance)) { + throw new HttpBadRequestException( + $request, + 'Instance with name ' . $parsedBody['instance_name'] . ' is not found' + ); + } + $family = $this->em->find('App\Entity\DatasetFamily', $parsedBody['id_dataset_family']); if (is_null($family)) { throw new HttpBadRequestException( @@ -103,7 +114,7 @@ final class DatasetListAction extends AbstractAction ); } - $dataset = $this->postDataset($parsedBody, $project, $family); + $dataset = $this->postDataset($parsedBody, $project, $instance, $family); $payload = json_encode($dataset); $response = $response->withStatus(201); } @@ -121,8 +132,12 @@ final class DatasetListAction extends AbstractAction * * @return Dataset */ - private function postDataset(array $parsedBody, Project $project, DatasetFamily $family): Dataset - { + private function postDataset( + array $parsedBody, + Project $project, + Instance $instance, + DatasetFamily $family + ): Dataset { $dataset = new Dataset($parsedBody['name']); $dataset->setTableRef($parsedBody['table_ref']); $dataset->setLabel($parsedBody['label']); @@ -133,6 +148,7 @@ final class DatasetListAction extends AbstractAction $dataset->setDataPath($parsedBody['data_path']); $dataset->setSelectableRow($parsedBody['selectable_row']); $dataset->setProject($project); + $dataset->setInstance($instance); $dataset->setDatasetFamily($family); $this->em->persist($dataset); diff --git a/src/Action/InstanceAction.php b/src/Action/InstanceAction.php new file mode 100644 index 0000000..9299926 --- /dev/null +++ b/src/Action/InstanceAction.php @@ -0,0 +1,95 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Instance; + +final class InstanceAction extends AbstractAction +{ + /** + * `GET` Returns the instance found + * `PUT` Full update the instance and returns the new version + * `DELETE` Delete the instance found and return a confirmation message + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct instance with primary key + $instance = $this->em->find('App\Entity\Instance', $args['name']); + + // If instance is not found 404 + if (is_null($instance)) { + throw new HttpNotFoundException( + $request, + 'Instance with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($instance); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + // If mandatories empty fields 400 + foreach (array('label', 'client_url') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the instance' + ); + } + } + + $this->editInstance($instance, $parsedBody); + $payload = json_encode($instance); + } + + if ($request->getMethod() === DELETE) { + $name = $instance->getName(); + $this->em->remove($instance); + $this->em->flush(); + $payload = json_encode(array('message' => 'Instance with name ' . $name . ' is removed!')); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Update instance object with setters + * + * @param Instance $instance The instance to update + * @param array $parsedBody Contains the new values ​​of the instance sent by the user + */ + private function editInstance(Instance $instance, array $parsedBody): void + { + $instance->setLabel($parsedBody['label']); + $instance->setClientUrl($parsedBody['client_url']); + $this->em->flush(); + } +} diff --git a/src/Action/InstanceListAction.php b/src/Action/InstanceListAction.php new file mode 100644 index 0000000..7cbfeac --- /dev/null +++ b/src/Action/InstanceListAction.php @@ -0,0 +1,81 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use App\Entity\Instance; + +final class InstanceListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all instances listed in the metamodel + * `POST` Add a new instance + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + if ($request->getMethod() === GET) { + // Retrieve user with email adress + $instances = $this->em->getRepository('App\Entity\Instance')->findAll(); + $payload = json_encode($instances); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs user information to update + foreach (array('name', 'label', 'client_url') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new instance' + ); + } + } + + $instance = $this->postInstance($parsedBody); + $payload = json_encode($instance); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Add a new instance into the metamodel + * + * @param array $parsedBody Contains the values ​​of the new instance sent by the user + */ + private function postInstance(array $parsedBody): Instance + { + $instance = new Instance($parsedBody['name'], $parsedBody['label']); + $instance->setClientUrl($parsedBody['client_url']); + + $this->em->persist($instance); + $this->em->flush(); + + return $instance; + } +} diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index b2205f8..340fc66 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -92,6 +92,14 @@ class Dataset implements \JsonSerializable */ protected $project; + /** + * @var Anis\Entity\Instance + * + * @ManyToOne(targetEntity="Instance") + * @JoinColumn(name="instance_name", referencedColumnName="name", nullable=false) + */ + protected $instance; + /** * @var Anis\Entity\Project * @@ -222,6 +230,16 @@ class Dataset implements \JsonSerializable $this->project = $project; } + public function getInstance(): Instance + { + return $this->instance; + } + + public function setInstance(Instance $instance): void + { + $this->instance = $instance; + } + public function getDatasetFamily() { return $this->datasetFamily; @@ -265,6 +283,7 @@ class Dataset implements \JsonSerializable 'data_path' => $this->getDataPath(), 'selectable_row' => $this->getSelectableRow(), 'project_name' => $this->getProject()->getName(), + 'instance_name' => $this->getInstance()->getName(), 'id_dataset_family' => $this->getDatasetFamily()->getId() ]; } diff --git a/src/Entity/Instance.php b/src/Entity/Instance.php new file mode 100644 index 0000000..24b6db1 --- /dev/null +++ b/src/Entity/Instance.php @@ -0,0 +1,82 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Entity; + +/** + * @Entity + * @Table(name="instance") + */ +class Instance implements \JsonSerializable +{ + /** + * @var integer + * + * @Id + * @Column(type="string", nullable=false) + */ + protected $name; + + /** + * @var string + * + * @Column(type="string", nullable=false) + */ + protected $label; + + /** + * @var string + * + * @Column(type="string", nullable=true) + */ + protected $clientUrl; + + public function __construct(string $name, string $label) + { + $this->name = $name; + $this->label = $label; + } + + public function getName(): string + { + return $this->name; + } + + public function getLabel(): string + { + return $this->label; + } + + public function setLabel(string $label): void + { + $this->label = $label; + } + + public function getClientUrl(): string + { + return $this->clientUrl; + } + + public function setClientUrl($clientUrl): void + { + $this->clientUrl = $clientUrl; + } + + public function jsonSerialize() + { + return [ + 'name' => $this->getName(), + 'label' => $this->getLabel(), + 'client_url' => $this->getClientUrl() + ]; + } +} diff --git a/tests/Action/AttributeActionTest.php b/tests/Action/AttributeActionTest.php index 4402516..97d423d 100644 --- a/tests/Action/AttributeActionTest.php +++ b/tests/Action/AttributeActionTest.php @@ -20,6 +20,7 @@ use Slim\Exception\HttpBadRequestException; use App\tests\EntityManagerBuilder; use App\Entity\Database; use App\Entity\Project; +use App\Entity\Instance; use App\Entity\DatasetFamily; use App\Entity\CriteriaFamily; use App\Entity\OutputFamily; @@ -161,6 +162,15 @@ final class AttributeActionTest extends TestCase return $project; } + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + private function addDatasetFamily(): DatasetFamily { $family = new DatasetFamily(); @@ -208,6 +218,7 @@ final class AttributeActionTest extends TestCase private function addADataset(): Dataset { $project = $this->addProject(); + $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset = new Dataset('obs_cat'); @@ -220,6 +231,7 @@ final class AttributeActionTest extends TestCase $dataset->setDataPath('/mnt/obs_cat'); $dataset->setSelectableRow(false); $dataset->setProject($project); + $dataset->setInstance($instance); $dataset->setDatasetFamily($family); $this->entityManager->persist($dataset); $this->entityManager->flush(); diff --git a/tests/Action/AttributeListActionTest.php b/tests/Action/AttributeListActionTest.php index f49816c..295327c 100644 --- a/tests/Action/AttributeListActionTest.php +++ b/tests/Action/AttributeListActionTest.php @@ -20,6 +20,7 @@ use Slim\Exception\HttpNotFoundException; use App\tests\EntityManagerBuilder; use App\Entity\Database; use App\Entity\Project; +use App\Entity\Instance; use App\Entity\DatasetFamily; use App\Entity\Dataset; use App\Entity\Attribute; @@ -97,6 +98,15 @@ final class AttributeListActionTest extends TestCase return $project; } + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + private function addDatasetFamily(): DatasetFamily { $family = new DatasetFamily(); @@ -110,6 +120,7 @@ final class AttributeListActionTest extends TestCase private function addADataset(): Dataset { $project = $this->addProject(); + $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset = new Dataset('obs_cat'); @@ -122,6 +133,7 @@ final class AttributeListActionTest extends TestCase $dataset->setDataPath('/mnt/obs_cat'); $dataset->setSelectableRow(false); $dataset->setProject($project); + $dataset->setInstance($instance); $dataset->setDatasetFamily($family); $this->entityManager->persist($dataset); $this->entityManager->flush(); diff --git a/tests/Action/DatasetActionTest.php b/tests/Action/DatasetActionTest.php index d3f4f54..15e3ec3 100644 --- a/tests/Action/DatasetActionTest.php +++ b/tests/Action/DatasetActionTest.php @@ -20,6 +20,7 @@ use Slim\Exception\HttpBadRequestException; use App\tests\EntityManagerBuilder; use App\Entity\Database; use App\Entity\Project; +use App\Entity\Instance; use App\Entity\DatasetFamily; use App\Entity\Dataset; @@ -92,7 +93,7 @@ final class DatasetActionTest extends TestCase array_merge( ['name' => 'obs_cat', 'table_ref' => 'v_obs_cat'], $fields, - ['project_name' => 'anis_project', 'id_dataset_family' => 1] + ['project_name' => 'anis_project', 'instance_name' => 'aspic', 'id_dataset_family' => 1] ), JSON_UNESCAPED_SLASHES ), @@ -101,7 +102,7 @@ final class DatasetActionTest extends TestCase $this->assertEquals(200, (int) $response->getStatusCode()); } - public function testDeleteADatabase(): void + public function testDeleteADataset(): void { $this->addADataset(); $request = $this->getRequest('DELETE'); @@ -162,6 +163,15 @@ final class DatasetActionTest extends TestCase return $project; } + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + private function addDatasetFamily(): DatasetFamily { $family = new DatasetFamily(); @@ -176,6 +186,7 @@ final class DatasetActionTest extends TestCase private function addADataset(): Dataset { $project = $this->addProject(); + $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset = new Dataset('obs_cat'); @@ -188,6 +199,7 @@ final class DatasetActionTest extends TestCase $dataset->setDataPath('/mnt/obs_cat'); $dataset->setSelectableRow(false); $dataset->setProject($project); + $dataset->setInstance($instance); $dataset->setDatasetFamily($family); $this->entityManager->persist($dataset); $this->entityManager->flush(); diff --git a/tests/Action/DatasetListActionTest.php b/tests/Action/DatasetListActionTest.php index dfcea82..2574981 100644 --- a/tests/Action/DatasetListActionTest.php +++ b/tests/Action/DatasetListActionTest.php @@ -24,6 +24,7 @@ use Doctrine\DBAL\Types\Type; use App\tests\EntityManagerBuilder; use App\Entity\Database; use App\Entity\Project; +use App\Entity\Instance; use App\Entity\DatasetFamily; use App\Entity\Dataset; @@ -78,6 +79,7 @@ final class DatasetListActionTest extends TestCase public function testAddANewDatasetFamilyNotFound(): void { $this->addProject(); + $this->addInstance(); $this->expectException(HttpBadRequestException::class); $this->expectExceptionMessage('Dataset family with id 1 is not found'); $fields = $this->getNewDatasetFields(); @@ -86,9 +88,21 @@ final class DatasetListActionTest extends TestCase $this->assertEquals(400, (int) $response->getStatusCode()); } + public function testAddANewDatasetInstanceNotFound(): void + { + $this->addProject(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Instance with name aspic is not found'); + $fields = $this->getNewDatasetFields(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + public function testAddANewDataset(): void { $this->addProject(); + $this->addInstance(); $this->addDatasetFamily(); $fields = $this->getNewDatasetFields(); $request = $this->getRequest('POST')->withParsedBody($fields); @@ -125,6 +139,7 @@ final class DatasetListActionTest extends TestCase 'data_path' => '/mnt/dataset1', 'selectable_row' => false, 'project_name' => 'anis_project', + 'instance_name' => 'aspic', 'id_dataset_family' => 1 ); } @@ -153,6 +168,15 @@ final class DatasetListActionTest extends TestCase return $project; } + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + private function addDatasetFamily(): DatasetFamily { $family = new DatasetFamily(); @@ -167,6 +191,7 @@ final class DatasetListActionTest extends TestCase private function addDatasets(): array { $project = $this->addProject(); + $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset1 = new Dataset('dataset1'); @@ -179,6 +204,7 @@ final class DatasetListActionTest extends TestCase $dataset1->setDataPath('/mnt/dataset1'); $dataset1->setSelectableRow(false); $dataset1->setProject($project); + $dataset1->setInstance($instance); $dataset1->setDatasetFamily($family); $this->entityManager->persist($dataset1); @@ -192,6 +218,7 @@ final class DatasetListActionTest extends TestCase $dataset2->setDataPath('/mnt/dataset2'); $dataset2->setSelectableRow(false); $dataset2->setProject($project); + $dataset2->setInstance($instance); $dataset2->setDatasetFamily($family); $this->entityManager->persist($dataset2); diff --git a/tests/Action/InstanceActionTest.php b/tests/Action/InstanceActionTest.php new file mode 100644 index 0000000..743bc20 --- /dev/null +++ b/tests/Action/InstanceActionTest.php @@ -0,0 +1,111 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Instance; + +final class InstanceActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\InstanceAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testInstanceIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Instance with name aspic is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAnInstanceByName(): void + { + $instance = $this->addAnInstance(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $this->assertSame(json_encode($instance), (string) $response->getBody()); + } + + public function testEditAnInstanceEmptyLabelField(): void + { + $this->addAnInstance(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the instance'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditAnInstance(): void + { + $fields = array( + 'label' => 'AspiC', + 'client_url' => 'http://aspic.lam.fr' + ); + $this->addAnInstance(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $this->assertSame(json_encode(array_merge(['name' => 'aspic'], $fields)), (string) $response->getBody()); + } + + public function testDeleteAnInstance(): void + { + $this->addAnInstance(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $this->assertSame( + json_encode(array('message' => 'Instance with name aspic is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/instance/aspic', array( + 'Content-Type' => 'application/json' + )); + } + + private function addAnInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } +} diff --git a/tests/Action/InstanceListActionTest.php b/tests/Action/InstanceListActionTest.php new file mode 100644 index 0000000..9977f2c --- /dev/null +++ b/tests/Action/InstanceListActionTest.php @@ -0,0 +1,101 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Instance; + +final class InstanceListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\InstanceListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testGetAllInstances(): void + { + $instances = $this->addInstances(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($instances), + (string) $response->getBody() + ); + } + + public function testAddANewInstanceEmptyNameField(): void + { + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param name needed to add a new instance'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array()); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewInstance(): void + { + $fields = array( + 'name' => 'aspic', + 'label' => 'Aspic', + 'client_url' => 'http://cesam.lam.fr/aspic' + ); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame( + json_encode($fields), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/instance', array( + 'Content-Type' => 'application/json' + )); + } + + private function addInstances(): array + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + + $instance2 = new Instance('exodat', 'ExoDat'); + $instance2->setClientUrl('http://cesam.lam.fr/exodat'); + $this->entityManager->persist($instance2); + + $this->entityManager->flush(); + return array($instance, $instance2); + } +} -- GitLab From bfefedb4a9a6837385b7bb5f1c2986a86cdadcd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Tue, 10 Dec 2019 15:39:13 +0100 Subject: [PATCH 04/31] Modifying dataset route to adapt instance feature --- app/routes.php | 2 +- src/Action/DatasetListAction.php | 23 +++++++++-------- tests/Action/DatasetListActionTest.php | 35 +++++++++++++------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/app/routes.php b/app/routes.php index b21d644..6871f36 100644 --- a/app/routes.php +++ b/app/routes.php @@ -24,7 +24,7 @@ $app->map([OPTIONS, GET, POST], '/output-category', App\Action\OutputCategoryLis $app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class); $app->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/instance/{name}', App\Action\InstanceAction::class); -$app->map([OPTIONS, GET, POST], '/dataset', App\Action\DatasetListAction::class); +$app->map([OPTIONS, GET, POST], '/instance/{name}/dataset', App\Action\DatasetListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class); $app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class); $app->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class); diff --git a/src/Action/DatasetListAction.php b/src/Action/DatasetListAction.php index dfee85e..5f5c7a6 100644 --- a/src/Action/DatasetListAction.php +++ b/src/Action/DatasetListAction.php @@ -15,6 +15,7 @@ namespace App\Action; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; use Doctrine\ORM\EntityManagerInterface; use App\Utils\DBALConnectionFactory; use App\Entity\Database; @@ -56,8 +57,18 @@ final class DatasetListAction extends AbstractAction return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); } + $instance = $this->em->find('App\Entity\Instance', $args['name']); + + // Returns HTTP 404 if the instance is not found + if (is_null($instance)) { + throw new HttpNotFoundException( + $request, + 'Instance with name ' . $args['name'] . ' is not found' + ); + } + if ($request->getMethod() === GET) { - $datasets = $this->em->getRepository('App\Entity\Dataset')->findAll(); + $datasets = $this->em->getRepository('App\Entity\Dataset')->findByInstance($instance); $payload = json_encode($datasets); } @@ -76,7 +87,6 @@ final class DatasetListAction extends AbstractAction 'data_path', 'selectable_row', 'project_name', - 'instance_name', 'id_dataset_family' ); foreach ($fields as $a) { @@ -97,15 +107,6 @@ final class DatasetListAction extends AbstractAction ); } - // Instance is mandatory to add a new dataset - $instance = $this->em->find('App\Entity\Instance', $parsedBody['instance_name']); - if (is_null($instance)) { - throw new HttpBadRequestException( - $request, - 'Instance with name ' . $parsedBody['instance_name'] . ' is not found' - ); - } - $family = $this->em->find('App\Entity\DatasetFamily', $parsedBody['id_dataset_family']); if (is_null($family)) { throw new HttpBadRequestException( diff --git a/tests/Action/DatasetListActionTest.php b/tests/Action/DatasetListActionTest.php index 2574981..9be0dce 100644 --- a/tests/Action/DatasetListActionTest.php +++ b/tests/Action/DatasetListActionTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase; use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Response; use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\SqliteSchemaManager; use Doctrine\DBAL\Schema\Column; @@ -46,11 +47,20 @@ final class DatasetListActionTest extends TestCase $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); } + public function testInstanceNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Instance with name aspic is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + public function testGetAllDatasets(): void { $datasets = $this->addDatasets(); $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); $this->assertSame( json_encode($datasets), (string) $response->getBody() @@ -59,20 +69,22 @@ final class DatasetListActionTest extends TestCase public function testAddANewDatasetEmptyNameField(): void { + $this->addInstance(); $this->expectException(HttpBadRequestException::class); $this->expectExceptionMessage('Param name needed to add a new dataset'); $request = $this->getRequest('POST')->withParsedBody(array()); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); $this->assertEquals(400, (int) $response->getStatusCode()); } public function testAddANewDatasetProjectNotFound(): void { + $this->addInstance(); $this->expectException(HttpBadRequestException::class); $this->expectExceptionMessage('Project with name anis_project is not found'); $fields = $this->getNewDatasetFields(); $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); $this->assertEquals(400, (int) $response->getStatusCode()); } @@ -84,18 +96,7 @@ final class DatasetListActionTest extends TestCase $this->expectExceptionMessage('Dataset family with id 1 is not found'); $fields = $this->getNewDatasetFields(); $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - } - - public function testAddANewDatasetInstanceNotFound(): void - { - $this->addProject(); - $this->expectException(HttpBadRequestException::class); - $this->expectExceptionMessage('Instance with name aspic is not found'); - $fields = $this->getNewDatasetFields(); - $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); $this->assertEquals(400, (int) $response->getStatusCode()); } @@ -106,7 +107,7 @@ final class DatasetListActionTest extends TestCase $this->addDatasetFamily(); $fields = $this->getNewDatasetFields(); $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('name' => 'aspic')); $this->assertSame( json_encode($fields), (string) $response->getBody() @@ -121,7 +122,7 @@ final class DatasetListActionTest extends TestCase private function getRequest(string $method): ServerRequest { - return new ServerRequest($method, '/dataset', array( + return new ServerRequest($method, '/instance/aspic/dataset', array( 'Content-Type' => 'application/json' )); } -- GitLab From b80f36c9edcec5318702ad691786dc5fc6e3de93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Tue, 10 Dec 2019 21:27:10 +0100 Subject: [PATCH 05/31] Tests + code coverage --- src/Entity/Attribute.php | 11 +- src/Entity/CriteriaFamily.php | 12 -- src/Entity/Dataset.php | 45 +------ src/Entity/DatasetFamily.php | 19 +-- src/Entity/DatasetPrivileges.php | 21 +-- src/Entity/File.php | 149 --------------------- src/Entity/Group.php | 12 -- src/Entity/OutputCategory.php | 14 +- src/Entity/OutputFamily.php | 12 -- src/Entity/Project.php | 12 -- src/Entity/User.php | 90 +------------ src/Middleware/AuthorizationMiddleware.php | 137 ------------------- tests/Action/FamilyActionTest.php | 2 +- tests/Action/FamilyListActionTest.php | 34 ++++- 14 files changed, 43 insertions(+), 527 deletions(-) delete mode 100644 src/Entity/File.php delete mode 100644 src/Middleware/AuthorizationMiddleware.php diff --git a/src/Entity/Attribute.php b/src/Entity/Attribute.php index 30ecbea..210379b 100644 --- a/src/Entity/Attribute.php +++ b/src/Entity/Attribute.php @@ -32,7 +32,7 @@ class Attribute implements \JsonSerializable * @var App\Entity\Dataset * * @Id - * @ManyToOne(targetEntity="Dataset", inversedBy="attributes") + * @ManyToOne(targetEntity="Dataset") * @JoinColumn(name="dataset_name", referencedColumnName="name", nullable=false) */ protected $dataset; @@ -250,7 +250,7 @@ class Attribute implements \JsonSerializable /** * @var App\Entity\CriteriaFamily * - * @ManyToOne(targetEntity="CriteriaFamily", inversedBy="attributes") + * @ManyToOne(targetEntity="CriteriaFamily") * @JoinColumn(name="criteria_family", referencedColumnName="id", nullable=true) */ protected $criteriaFamily; @@ -258,7 +258,7 @@ class Attribute implements \JsonSerializable /** * @var App\Entity\OutputCategory * - * @ManyToOne(targetEntity="OutputCategory", inversedBy="attributes") + * @ManyToOne(targetEntity="OutputCategory") * @JoinColumn(name="output_category", referencedColumnName="id", nullable=true) */ protected $outputCategory; @@ -574,11 +574,6 @@ class Attribute implements \JsonSerializable $this->options = $options; } - public function getDataset() - { - return $this->dataset; - } - public function getCriteriaFamily() { return $this->criteriaFamily; diff --git a/src/Entity/CriteriaFamily.php b/src/Entity/CriteriaFamily.php index 1646654..d5e43e4 100644 --- a/src/Entity/CriteriaFamily.php +++ b/src/Entity/CriteriaFamily.php @@ -41,13 +41,6 @@ class CriteriaFamily implements \JsonSerializable */ protected $display; - /** - * @var Anis\Entity\Attribute[] - * - * @OneToMany(targetEntity="Attribute", mappedBy="criteriaFamily") - */ - protected $attributes; - public function getId() { return $this->id; @@ -73,11 +66,6 @@ class CriteriaFamily implements \JsonSerializable $this->display = $display; } - public function getAttributes() - { - return $this->attributes; - } - public function jsonSerialize() { return [ diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index 340fc66..1740f0d 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -87,7 +87,7 @@ class Dataset implements \JsonSerializable /** * @var Anis\Entity\Project * - * @ManyToOne(targetEntity="Project", inversedBy="datasets") + * @ManyToOne(targetEntity="Project") * @JoinColumn(name="project_name", referencedColumnName="name", nullable=false) */ protected $project; @@ -103,31 +103,10 @@ class Dataset implements \JsonSerializable /** * @var Anis\Entity\Project * - * @ManyToOne(targetEntity="DatasetFamily", inversedBy="datasets") + * @ManyToOne(targetEntity="DatasetFamily") * @JoinColumn(name="id_dataset_family", referencedColumnName="id", nullable=true) */ protected $datasetFamily; - - /** - * @var Anis\Entity\Attribute[] - * - * @OneToMany(targetEntity="Attribute", mappedBy="dataset", cascade={"remove"}) - */ - protected $attributes; - - /** - * @var Anis\Entity\File[] - * - * @OneToMany(targetEntity="File", mappedBy="dataset", cascade={"remove"}) - */ - protected $files; - - /** - * @var Anis\Entity\DatasetPrivileges - * - * @OneToMany(targetEntity="DatasetPrivileges", mappedBy="dataset") - */ - protected $datasetPrivileges; public function __construct($name) { @@ -249,26 +228,6 @@ class Dataset implements \JsonSerializable { $this->datasetFamily = $datasetFamily; } - - public function getAttributes() - { - return $this->attributes; - } - - public function getFiles() - { - return $this->files; - } - - public function getJoins() - { - return $this->joins; - } - - public function getDatasetPrivileges() - { - return $this->datasetPrivileges; - } public function jsonSerialize() { diff --git a/src/Entity/DatasetFamily.php b/src/Entity/DatasetFamily.php index 1137e75..f679255 100644 --- a/src/Entity/DatasetFamily.php +++ b/src/Entity/DatasetFamily.php @@ -43,13 +43,6 @@ class DatasetFamily implements \JsonSerializable */ protected $display; - /** - * @var Anis\Entity\Dataset[] - * - * @OneToMany(targetEntity="Dataset", mappedBy="datasetFamily") - */ - protected $datasets; - public function __construct() { $this->datasets = new ArrayCollection(); @@ -80,23 +73,13 @@ class DatasetFamily implements \JsonSerializable $this->display = (int) $display; } - public function getDatasets() - { - return $this->datasets; - } - public function jsonSerialize() { - $datasets = array(); - foreach ($this->getDatasets() as $dataset) { - $datasets[] = $dataset->getName(); - } return [ 'id' => $this->getId(), 'label' => $this->getLabel(), 'display' => $this->getDisplay(), - 'type' => 'dataset', - 'datasets' => $datasets + 'type' => 'dataset' ]; } } diff --git a/src/Entity/DatasetPrivileges.php b/src/Entity/DatasetPrivileges.php index d7c4361..8fbc8d0 100644 --- a/src/Entity/DatasetPrivileges.php +++ b/src/Entity/DatasetPrivileges.php @@ -22,7 +22,7 @@ class DatasetPrivileges * @var Anis\Entity\Dataset * * @Id - * @ManyToOne(targetEntity="Dataset", inversedBy="datasetPrivileges") + * @ManyToOne(targetEntity="Dataset") * @JoinColumn(name="dataset_name", referencedColumnName="name", nullable=false) */ protected $dataset; @@ -36,17 +36,10 @@ class DatasetPrivileges */ protected $group; - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $visible; - public function __construct(Dataset $dataset, Group $group) { $this->dataset = $dataset; - $this->group = $group; + $this->group = $group; } public function getDataset() @@ -58,14 +51,4 @@ class DatasetPrivileges { return $this->group; } - - public function getVisible() - { - return $this->visible; - } - - public function setVisible($visible) - { - $this->visible = (bool) $visible; - } } diff --git a/src/Entity/File.php b/src/Entity/File.php deleted file mode 100644 index 1065603..0000000 --- a/src/Entity/File.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Entity; - -/** -* @Entity -* @Table(name="file") -*/ -class File implements \JsonSerializable -{ - /** - * @var integer - * - * @Id - * @Column(type="integer", nullable=false) - * @GeneratedValue - */ - protected $id; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $label; - - /** - * @var string - * - * @Column(type="string", name="file_loc", nullable=true) - */ - protected $fileLoc; - - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $type; - - /** - * @var integer - * - * @Column(type="integer", nullable=false) - */ - protected $display; - - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $visible; - - /** - * @var Anis\Entity\Dataset - * - * @ManyToOne(targetEntity="Dataset", inversedBy="files") - * @JoinColumn(name="dataset_name", referencedColumnName="name", nullable=false) - */ - protected $dataset; - - public function __construct(Dataset $dataset) - { - $this->dataset = $dataset; - } - - public function getId() - { - return $this->id; - } - - public function getLabel() - { - return $this->label; - } - - public function setLabel($label) - { - $this->label = $label; - } - - public function getFileLoc() - { - return $this->fileLoc; - } - - public function setFileLoc($fileLoc) - { - $this->fileLoc = $fileLoc; - } - - public function getType() - { - return $this->type; - } - - public function setType($type) - { - $this->type = $type; - } - - public function getDisplay() - { - return $this->display; - } - - public function setDisplay($display) - { - $this->display = (int) $display; - } - - public function getVisible() - { - return $this->visible; - } - - public function setVisible($visible) - { - $this->visible = $visible; - } - - public function getDataset() - { - return $this->dataset; - } - - public function jsonSerialize() - { - return [ - 'id' => $this->getId(), - 'label' => $this->getLabel(), - 'file_loc' => $this->getFileLoc(), - 'type' => $this->getType(), - 'display' => $this->getDisplay(), - 'visible' => $this->getVisible() - ]; - } -} diff --git a/src/Entity/Group.php b/src/Entity/Group.php index f4fe223..006cdf3 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -34,13 +34,6 @@ class Group implements \JsonSerializable */ protected $label; - /** - * @var Anis\Entity\User[] - * - * @OneToMany(targetEntity="User", mappedBy="group") - */ - protected $users; - /** * @var Anis\Entity\DatasetPrivileges * @@ -63,11 +56,6 @@ class Group implements \JsonSerializable $this->label = $label; } - public function getUsers() - { - return $this->users; - } - public function getDatasetPrivileges() { return $this->datasetPrivileges; diff --git a/src/Entity/OutputCategory.php b/src/Entity/OutputCategory.php index 9fdff31..f848f34 100644 --- a/src/Entity/OutputCategory.php +++ b/src/Entity/OutputCategory.php @@ -44,17 +44,10 @@ class OutputCategory implements \JsonSerializable /** * @var Anis\Entity\OutputFamily * - * @ManyToOne(targetEntity="OutputFamily", inversedBy="outputCategories") + * @ManyToOne(targetEntity="OutputFamily") * @JoinColumn(name="output_family", referencedColumnName="id", nullable=false) */ protected $outputFamily; - - /** - * @var Anis\Entity\Attribute[] - * - * @OneToMany(targetEntity="Attribute", mappedBy="outputCategory") - */ - protected $attributes; public function getId() { @@ -91,11 +84,6 @@ class OutputCategory implements \JsonSerializable return $this->outputFamily; } - public function getAttributes() - { - return $this->attributes; - } - public function jsonSerialize() { return [ diff --git a/src/Entity/OutputFamily.php b/src/Entity/OutputFamily.php index d768a47..e9ca386 100644 --- a/src/Entity/OutputFamily.php +++ b/src/Entity/OutputFamily.php @@ -41,13 +41,6 @@ class OutputFamily implements \JsonSerializable */ protected $display; - /** - * @var Anis\Entity\OutputCategory[] - * - * @OneToMany(targetEntity="OutputCategory", mappedBy="outputFamily", cascade={"remove"}) - */ - protected $outputCategories; - public function getId() { return $this->id; @@ -73,11 +66,6 @@ class OutputFamily implements \JsonSerializable return $this->display; } - public function getOutputCategories() - { - return $this->outputCategories; - } - public function jsonSerialize() { return [ diff --git a/src/Entity/Project.php b/src/Entity/Project.php index e4f642f..67e1e09 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -62,13 +62,6 @@ class Project implements \JsonSerializable */ protected $database; - /** - * @var Anis\Entity\Dataset[] - * - * @OneToMany(targetEntity="Dataset", mappedBy="project", cascade={"remove"}) - */ - protected $datasets; - public function __construct($name) { $this->name = $name; @@ -129,11 +122,6 @@ class Project implements \JsonSerializable return $this->database; } - public function getDatasets() - { - return $this->datasets; - } - public function jsonSerialize() { return [ diff --git a/src/Entity/User.php b/src/Entity/User.php index 17a5294..c2e1089 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -26,45 +26,10 @@ class User implements \JsonSerializable */ protected $email; - /** - * @var string - * - * @Column(type="string", nullable=false) - */ - protected $password; - - /** - * @var string - * - * @Column(type="string", name="activation_key", nullable=false) - */ - protected $activationKey; - - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $activated; - - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $adminsi; - - /** - * @var bool - * - * @Column(type="boolean", nullable=false) - */ - protected $superuser; - /** * @var Anis\Entity\Group * - * @ManyToOne(targetEntity="Group", inversedBy="users") + * @ManyToOne(targetEntity="Group") * @JoinColumn(name="group_id", referencedColumnName="id", nullable=true) */ protected $group; @@ -79,56 +44,6 @@ class User implements \JsonSerializable return $this->email; } - public function getPassword() - { - return $this->password; - } - - public function setPassword($password) - { - $this->password = $password; - } - - public function getActivationKey() - { - return $this->activationKey; - } - - public function setActivationKey($activationKey) - { - $this->activationKey = $activationKey; - } - - public function getActivated() - { - return $this->activated; - } - - public function setActivated($activated) - { - $this->activated = $activated; - } - - public function getAdminsi() - { - return $this->adminsi; - } - - public function setAdminsi($adminsi) - { - $this->adminsi = $adminsi; - } - - public function getSuperuser() - { - return $this->superuser; - } - - public function setSuperuser($superuser) - { - $this->superuser = $superuser; - } - public function getGroup() { return $this->group; @@ -143,9 +58,6 @@ class User implements \JsonSerializable { return [ 'email' => $this->getEmail(), - 'activated' => $this->getActivated(), - 'adminsi' => $this->getAdminsi(), - 'superuser' => $this->getSuperuser(), 'id_group' => $this->getGroup()->getId() ]; } diff --git a/src/Middleware/AuthorizationMiddleware.php b/src/Middleware/AuthorizationMiddleware.php deleted file mode 100644 index 739dd60..0000000 --- a/src/Middleware/AuthorizationMiddleware.php +++ /dev/null @@ -1,137 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Middleware; - -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\RequestHandlerInterface as RequestHandler; -use Psr\Http\Server\MiddlewareInterface; -use Slim\Exception\HttpBadRequestException; -use Slim\Exception\HttpUnauthorizedException; -use Doctrine\ORM\EntityManagerInterface; -use Lcobucci\JWT\ValidationData; -use Lcobucci\JWT\Parser; -use Lcobucci\JWT\Signer\Hmac\Sha256; - -/** - * Middleware to handle authentication jwt - * - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Middleware - */ -final class AuthorizationMiddleware implements MiddlewareInterface -{ - /** - * The EntityManager is the central access point to Doctrine ORM functionality - * - * @var EntityManagerInterface - */ - protected $em; - - /** - * This object contains token options from settings file - * - * @var array - */ - private $tokenOptions; - - /** - * Create the classe before call process to execute the middleware - * - * @param array $tokenOptions Options to create the token - */ - public function __construct(EntityManagerInterface $em, array $tokenOptions) - { - $this->em = $em; - $this->tokenOptions = $tokenOptions; - } - - /** - * Handle jwt authentication - * - * @param ServerRequest $request PSR-7 request - * @param RequestHandler $handler PSR-15 request handler - * - * @return Response - */ - public function process(Request $request, RequestHandler $handler): Response - { - if (!$request->hasHeader('Authorization')) { - throw new HttpBadRequestException( - $request, - 'Header Authorization needed for this route' - ); - } - - $authorization = $request->getHeaderLine('Authorization'); - - list($title, $jwt) = explode(' ', $authorization); - if ($title !== 'Bearer') { - throw new HttpBadRequestException( - $request, - 'The format of the header Authorization must be Bearer jwt' - ); - } - - $token = (new Parser())->parse((string) $jwt); - - if (!$token->validate($this->getValidationData())) { - throw new HttpUnauthorizedException( - $request, - 'The jwt token is not validate' - ); - } - - $key = file_get_contents($this->tokenOptions['key']); - if (!$token->verify(new Sha256(), $key)) { - throw new HttpUnauthorizedException( - $request, - 'The jwt token signature is not verify' - ); - } - - $jti = $token->getClaim('jti'); - if ($this->getRevoked($jti)) { - throw new HttpUnauthorizedException( - $request, - 'The jwt token is revoked' - ); - } - - return $handler->handle($request->withAttribute('superuser', $token->getClaim('superuser'))); - } - - /** - * Create and return the object used to validate a token - * - * @return ValidationData The object to validate a token - */ - private function getValidationData(): ValidationData - { - $validationData = new ValidationData(); - $validationData->setIssuer($this->tokenOptions['iss']); - $validationData->setAudience($this->tokenOptions['aud']); - return $validationData; - } - - /** - * Return the state of the token (revoked or not) - * - * @return bool The state of the token - */ - private function getRevoked($jti): bool - { - $entityToken = $this->em->find('App\Entity\Token', $jti); - return $entityToken->getRevoked(); - } -} diff --git a/tests/Action/FamilyActionTest.php b/tests/Action/FamilyActionTest.php index 313d9a2..84ba23c 100644 --- a/tests/Action/FamilyActionTest.php +++ b/tests/Action/FamilyActionTest.php @@ -84,7 +84,7 @@ final class FamilyActionTest extends TestCase $request = $this->getRequest('PUT')->withParsedBody($fields); $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); $this->assertSame( - json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset', 'datasets' => array()])), + json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset'])), (string) $response->getBody() ); } diff --git a/tests/Action/FamilyListActionTest.php b/tests/Action/FamilyListActionTest.php index 2c7ab2b..8206c58 100644 --- a/tests/Action/FamilyListActionTest.php +++ b/tests/Action/FamilyListActionTest.php @@ -66,7 +66,7 @@ final class FamilyListActionTest extends TestCase $this->assertEquals(400, (int) $response->getStatusCode()); } - public function testAddANewDatabase(): void + public function testAddANewDatasetFamily(): void { $fields = array( 'label' => 'Default family', @@ -75,7 +75,37 @@ final class FamilyListActionTest extends TestCase $request = $this->getRequest('POST')->withParsedBody($fields); $response = ($this->action)($request, new Response(), array('type' => 'dataset')); $this->assertSame( - json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset', 'datasets' => array()])), + json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset'])), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + public function testAddANewCriteriaFamily(): void + { + $fields = array( + 'label' => 'Default criteria', + 'display' => 10 + ); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('type' => 'criteria')); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields, ['type' => 'criteria'])), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + public function testAddANewOutputFamily(): void + { + $fields = array( + 'label' => 'Default output', + 'display' => 10 + ); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('type' => 'output')); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields, ['type' => 'output'])), (string) $response->getBody() ); $this->assertEquals(201, (int) $response->getStatusCode()); -- GitLab From 903c16a9e0f993f0d014bd8e6f8fb5594cd3ede7 Mon Sep 17 00:00:00 2001 From: Tifenn GUILLAS <tifenn.guillas@lam.fr> Date: Mon, 16 Dec 2019 15:04:24 +0100 Subject: [PATCH 06/31] Add CORS support --- app/middlewares.php | 1 + docker-compose.yml | 2 +- src/Middleware/CorsMiddleware.php | 49 +++++++++++++++++++++ tests/Middleware/CorsMiddlewareTest.php | 58 +++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/CorsMiddleware.php create mode 100644 tests/Middleware/CorsMiddlewareTest.php diff --git a/app/middlewares.php b/app/middlewares.php index b029af0..0b27e31 100644 --- a/app/middlewares.php +++ b/app/middlewares.php @@ -12,3 +12,4 @@ declare(strict_types=1); $app->add(new App\Middleware\JsonBodyParserMiddleware()); $app->add(new App\Middleware\ContentTypeJsonMiddleware()); +$app->add(new App\Middleware\CorsMiddleware()); diff --git a/docker-compose.yml b/docker-compose.yml index 704697d..42f384a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: LOGGER_PATH: "php://stderr" LOGGER_LEVEL: "debug" ports: - - 8082:80 + - 8080:80 volumes: - .:/project - ./conf-dev/dev-php.ini:/usr/local/etc/php/conf.d/dev-php.ini diff --git a/src/Middleware/CorsMiddleware.php b/src/Middleware/CorsMiddleware.php new file mode 100644 index 0000000..ea050f8 --- /dev/null +++ b/src/Middleware/CorsMiddleware.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of Anis Auth. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Middleware; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use Psr\Http\Server\MiddlewareInterface; + +/** + * Middleware to allow resources to be requested from another origin + * + * @author Tifenn Guillas <tifenn.guillas@lam.fr> + * @package App\Middleware + */ +final class CorsMiddleware implements MiddlewareInterface +{ + /** + * Allow resources to be requested from another origin + * + * @param ServerRequest $request PSR-7 request + * @param RequestHandler $handler PSR-15 request handler + * + * @return Response + */ + public function process(Request $request, RequestHandler $handler): Response + { + $response = $handler->handle($request); + + if ($request->getMethod() === OPTIONS) { + return $response + ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization') + ->withHeader('Access-Control-Allow-Origin', '*'); + } + + return $response + ->withHeader('Access-Control-Allow-Origin', '*'); + } +} diff --git a/tests/Middleware/CorsMiddlewareTest.php b/tests/Middleware/CorsMiddlewareTest.php new file mode 100644 index 0000000..d74c690 --- /dev/null +++ b/tests/Middleware/CorsMiddlewareTest.php @@ -0,0 +1,58 @@ +<?php + +/* + * This file is part of Anis Auth. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Middleware; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; + +final class CorsMiddlewareTest extends TestCase +{ + public function testCorsHeadersForOptionsMethod() + { + $request = new ServerRequest('OPTIONS', '/'); + + $requestHandler = $this->getMockBuilder(RequestHandler::class) + ->disableOriginalConstructor() + ->setMethods(['handle']) + ->getMock(); + + $requestHandler->method('handle') + ->with($this->identicalTo($request)) + ->will($this->returnValue(new Response())); + + $corsMiddleware = new \App\Middleware\CorsMiddleware(); + $response = $corsMiddleware->process($request, $requestHandler); + $this->assertSame((string) $response->getHeaderLine('Access-Control-Allow-Origin'), '*'); + $this->assertSame('Content-Type, Authorization', (string) $response->getHeaderLine('Access-Control-Allow-Headers')); + } + + public function testCorsHeadersForGetMethod() + { + $request = new ServerRequest('GET', '/'); + + $requestHandler = $this->getMockBuilder(RequestHandler::class) + ->disableOriginalConstructor() + ->setMethods(['handle']) + ->getMock(); + + $requestHandler->method('handle') + ->with($this->identicalTo($request)) + ->will($this->returnValue(new Response())); + + $corsMiddleware = new \App\Middleware\CorsMiddleware(); + $response = $corsMiddleware->process($request, $requestHandler); + $this->assertSame((string) $response->getHeaderLine('Access-Control-Allow-Origin'), '*'); + } +} -- GitLab From af8995abada93ef3ec46866977bdb8626186f546 Mon Sep 17 00:00:00 2001 From: Tifenn GUILLAS <tifenn.guillas@lam.fr> Date: Mon, 16 Dec 2019 15:11:27 +0100 Subject: [PATCH 07/31] Change port 8082 for 8080 --- README.md | 8 ++++---- anis-server.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8b3e7ff..238d2bd 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ You can find an open api documentation into the `anis-server.yaml` file. ## Few examples with curl -* To list all datasets available in the default instance => http://localhost:8082/dataset -* To print all data for the obs_cat dataset with column 1, 2 and 3 => http://localhost:8082/search/obs_cat?a=1;2;3 -* To count the number of data available for a request => http://localhost:8082/search/obs_cat?a=count -* To print only 3 obs_cat data (search by id) => http://localhost:8082/search/obs_cat?a=1;2;3&c=1::in::104600094|104600095|104600108 \ No newline at end of file +* To list all datasets available in the default instance => http://localhost:8080/dataset +* To print all data for the obs_cat dataset with column 1, 2 and 3 => http://localhost:8080/search/obs_cat?a=1;2;3 +* To count the number of data available for a request => http://localhost:8080/search/obs_cat?a=count +* To print only 3 obs_cat data (search by id) => http://localhost:8080/search/obs_cat?a=1;2;3&c=1::in::104600094|104600095|104600108 \ No newline at end of file diff --git a/anis-server.yaml b/anis-server.yaml index 4fc1895..ac525c1 100644 --- a/anis-server.yaml +++ b/anis-server.yaml @@ -9,7 +9,7 @@ info: name: CeCILL 2.1 url: http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.html servers: - - url: http://localhost:8082 + - url: http://localhost:8080 tags: - name: root description: Root path -- GitLab From 25fe9c5ecda3f9d976d947a41c35c0315fe673a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Mon, 16 Dec 2019 15:58:46 +0100 Subject: [PATCH 08/31] Fixed bug: Instance dependancies + dev_meta routes --- app/dependencies.php | 8 ++++++++ conf-dev/dev-meta.sh | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/dependencies.php b/app/dependencies.php index e64cc63..4df1ae2 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -83,6 +83,14 @@ $container->set('App\Action\OutputCategoryAction', function (ContainerInterface return new App\Action\OutputCategoryAction($c->get('em')); }); +$container->set('App\Action\InstanceListAction', function (ContainerInterface $c) { + return new App\Action\InstanceListAction($c->get('em')); +}); + +$container->set('App\Action\InstanceAction', function (ContainerInterface $c) { + return new App\Action\InstanceAction($c->get('em')); +}); + $container->set('App\Action\DatasetListAction', function (ContainerInterface $c) { return new App\Action\DatasetListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); }); diff --git a/conf-dev/dev-meta.sh b/conf-dev/dev-meta.sh index 9db6662..c9efe8a 100644 --- a/conf-dev/dev-meta.sh +++ b/conf-dev/dev-meta.sh @@ -5,5 +5,6 @@ curl -d '{"label":"Test","dbname":"anis_test","dbtype":"pdo_pgsql","dbhost":"db" curl -d '{"name":"anis_project","label":"Anis Project Test","description":"Project used for testing","link":"http://project.com","manager":"M. Durand","id_database":1}' -H "Content-Type: application/json" -X POST http://localhost/project curl -d '{"label":"Default dataset family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/dataset -curl -d '{"name":"obs_cat","table_ref":"obs_cat","label":"ObsCat dataset","description":"ObsCat","display":"10","count":"10000","vo":false,"data_path":"/mnt/mount","selectable_row":true,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/dataset -curl -d '{"name":"observations","table_ref":"observations_info","label":"Observations dataset","description":"Observations","display":"20","count":"177454","vo":false,"data_path":"/mnt/mount","selectable_row":false,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/dataset +curl -d '{"name":"default","label":"Default instance","client_url":"http://localhost:4200"}' -H "Content-Type: application/json" -X POST http://localhost/instance +curl -d '{"name":"obs_cat","table_ref":"obs_cat","label":"ObsCat dataset","description":"ObsCat","display":"10","count":"10000","vo":false,"data_path":"/mnt/mount","selectable_row":true,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/instance/default/dataset +curl -d '{"name":"observations","table_ref":"observations_info","label":"Observations dataset","description":"Observations","display":"20","count":"177454","vo":false,"data_path":"/mnt/mount","selectable_row":false,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/instance/default/dataset -- GitLab From 7235d8afd5fceba383169ea76132b5149f0db22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Mon, 16 Dec 2019 17:42:44 +0100 Subject: [PATCH 09/31] Fixed bug: Dockerfile copy files into /project --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index dc688d2..28aed33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,9 +7,9 @@ RUN apt-get update \ RUN a2enmod rewrite -COPY . /srv/app +COPY . /project COPY ./conf-dev/vhost.conf /etc/apache2/sites-available/000-default.conf -WORKDIR /srv/app +WORKDIR /project CMD ["apache2-foreground"] -- GitLab From 3c6bfe8cb787b22a2070e4bc58ccf7191e895e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Tue, 17 Dec 2019 11:55:24 +0100 Subject: [PATCH 10/31] Add getAttributes method to the dataset class needed by searchAction --- src/Entity/Attribute.php | 2 +- src/Entity/Dataset.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Entity/Attribute.php b/src/Entity/Attribute.php index 210379b..cdb7191 100644 --- a/src/Entity/Attribute.php +++ b/src/Entity/Attribute.php @@ -32,7 +32,7 @@ class Attribute implements \JsonSerializable * @var App\Entity\Dataset * * @Id - * @ManyToOne(targetEntity="Dataset") + * @ManyToOne(targetEntity="Dataset", inversedBy="attributes") * @JoinColumn(name="dataset_name", referencedColumnName="name", nullable=false) */ protected $dataset; diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index 1740f0d..84f07f2 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -107,6 +107,13 @@ class Dataset implements \JsonSerializable * @JoinColumn(name="id_dataset_family", referencedColumnName="id", nullable=true) */ protected $datasetFamily; + + /** + * @var Anis\Entity\Attribute[] + * + * @OneToMany(targetEntity="Attribute", mappedBy="dataset", cascade={"remove"}) + */ + protected $attributes; public function __construct($name) { @@ -229,6 +236,11 @@ class Dataset implements \JsonSerializable $this->datasetFamily = $datasetFamily; } + public function getAttributes() + { + return $this->attributes; + } + public function jsonSerialize() { return [ -- GitLab From d72d5ff3589c445f918f6c052a9a86db2c533a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Wed, 18 Dec 2019 15:17:08 +0100 Subject: [PATCH 11/31] Add create-db shell script --- Makefile | 5 +---- conf-dev/{dev-meta.sh => create-db.sh} | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) rename conf-dev/{dev-meta.sh => create-db.sh} (73%) diff --git a/Makefile b/Makefile index bf6867b..bfbd169 100644 --- a/Makefile +++ b/Makefile @@ -59,10 +59,7 @@ phpcs: -w /project jakzal/phpqa phpcs --standard=PSR12 --extensions=php --colors src tests create-db: - @docker-compose exec php ./vendor/bin/doctrine orm:schema-tool:create - -dev-meta: - @docker-compose exec php sh ./conf-dev/dev-meta.sh + @docker-compose exec php sh ./conf-dev/create-db.sh remove-pgdata: @docker volume rm anis-server_pgdata diff --git a/conf-dev/dev-meta.sh b/conf-dev/create-db.sh similarity index 73% rename from conf-dev/dev-meta.sh rename to conf-dev/create-db.sh index c9efe8a..72f2746 100644 --- a/conf-dev/dev-meta.sh +++ b/conf-dev/create-db.sh @@ -1,6 +1,9 @@ #!/bin/sh set -e +# Create the settings database (only tables) +./vendor/bin/doctrine orm:schema-tool:create + curl -d '{"label":"Test","dbname":"anis_test","dbtype":"pdo_pgsql","dbhost":"db","dbport":5432,"dblogin":"anis","dbpassword":"anis"}' -H "Content-Type: application/json" -X POST http://localhost/database curl -d '{"name":"anis_project","label":"Anis Project Test","description":"Project used for testing","link":"http://project.com","manager":"M. Durand","id_database":1}' -H "Content-Type: application/json" -X POST http://localhost/project @@ -8,3 +11,7 @@ curl -d '{"label":"Default dataset family","display":10}' -H "Content-Type: appl curl -d '{"name":"default","label":"Default instance","client_url":"http://localhost:4200"}' -H "Content-Type: application/json" -X POST http://localhost/instance curl -d '{"name":"obs_cat","table_ref":"obs_cat","label":"ObsCat dataset","description":"ObsCat","display":"10","count":"10000","vo":false,"data_path":"/mnt/mount","selectable_row":true,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/instance/default/dataset curl -d '{"name":"observations","table_ref":"observations_info","label":"Observations dataset","description":"Observations","display":"20","count":"177454","vo":false,"data_path":"/mnt/mount","selectable_row":false,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/instance/default/dataset + +curl -d '{"label":"Default criteria family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/criteria +curl -d '{"label":"Default output family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/output +curl -d '{"label":"Default output category","display":10,"id_output_family":1}' -H "Content-Type: application/json" -X POST http://localhost/output-category -- GitLab From 8d0f6b8c58d13dd5bf7dc8a0ee584b46269e7b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Wed, 18 Dec 2019 22:22:25 +0100 Subject: [PATCH 12/31] WIP --- app/routes.php | 14 ++-- src/Entity/Attribute.php | 134 +++++++++++++++++----------------- src/Entity/CriteriaFamily.php | 30 +++++--- src/Entity/Database.php | 30 ++++---- src/Entity/Dataset.php | 71 +++++++----------- src/Entity/DatasetFamily.php | 26 ++++--- src/Entity/OutputCategory.php | 14 ++-- src/Entity/OutputFamily.php | 23 ++++-- src/Entity/Project.php | 24 +++--- 9 files changed, 192 insertions(+), 174 deletions(-) diff --git a/app/routes.php b/app/routes.php index 6871f36..0056afa 100644 --- a/app/routes.php +++ b/app/routes.php @@ -18,14 +18,18 @@ $app->map([OPTIONS, GET, PUT, DELETE], '/database/{id}', App\Action\DatabaseActi $app->map([OPTIONS, GET], '/database/{id}/table', App\Action\TableListAction::class); $app->map([OPTIONS, GET, POST], '/project', App\Action\ProjectListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/project/{name}', App\Action\ProjectAction::class); -$app->map([OPTIONS, GET, POST], '/family/{type}', App\Action\FamilyListAction::class); -$app->map([OPTIONS, GET, PUT, DELETE], '/family/{type}/{id}', App\Action\FamilyAction::class); -$app->map([OPTIONS, GET, POST], '/output-category', App\Action\OutputCategoryListAction::class); -$app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class); $app->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/instance/{name}', App\Action\InstanceAction::class); -$app->map([OPTIONS, GET, POST], '/instance/{name}/dataset', App\Action\DatasetListAction::class); +$app->map([OPTIONS, GET, POST], '/instance/{name}/dataset-family', App\Action\DatasetFamilyListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/dataset-family/{id}', App\Action\DatasetFamilyAction::class); +$app->map([OPTIONS, GET, POST], '/dataset-family/{id}/dataset', App\Action\DatasetListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class); +$app->map([OPTIONS, GET, POST], '/dataset/{name}/criteria-family', App\Action\CriteriaFamilyListAction::class); +$app->map([OPTIONS, GET, POST], '/dataset/{name}/output-family', App\Action\OutputFamilyListAction::class); $app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class); $app->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/criteria-family/{id}', App\Action\CriteriaFamilyAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/output-family/{id}', App\Action\OutputFamilyAction::class); +$app->map([OPTIONS, GET, POST], '/output-family/{id}/output-category', App\Action\OutputCategoryListAction::class); +$app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class); $app->get('/search/{dname}', App\Action\SearchAction::class); diff --git a/src/Entity/Attribute.php b/src/Entity/Attribute.php index cdb7191..4bcfa35 100644 --- a/src/Entity/Attribute.php +++ b/src/Entity/Attribute.php @@ -263,333 +263,333 @@ class Attribute implements \JsonSerializable */ protected $outputCategory; - public function __construct($id, $dataset) + public function __construct(int $id, Dataset $dataset) { $this->id = $id; $this->dataset = $dataset; } - public function getId() + public function getId(): int { return $this->id; } - public function getName() + public function getName(): string { return $this->name; } - public function setName($name) + public function setName(string $name): void { $this->name = $name; } - public function getTableName() + public function getTableName(): string { return $this->tableName; } - public function setTableName($tableName) + public function setTableName(string $tableName): void { $this->tableName = $tableName; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } - public function getFormLabel() + public function getFormLabel(): string { return $this->formLabel; } - public function setFormLabel($formLabel) + public function setFormLabel(string $formLabel): void { $this->formLabel = $formLabel; } - public function getDescription() + public function getDescription(): string { return $this->description; } - public function setDescription($description) + public function setDescription(string $description): void { $this->description = $description; } - public function getOutputDisplay() + public function getOutputDisplay(): int { return $this->outputDisplay; } - public function setOutputDisplay($outputDisplay) + public function setOutputDisplay(int $outputDisplay): void { $this->outputDisplay = $outputDisplay; } - public function setCriteriaDisplay($criteriaDisplay) + public function setCriteriaDisplay(int $criteriaDisplay): void { $this->criteriaDisplay = $criteriaDisplay; } - public function getCriteriaDisplay() + public function getCriteriaDisplay(): int { return $this->criteriaDisplay; } - public function getSearchFlag() + public function getSearchFlag(): string { return $this->searchFlag; } - public function setSearchFlag($searchFlag) + public function setSearchFlag(string $searchFlag): void { $this->searchFlag = $searchFlag; } - public function getSearchType() + public function getSearchType(): string { return $this->searchType; } - public function setSearchType($searchType) + public function setSearchType(string $searchType): void { $this->searchType = $searchType; } - public function getOperator() + public function getOperator(): string { return $this->operator; } - public function setOperator($operator) + public function setOperator(string $operator): void { $this->operator = $operator; } - public function getType() + public function getType(): string { return $this->type; } - public function setType($type) + public function setType(string $type): void { $this->type = $type; } - public function getMin() + public function getMin(): string { return $this->min; } - public function setMin($min) + public function setMin(string $min): void { $this->min = $min; } - public function getMax() + public function getMax(): string { return $this->max; } - public function setMax($max) + public function setMax(string $max): void { $this->max = $max; } - public function getPlaceholderMin() + public function getPlaceholderMin(): string { return $this->placeholderMin; } - public function setPlaceholderMin($placeholderMin) + public function setPlaceholderMin(string $placeholderMin): void { $this->placeholderMin = $placeholderMin; } - public function getPlaceholderMax() + public function getPlaceholderMax(): string { return $this->placeholderMax; } - public function setPlaceholderMax($placeholderMax) + public function setPlaceholderMax(string $placeholderMax): void { $this->placeholderMax = $placeholderMax; } - public function getUriAction() + public function getUriAction(): string { return $this->uriAction; } - public function setUriAction($uriAction) + public function setUriAction(string $uriAction): void { $this->uriAction = $uriAction; } - public function getRenderer() + public function getRenderer(): string { return $this->renderer; } - public function setRenderer($renderer) + public function setRenderer(string $renderer): void { $this->renderer = $renderer; } - public function getDisplayDetail() + public function getDisplayDetail(): int { return $this->displayDetail; } - public function setDisplayDetail($displayDetail) + public function setDisplayDetail(int $displayDetail): void { $this->displayDetail = $displayDetail; } - public function getVoUtype() + public function getVoUtype(): string { return $this->voUtype; } - public function setVoUtype($voUtype) + public function setVoUtype(string $voUtype): void { $this->voUtype = $voUtype; } - public function getVoUcd() + public function getVoUcd(): string { return $this->voUcd; } - public function setVoUcd($voUcd) + public function setVoUcd(string $voUcd): void { $this->voUcd = $voUcd; } - public function getVoUnit() + public function getVoUnit(): string { return $this->voUnit; } - public function setVoUnit($voUnit) + public function setVoUnit(string $voUnit): void { $this->voUnit = $voUnit; } - public function getVoDescription() + public function getVoDescription(): string { return $this->voDescription; } - public function setVoDescription($voDescription) + public function setVoDescription(string $voDescription): void { $this->voDescription = $voDescription; } - public function getVoDatatype() + public function getVoDatatype(): string { return $this->voDatatype; } - public function setVoDatatype($voDatatype) + public function setVoDatatype(string $voDatatype): void { $this->voDatatype = $voDatatype; } - public function getVoSize() + public function getVoSize(): int { return $this->voSize; } - public function setVoSize($voSize) + public function setVoSize(int $voSize): void { - $this->voSize = (int) $voSize; + $this->voSize = $voSize; } - public function getSelected() + public function getSelected(): bool { return $this->selected; } - public function setSelected($selected) + public function setSelected(bool $selected): void { $this->selected = $selected; } - public function getOrderBy() + public function getOrderBy(): bool { return $this->orderBy; } - public function setOrderBy($orderBy) + public function setOrderBy(bool $orderBy): void { $this->orderBy = $orderBy; } - public function getOrderDisplay() + public function getOrderDisplay(): int { return $this->orderDisplay; } - public function setOrderDisplay($orderDisplay) + public function setOrderDisplay(int $orderDisplay): void { $this->orderDisplay = $orderDisplay; } - public function getDetail() + public function getDetail(): bool { return $this->detail; } - public function setDetail($detail) + public function setDetail(bool $detail): void { $this->detail = $detail; } - public function getRendererDetail() + public function getRendererDetail(): string { return $this->rendererDetail; } - public function setRendererDetail($rendererDetail) + public function setRendererDetail(string $rendererDetail): void { $this->rendererDetail = $rendererDetail; } - public function getOptions() + public function getOptions(): string { return $this->options; } - public function setOptions($options) + public function setOptions(string $options): void { $this->options = $options; } - public function getCriteriaFamily() + public function getCriteriaFamily(): CriteriaFamily { return $this->criteriaFamily; } - public function setCriteriaFamily($criteriaFamily) + public function setCriteriaFamily(CriteriaFamily $criteriaFamily): void { $this->criteriaFamily = $criteriaFamily; } - public function getOutputCategory() + public function getOutputCategory(): OutputCategory { return $this->outputCategory; } - public function setOutputCategory($outputCategory) + public function setOutputCategory(OutputCategory $outputCategory): void { $this->outputCategory = $outputCategory; } diff --git a/src/Entity/CriteriaFamily.php b/src/Entity/CriteriaFamily.php index d5e43e4..158be1d 100644 --- a/src/Entity/CriteriaFamily.php +++ b/src/Entity/CriteriaFamily.php @@ -19,7 +19,7 @@ namespace App\Entity; class CriteriaFamily implements \JsonSerializable { /** - * @var integer + * @var int * * @Id * @Column(type="integer", nullable=false) @@ -35,33 +35,46 @@ class CriteriaFamily implements \JsonSerializable protected $label; /** - * @var integer + * @var int * * @Column(type="integer", nullable=false) */ protected $display; - public function getId() + /** + * @var Anis\Entity\Dataset + * + * @ManyToOne(targetEntity="Dataset") + * @JoinColumn(name="dataset_name", referencedColumnName="name", nullable=false) + */ + protected $dataset; + + public function __construct(Dataset $dataset) + { + $this->dataset = $dataset; + } + + public function getId(): int { return $this->id; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } - public function getDisplay() + public function getDisplay(): int { return $this->display; } - public function setDisplay($display) + public function setDisplay(int $display): void { $this->display = $display; } @@ -71,8 +84,7 @@ class CriteriaFamily implements \JsonSerializable return [ 'id' => $this->getId(), 'label' => $this->getLabel(), - 'display' => $this->getDisplay(), - 'type' => 'criteria' + 'display' => $this->getDisplay() ]; } } diff --git a/src/Entity/Database.php b/src/Entity/Database.php index 1cf4c5d..5d36199 100644 --- a/src/Entity/Database.php +++ b/src/Entity/Database.php @@ -81,7 +81,7 @@ class Database implements \JsonSerializable * * @return int */ - public function getId() + public function getId(): int { return $this->id; } @@ -91,7 +91,7 @@ class Database implements \JsonSerializable * * @return string */ - public function getLabel() + public function getLabel(): string { return $this->label; } @@ -101,7 +101,7 @@ class Database implements \JsonSerializable * * @param string $label */ - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } @@ -111,7 +111,7 @@ class Database implements \JsonSerializable * * @return string */ - public function getDbName() + public function getDbName(): string { return $this->dbname; } @@ -121,57 +121,57 @@ class Database implements \JsonSerializable * * @param string $dbname */ - public function setDbName($dbname) + public function setDbName(string $dbname): void { $this->dbname = $dbname; } - public function getType() + public function getType(): string { return $this->type; } - public function setType($type) + public function setType(string $type): void { $this->type = $type; } - public function getHost() + public function getHost(): string { return $this->host; } - public function setHost($host) + public function setHost(string $host): void { $this->host = $host; } - public function getPort() + public function getPort(): int { return $this->port; } - public function setPort($port) + public function setPort(int $port): void { $this->port = $port; } - public function getLogin() + public function getLogin(): string { return $this->login; } - public function setLogin($login) + public function setLogin(string $login): void { $this->login = $login; } - public function getPassword() + public function getPassword(): string { return $this->password; } - public function setPassword($password) + public function setPassword(string $password): void { $this->password = $password; } diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index 84f07f2..5d47433 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -92,14 +92,6 @@ class Dataset implements \JsonSerializable */ protected $project; - /** - * @var Anis\Entity\Instance - * - * @ManyToOne(targetEntity="Instance") - * @JoinColumn(name="instance_name", referencedColumnName="name", nullable=false) - */ - protected $instance; - /** * @var Anis\Entity\Project * @@ -121,122 +113,112 @@ class Dataset implements \JsonSerializable $this->attributes = new ArrayCollection(); } - public function getName() + public function getName(): string { return $this->name; } - public function getTableRef() + public function getTableRef(): string { return $this->tableRef; } - public function setTableRef($tableRef) + public function setTableRef(string $tableRef): void { - return $this->tableRef = $tableRef; + $this->tableRef = $tableRef; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } - public function getDescription() + public function getDescription(): string { return $this->description; } - public function setDescription($description) + public function setDescription(string $description): void { $this->description = $description; } - public function getDisplay() + public function getDisplay(): int { return $this->display; } - public function setDisplay($display) + public function setDisplay(int $display): void { - $this->display = (int) $display; + $this->display = $display; } - public function getCount() + public function getCount(): int { return $this->count; } - public function setCount($count) + public function setCount(int $count): void { - $this->count = (int) $count; + $this->count = $count; } - public function getVo() + public function getVo(): bool { return $this->vo; } - public function setVo($vo) + public function setVo(bool $vo): void { - $this->vo = (bool) $vo; + $this->vo = $vo; } - public function getDataPath() + public function getDataPath(): string { return $this->dataPath; } - public function setDataPath($dataPath) + public function setDataPath(string $dataPath): void { $this->dataPath = $dataPath; } - public function getSelectableRow() + public function getSelectableRow(): bool { return $this->selectableRow; } - public function setSelectableRow($selectableRow) + public function setSelectableRow(bool $selectableRow): void { $this->selectableRow = $selectableRow; } - public function getProject() + public function getProject(): Project { return $this->project; } - public function setProject(Project $project) + public function setProject(Project $project): void { $this->project = $project; } - public function getInstance(): Instance - { - return $this->instance; - } - - public function setInstance(Instance $instance): void - { - $this->instance = $instance; - } - - public function getDatasetFamily() + public function getDatasetFamily(): DatasetFamily { return $this->datasetFamily; } - public function setDatasetFamily($datasetFamily) + public function setDatasetFamily(DatasetFamily $datasetFamily): void { $this->datasetFamily = $datasetFamily; } - public function getAttributes() + public function getAttributes(): ArrayCollection { return $this->attributes; } @@ -254,7 +236,6 @@ class Dataset implements \JsonSerializable 'data_path' => $this->getDataPath(), 'selectable_row' => $this->getSelectableRow(), 'project_name' => $this->getProject()->getName(), - 'instance_name' => $this->getInstance()->getName(), 'id_dataset_family' => $this->getDatasetFamily()->getId() ]; } diff --git a/src/Entity/DatasetFamily.php b/src/Entity/DatasetFamily.php index f679255..0736edb 100644 --- a/src/Entity/DatasetFamily.php +++ b/src/Entity/DatasetFamily.php @@ -43,34 +43,43 @@ class DatasetFamily implements \JsonSerializable */ protected $display; - public function __construct() + /** + * @var Anis\Entity\Instance + * + * @ManyToOne(targetEntity="Instance") + * @JoinColumn(name="instance_name", referencedColumnName="name", nullable=false) + */ + protected $instance; + + public function __construct(Instance $instance) { + $this->instance = $instance; $this->datasets = new ArrayCollection(); } - public function getId() + public function getId(): int { return $this->id; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } - public function getDisplay() + public function getDisplay(): int { return $this->display; } - public function setDisplay($display) + public function setDisplay(int $display): void { - $this->display = (int) $display; + $this->display = $display; } public function jsonSerialize() @@ -78,8 +87,7 @@ class DatasetFamily implements \JsonSerializable return [ 'id' => $this->getId(), 'label' => $this->getLabel(), - 'display' => $this->getDisplay(), - 'type' => 'dataset' + 'display' => $this->getDisplay() ]; } } diff --git a/src/Entity/OutputCategory.php b/src/Entity/OutputCategory.php index f848f34..5458879 100644 --- a/src/Entity/OutputCategory.php +++ b/src/Entity/OutputCategory.php @@ -49,37 +49,37 @@ class OutputCategory implements \JsonSerializable */ protected $outputFamily; - public function getId() + public function getId(): int { return $this->id; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } - public function getDisplay() + public function getDisplay(): int { return $this->display; } - public function setDisplay($display) + public function setDisplay(int $display): void { $this->display = $display; } - public function setOutputFamily($outputFamily) + public function setOutputFamily(OutputFamily $outputFamily): void { $this->outputFamily = $outputFamily; } - public function getOutputFamily() + public function getOutputFamily(): OutputFamily { return $this->outputFamily; } diff --git a/src/Entity/OutputFamily.php b/src/Entity/OutputFamily.php index e9ca386..6f54613 100644 --- a/src/Entity/OutputFamily.php +++ b/src/Entity/OutputFamily.php @@ -41,27 +41,40 @@ class OutputFamily implements \JsonSerializable */ protected $display; - public function getId() + /** + * @var Anis\Entity\Dataset + * + * @ManyToOne(targetEntity="Dataset") + * @JoinColumn(name="dataset_name", referencedColumnName="name", nullable=false) + */ + protected $dataset; + + public function __construct(Dataset $dataset) + { + $this->dataset = $dataset; + } + + public function getId(): int { return $this->id; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } - public function setDisplay($display) + public function setDisplay(int $display): void { $this->display = $display; } - public function getDisplay() + public function getDisplay(): int { return $this->display; } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 67e1e09..0151129 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -62,62 +62,62 @@ class Project implements \JsonSerializable */ protected $database; - public function __construct($name) + public function __construct(string $name) { $this->name = $name; } - public function getName() + public function getName(): string { return $this->name; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function setLabel($label) + public function setLabel(string $label): void { $this->label = $label; } - public function getDescription() + public function getDescription(): string { return $this->description; } - public function setDescription($description) + public function setDescription(string $description): void { $this->description = $description; } - public function getLink() + public function getLink(): string { return $this->link; } - public function setLink($link) + public function setLink(string $link): void { $this->link = $link; } - public function getManager() + public function getManager(): string { return $this->manager; } - public function setManager($manager) + public function setManager(string $manager): void { $this->manager = $manager; } - public function setDatabase(Database $database) + public function setDatabase(Database $database): void { $this->database = $database; } - public function getDatabase() + public function getDatabase(): Database { return $this->database; } -- GitLab From 0c1654c78f0d64cff10e04488bc50cdf79c230c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 11:45:33 +0100 Subject: [PATCH 13/31] DatasetFamilyListAction => done --- app/routes.php | 7 +- src/Action/DatasetFamilyListAction.php | 87 +++++++++++++ src/Entity/Dataset.php | 2 +- tests/Action/DatasetFamilyListActionTest.php | 127 +++++++++++++++++++ 4 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 src/Action/DatasetFamilyListAction.php create mode 100644 tests/Action/DatasetFamilyListActionTest.php diff --git a/app/routes.php b/app/routes.php index 0056afa..14afa08 100644 --- a/app/routes.php +++ b/app/routes.php @@ -21,15 +21,16 @@ $app->map([OPTIONS, GET, PUT, DELETE], '/project/{name}', App\Action\ProjectActi $app->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/instance/{name}', App\Action\InstanceAction::class); $app->map([OPTIONS, GET, POST], '/instance/{name}/dataset-family', App\Action\DatasetFamilyListAction::class); +$app->map([OPTIONS, GET], '/instance/{name}/dataset', App\Action\InstanceAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/dataset-family/{id}', App\Action\DatasetFamilyAction::class); $app->map([OPTIONS, GET, POST], '/dataset-family/{id}/dataset', App\Action\DatasetListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class); $app->map([OPTIONS, GET, POST], '/dataset/{name}/criteria-family', App\Action\CriteriaFamilyListAction::class); -$app->map([OPTIONS, GET, POST], '/dataset/{name}/output-family', App\Action\OutputFamilyListAction::class); -$app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class); -$app->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/criteria-family/{id}', App\Action\CriteriaFamilyAction::class); +$app->map([OPTIONS, GET, POST], '/dataset/{name}/output-family', App\Action\OutputFamilyListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/output-family/{id}', App\Action\OutputFamilyAction::class); $app->map([OPTIONS, GET, POST], '/output-family/{id}/output-category', App\Action\OutputCategoryListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class); +$app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class); +$app->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class); $app->get('/search/{dname}', App\Action\SearchAction::class); diff --git a/src/Action/DatasetFamilyListAction.php b/src/Action/DatasetFamilyListAction.php new file mode 100644 index 0000000..ec2a650 --- /dev/null +++ b/src/Action/DatasetFamilyListAction.php @@ -0,0 +1,87 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Instance; +use App\Entity\DatasetFamily; + +final class DatasetFamilyListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all dataset family for a given instance + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + $instance = $this->em->find('App\Entity\Instance', $args['name']); + + // Returns HTTP 404 if the dataset is not found + if (is_null($instance)) { + throw new HttpNotFoundException( + $request, + 'Instance with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $families = $this->em->getRepository('App\Entity\DatasetFamily')->findByInstance($instance); + $payload = json_encode($families); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs information + foreach (array('label', 'display') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new dataset family' + ); + } + } + + $family = $this->postDatasetFamily($parsedBody, $instance); + $payload = json_encode($family); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + private function postDatasetFamily(array $parsedBody, Instance $instance): DatasetFamily + { + $family = new DatasetFamily($instance); + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + + $this->em->persist($family); + $this->em->flush(); + + return $family; + } +} diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index 5d47433..ff6da07 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -96,7 +96,7 @@ class Dataset implements \JsonSerializable * @var Anis\Entity\Project * * @ManyToOne(targetEntity="DatasetFamily") - * @JoinColumn(name="id_dataset_family", referencedColumnName="id", nullable=true) + * @JoinColumn(name="id_dataset_family", referencedColumnName="id", nullable=false) */ protected $datasetFamily; diff --git a/tests/Action/DatasetFamilyListActionTest.php b/tests/Action/DatasetFamilyListActionTest.php new file mode 100644 index 0000000..2a66c4b --- /dev/null +++ b/tests/Action/DatasetFamilyListActionTest.php @@ -0,0 +1,127 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\tests\EntityManagerBuilder; +use App\Entity\Instance; +use App\Entity\DatasetFamily; + +final class DatasetFamilyListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\DatasetFamilyListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testInstanceIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Instance with name default is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'default')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAllDatasetFamiliesOfAnInstance(): void + { + $families = $this->addFamilies(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'default')); + $this->assertSame( + json_encode($families), + (string) $response->getBody() + ); + } + + public function testAddANewDatasetFamilyEmptyLabelField(): void + { + $this->addInstance(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to add a new dataset family'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('name' => 'default')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewDatasetFamily(): void + { + $fields = array( + 'label' => 'Default family', + 'display' => 10 + ); + $this->addInstance(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'default')); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields)), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/instance/default/dataset-family', array( + 'Content-Type' => 'application/json' + )); + } + + private function addInstance(): Instance + { + $instance = new Instance('default', 'Default instance'); + $instance->setClientUrl('http://anis.lam.fr'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addFamilies(): array + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $family2 = new DatasetFamily($instance); + $family2->setLabel('Another family'); + $family2->setDisplay(20); + $this->entityManager->persist($family2); + + $this->entityManager->flush(); + + return array($family, $family2); + } +} -- GitLab From a47d2e48e621b957ba17d446126d89649236792b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 12:06:15 +0100 Subject: [PATCH 14/31] Add DatasetFamilyListAction into dependencies --- app/dependencies.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/dependencies.php b/app/dependencies.php index 4df1ae2..1a357f3 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -91,6 +91,10 @@ $container->set('App\Action\InstanceAction', function (ContainerInterface $c) { return new App\Action\InstanceAction($c->get('em')); }); +$container->set('App\Action\DatasetFamilyListAction', function (ContainerInterface $c) { + return new App\Action\DatasetFamilyListAction($c->get('em')); +}); + $container->set('App\Action\DatasetListAction', function (ContainerInterface $c) { return new App\Action\DatasetListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); }); -- GitLab From cca62613c98bb630956d9ba3dc183ecdf48d2364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 12:36:17 +0100 Subject: [PATCH 15/31] DatasetListByInstanceAction => done --- app/dependencies.php | 4 + app/routes.php | 2 +- src/Action/DatasetFamilyListAction.php | 1 + src/Action/DatasetListByInstanceAction.php | 60 +++++++ .../DatasetListByInstanceActionTest.php | 155 ++++++++++++++++++ 5 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/Action/DatasetListByInstanceAction.php create mode 100644 tests/Action/DatasetListByInstanceActionTest.php diff --git a/app/dependencies.php b/app/dependencies.php index 1a357f3..786e97d 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -95,6 +95,10 @@ $container->set('App\Action\DatasetFamilyListAction', function (ContainerInterfa return new App\Action\DatasetFamilyListAction($c->get('em')); }); +$container->set('App\Action\DatasetListByInstanceAction', function (ContainerInterface $c) { + return new App\Action\DatasetListByInstanceAction($c->get('em')); +}); + $container->set('App\Action\DatasetListAction', function (ContainerInterface $c) { return new App\Action\DatasetListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); }); diff --git a/app/routes.php b/app/routes.php index 14afa08..57a2bed 100644 --- a/app/routes.php +++ b/app/routes.php @@ -21,7 +21,7 @@ $app->map([OPTIONS, GET, PUT, DELETE], '/project/{name}', App\Action\ProjectActi $app->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/instance/{name}', App\Action\InstanceAction::class); $app->map([OPTIONS, GET, POST], '/instance/{name}/dataset-family', App\Action\DatasetFamilyListAction::class); -$app->map([OPTIONS, GET], '/instance/{name}/dataset', App\Action\InstanceAction::class); +$app->map([OPTIONS, GET], '/instance/{name}/dataset', App\Action\DatasetListByInstanceAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/dataset-family/{id}', App\Action\DatasetFamilyAction::class); $app->map([OPTIONS, GET, POST], '/dataset-family/{id}/dataset', App\Action\DatasetListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class); diff --git a/src/Action/DatasetFamilyListAction.php b/src/Action/DatasetFamilyListAction.php index ec2a650..a35af6c 100644 --- a/src/Action/DatasetFamilyListAction.php +++ b/src/Action/DatasetFamilyListAction.php @@ -23,6 +23,7 @@ final class DatasetFamilyListAction extends AbstractAction { /** * `GET` Returns a list of all dataset family for a given instance + * `POST` Add a new dataset family to a given instance * * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request * @param ResponseInterface $response PSR-7 This object represents the HTTP response diff --git a/src/Action/DatasetListByInstanceAction.php b/src/Action/DatasetListByInstanceAction.php new file mode 100644 index 0000000..dc45baf --- /dev/null +++ b/src/Action/DatasetListByInstanceAction.php @@ -0,0 +1,60 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpNotFoundException; + +final class DatasetListByInstanceAction extends AbstractAction +{ + /** + * `GET` Returns a list of all datasets for a given instance + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + } + + $instance = $this->em->find('App\Entity\Instance', $args['name']); + + // Returns HTTP 404 if the dataset is not found + if (is_null($instance)) { + throw new HttpNotFoundException( + $request, + 'Instance with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $qb = $this->em->createQueryBuilder(); + $qb->select('d') + ->from('App\Entity\Dataset', 'd') + ->join('d.datasetFamily', 'f') + ->where($qb->expr()->eq('IDENTITY(f.instance)', ':instanceName')) + ->setParameter('instanceName', $instance->getName()); + $datasets = $qb->getQuery()->getResult(); + $payload = json_encode($datasets); + } + + $response->getBody()->write($payload); + return $response; + } +} diff --git a/tests/Action/DatasetListByInstanceActionTest.php b/tests/Action/DatasetListByInstanceActionTest.php new file mode 100644 index 0000000..041da99 --- /dev/null +++ b/tests/Action/DatasetListByInstanceActionTest.php @@ -0,0 +1,155 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; + +final class DatasetListByInstanceActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\DatasetListByInstanceAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, OPTIONS'); + } + + public function testInstanceIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Instance with name default is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'default')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAllDatasetsForAnInstance(): void + { + $datasets = $this->addDatasets(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'default')); + $this->assertSame( + json_encode($datasets), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/instance/default/dataset', array( + 'Content-Type' => 'application/json' + )); + } + + private function addInstance(): Instance + { + $instance = new Instance('default', 'Default instance'); + $instance->setClientUrl('http://anis.lam.fr'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset family'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + return $family; + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + return $project; + } + + private function addDatasets(): array + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + + $dataset2 = new Dataset('observations'); + $dataset2->setTableRef('v_observations'); + $dataset2->setLabel('Observations label'); + $dataset2->setDescription('Observations description'); + $dataset2->setDisplay(20); + $dataset2->setCount(5000); + $dataset2->setVo(false); + $dataset2->setDataPath('/mnt/observations'); + $dataset2->setSelectableRow(false); + $dataset2->setProject($project); + $dataset2->setDatasetFamily($family); + $this->entityManager->persist($dataset2); + + $this->entityManager->flush(); + + return array($dataset, $dataset2); + } +} -- GitLab From 89dace2747cff9adeacb7ab047a4135bc40a1e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 14:19:35 +0100 Subject: [PATCH 16/31] DatasetFamilyAction => done --- app/dependencies.php | 4 + src/Action/DatasetFamilyAction.php | 80 +++++++++++++++ tests/Action/DatasetFamilyActionTest.php | 124 +++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 src/Action/DatasetFamilyAction.php create mode 100644 tests/Action/DatasetFamilyActionTest.php diff --git a/app/dependencies.php b/app/dependencies.php index 786e97d..53a5bc3 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -99,6 +99,10 @@ $container->set('App\Action\DatasetListByInstanceAction', function (ContainerInt return new App\Action\DatasetListByInstanceAction($c->get('em')); }); +$container->set('App\Action\DatasetFamilyAction', function (ContainerInterface $c) { + return new App\Action\DatasetFamilyAction($c->get('em')); +}); + $container->set('App\Action\DatasetListAction', function (ContainerInterface $c) { return new App\Action\DatasetListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); }); diff --git a/src/Action/DatasetFamilyAction.php b/src/Action/DatasetFamilyAction.php new file mode 100644 index 0000000..c22f483 --- /dev/null +++ b/src/Action/DatasetFamilyAction.php @@ -0,0 +1,80 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\DatasetFamily; + +final class DatasetFamilyAction extends AbstractAction +{ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct dataset family with primary key + $datasetFamily = $this->em->find('App\Entity\DatasetFamily', $args['id']); + + // If dataset family is not found 404 + if (is_null($datasetFamily)) { + throw new HttpNotFoundException( + $request, + 'Dataset family with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($datasetFamily); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + $fields = array('label', 'display'); + foreach ($fields as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the dataset family' + ); + } + } + + $this->editDatasetFamily($datasetFamily, $parsedBody); + $payload = json_encode($datasetFamily); + } + + if ($request->getMethod() === DELETE) { + $id = $datasetFamily->getId(); + $this->em->remove($datasetFamily); + $this->em->flush(); + $payload = json_encode(array( + 'message' => 'Dataset family with id ' . $id . ' is removed!' + )); + } + + $response->getBody()->write($payload); + return $response; + } + + private function editDatasetFamily(DatasetFamily $family, array $parsedBody): void + { + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + $this->em->flush(); + } +} diff --git a/tests/Action/DatasetFamilyActionTest.php b/tests/Action/DatasetFamilyActionTest.php new file mode 100644 index 0000000..6968f4f --- /dev/null +++ b/tests/Action/DatasetFamilyActionTest.php @@ -0,0 +1,124 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Instance; +use App\Entity\DatasetFamily; + +final class DatasetFamilyActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\DatasetFamilyAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testDatasetFamilyIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Dataset family with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetADatasetFamilyById(): void + { + $datasetFamily = $this->addADatasetFamily(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode($datasetFamily), (string) $response->getBody()); + } + + public function testEditADatasetFamilyEmptyLabelField(): void + { + $this->addADatasetFamily(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the dataset family'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditADatasetFamily(): void + { + $fields = array( + 'label' => 'Modfied family', + 'display' => 20 + ); + $this->addADatasetFamily(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode(array_merge(['id' => 1], $fields)), (string) $response->getBody()); + } + + public function testDeleteADatasetFamily(): void + { + $this->addADatasetFamily(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame( + json_encode(array('message' => 'Dataset family with id 1 is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset-family/1', array( + 'Content-Type' => 'application/json' + )); + } + + private function addInstance(): Instance + { + $instance = new Instance('default', 'Default instance'); + $instance->setClientUrl('http://anis.lam.fr'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addADatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $dataset = new DatasetFamily($instance); + $dataset->setLabel('Test1'); + $dataset->setDisplay(10); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } +} -- GitLab From a6c164f21c3d6cddd9ceb80312cec9e86aafbb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 14:51:33 +0100 Subject: [PATCH 17/31] DatasetListAction => done --- src/Action/DatasetListAction.php | 33 +++++++--------------- tests/Action/DatasetListActionTest.php | 39 ++++++++------------------ 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/src/Action/DatasetListAction.php b/src/Action/DatasetListAction.php index 5f5c7a6..4eb365d 100644 --- a/src/Action/DatasetListAction.php +++ b/src/Action/DatasetListAction.php @@ -18,9 +18,7 @@ use Slim\Exception\HttpBadRequestException; use Slim\Exception\HttpNotFoundException; use Doctrine\ORM\EntityManagerInterface; use App\Utils\DBALConnectionFactory; -use App\Entity\Database; use App\Entity\Project; -use App\Entity\Instance; use App\Entity\DatasetFamily; use App\Entity\Dataset; use App\Entity\Attribute; @@ -42,7 +40,7 @@ final class DatasetListAction extends AbstractAction } /** - * `GET` Returns a list of all datasets listed in the metamodel database + * `GET` Returns a list of all datasets for a given dataset family * `POST` Add a new dataset * * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request @@ -57,18 +55,18 @@ final class DatasetListAction extends AbstractAction return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); } - $instance = $this->em->find('App\Entity\Instance', $args['name']); + $datasetFamily = $this->em->find('App\Entity\DatasetFamily', $args['id']); - // Returns HTTP 404 if the instance is not found - if (is_null($instance)) { + // Returns HTTP 404 if the dataset family is not found + if (is_null($datasetFamily)) { throw new HttpNotFoundException( $request, - 'Instance with name ' . $args['name'] . ' is not found' + 'Dataset family with id ' . $args['id'] . ' is not found' ); } if ($request->getMethod() === GET) { - $datasets = $this->em->getRepository('App\Entity\Dataset')->findByInstance($instance); + $datasets = $this->em->getRepository('App\Entity\Dataset')->findByDatasetFamily($datasetFamily); $payload = json_encode($datasets); } @@ -86,8 +84,7 @@ final class DatasetListAction extends AbstractAction 'vo', 'data_path', 'selectable_row', - 'project_name', - 'id_dataset_family' + 'project_name' ); foreach ($fields as $a) { if ($this->isEmptyField($a, $parsedBody)) { @@ -107,15 +104,7 @@ final class DatasetListAction extends AbstractAction ); } - $family = $this->em->find('App\Entity\DatasetFamily', $parsedBody['id_dataset_family']); - if (is_null($family)) { - throw new HttpBadRequestException( - $request, - 'Dataset family with id ' . $parsedBody['id_dataset_family'] . ' is not found' - ); - } - - $dataset = $this->postDataset($parsedBody, $project, $instance, $family); + $dataset = $this->postDataset($parsedBody, $project, $datasetFamily); $payload = json_encode($dataset); $response = $response->withStatus(201); } @@ -136,8 +125,7 @@ final class DatasetListAction extends AbstractAction private function postDataset( array $parsedBody, Project $project, - Instance $instance, - DatasetFamily $family + DatasetFamily $datasetFamily ): Dataset { $dataset = new Dataset($parsedBody['name']); $dataset->setTableRef($parsedBody['table_ref']); @@ -149,8 +137,7 @@ final class DatasetListAction extends AbstractAction $dataset->setDataPath($parsedBody['data_path']); $dataset->setSelectableRow($parsedBody['selectable_row']); $dataset->setProject($project); - $dataset->setInstance($instance); - $dataset->setDatasetFamily($family); + $dataset->setDatasetFamily($datasetFamily); $this->em->persist($dataset); $this->postAttributes($dataset); diff --git a/tests/Action/DatasetListActionTest.php b/tests/Action/DatasetListActionTest.php index 9be0dce..2d481b2 100644 --- a/tests/Action/DatasetListActionTest.php +++ b/tests/Action/DatasetListActionTest.php @@ -47,12 +47,12 @@ final class DatasetListActionTest extends TestCase $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); } - public function testInstanceNotFound(): void + public function testDatasetFamilyNotFound(): void { $this->expectException(HttpNotFoundException::class); - $this->expectExceptionMessage('Instance with name aspic is not found'); + $this->expectExceptionMessage('Dataset family with id 1 is not found'); $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertEquals(404, (int) $response->getStatusCode()); } @@ -60,7 +60,7 @@ final class DatasetListActionTest extends TestCase { $datasets = $this->addDatasets(); $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertSame( json_encode($datasets), (string) $response->getBody() @@ -69,45 +69,32 @@ final class DatasetListActionTest extends TestCase public function testAddANewDatasetEmptyNameField(): void { - $this->addInstance(); + $this->addDatasetFamily(); $this->expectException(HttpBadRequestException::class); $this->expectExceptionMessage('Param name needed to add a new dataset'); $request = $this->getRequest('POST')->withParsedBody(array()); - $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertEquals(400, (int) $response->getStatusCode()); } public function testAddANewDatasetProjectNotFound(): void { - $this->addInstance(); + $this->addDatasetFamily(); $this->expectException(HttpBadRequestException::class); $this->expectExceptionMessage('Project with name anis_project is not found'); $fields = $this->getNewDatasetFields(); $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array('name' => 'aspic')); - $this->assertEquals(400, (int) $response->getStatusCode()); - } - - public function testAddANewDatasetFamilyNotFound(): void - { - $this->addProject(); - $this->addInstance(); - $this->expectException(HttpBadRequestException::class); - $this->expectExceptionMessage('Dataset family with id 1 is not found'); - $fields = $this->getNewDatasetFields(); - $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertEquals(400, (int) $response->getStatusCode()); } public function testAddANewDataset(): void { $this->addProject(); - $this->addInstance(); $this->addDatasetFamily(); $fields = $this->getNewDatasetFields(); $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array('name' => 'aspic')); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertSame( json_encode($fields), (string) $response->getBody() @@ -140,7 +127,6 @@ final class DatasetListActionTest extends TestCase 'data_path' => '/mnt/dataset1', 'selectable_row' => false, 'project_name' => 'anis_project', - 'instance_name' => 'aspic', 'id_dataset_family' => 1 ); } @@ -180,7 +166,9 @@ final class DatasetListActionTest extends TestCase private function addDatasetFamily(): DatasetFamily { - $family = new DatasetFamily(); + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); $family->setLabel('Default dataset'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -192,7 +180,6 @@ final class DatasetListActionTest extends TestCase private function addDatasets(): array { $project = $this->addProject(); - $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset1 = new Dataset('dataset1'); @@ -205,7 +192,6 @@ final class DatasetListActionTest extends TestCase $dataset1->setDataPath('/mnt/dataset1'); $dataset1->setSelectableRow(false); $dataset1->setProject($project); - $dataset1->setInstance($instance); $dataset1->setDatasetFamily($family); $this->entityManager->persist($dataset1); @@ -219,7 +205,6 @@ final class DatasetListActionTest extends TestCase $dataset2->setDataPath('/mnt/dataset2'); $dataset2->setSelectableRow(false); $dataset2->setProject($project); - $dataset2->setInstance($instance); $dataset2->setDatasetFamily($family); $this->entityManager->persist($dataset2); -- GitLab From e047459c8971dbad478497541866e258dab4c96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 14:56:31 +0100 Subject: [PATCH 18/31] DatasetAction => done --- tests/Action/DatasetActionTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Action/DatasetActionTest.php b/tests/Action/DatasetActionTest.php index 15e3ec3..f8877bc 100644 --- a/tests/Action/DatasetActionTest.php +++ b/tests/Action/DatasetActionTest.php @@ -93,7 +93,7 @@ final class DatasetActionTest extends TestCase array_merge( ['name' => 'obs_cat', 'table_ref' => 'v_obs_cat'], $fields, - ['project_name' => 'anis_project', 'instance_name' => 'aspic', 'id_dataset_family' => 1] + ['project_name' => 'anis_project', 'id_dataset_family' => 1] ), JSON_UNESCAPED_SLASHES ), @@ -174,7 +174,9 @@ final class DatasetActionTest extends TestCase private function addDatasetFamily(): DatasetFamily { - $family = new DatasetFamily(); + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); $family->setLabel('Default dataset'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -186,7 +188,6 @@ final class DatasetActionTest extends TestCase private function addADataset(): Dataset { $project = $this->addProject(); - $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset = new Dataset('obs_cat'); @@ -199,7 +200,6 @@ final class DatasetActionTest extends TestCase $dataset->setDataPath('/mnt/obs_cat'); $dataset->setSelectableRow(false); $dataset->setProject($project); - $dataset->setInstance($instance); $dataset->setDatasetFamily($family); $this->entityManager->persist($dataset); $this->entityManager->flush(); -- GitLab From 3bb91acc2176a27c2bf0f3a3f17a1ee2fba9fd6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 15:14:30 +0100 Subject: [PATCH 19/31] CriteriaFamilyListAction => done --- src/Action/CriteriaFamilyListAction.php | 88 ++++++++ tests/Action/CriteriaFamilyListActionTest.php | 189 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 src/Action/CriteriaFamilyListAction.php create mode 100644 tests/Action/CriteriaFamilyListActionTest.php diff --git a/src/Action/CriteriaFamilyListAction.php b/src/Action/CriteriaFamilyListAction.php new file mode 100644 index 0000000..f9ea236 --- /dev/null +++ b/src/Action/CriteriaFamilyListAction.php @@ -0,0 +1,88 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Dataset; +use App\Entity\CriteriaFamily; + +final class CriteriaFamilyListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all criteria family for a given dataset + * `POST` Add a new dataset criteria family to a given dataset + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + $dataset = $this->em->find('App\Entity\Dataset', $args['name']); + + // Returns HTTP 404 if the dataset is not found + if (is_null($dataset)) { + throw new HttpNotFoundException( + $request, + 'Dataset with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $families = $this->em->getRepository('App\Entity\CriteriaFamily')->findByDataset($dataset); + $payload = json_encode($families); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs information + foreach (array('label', 'display') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new criteria family' + ); + } + } + + $family = $this->postCriteriaFamily($parsedBody, $dataset); + $payload = json_encode($family); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + private function postCriteriaFamily(array $parsedBody, Dataset $dataset): CriteriaFamily + { + $family = new CriteriaFamily($dataset); + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + + $this->em->persist($family); + $this->em->flush(); + + return $family; + } +} diff --git a/tests/Action/CriteriaFamilyListActionTest.php b/tests/Action/CriteriaFamilyListActionTest.php new file mode 100644 index 0000000..d42ebf1 --- /dev/null +++ b/tests/Action/CriteriaFamilyListActionTest.php @@ -0,0 +1,189 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; +use App\Entity\CriteriaFamily; + +final class CriteriaFamilyListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\CriteriaFamilyListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testDatasetIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Dataset with name obs_cat is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAllCriteriaFamiliesOfADataset(): void + { + $families = $this->addFamilies(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame( + json_encode($families), + (string) $response->getBody() + ); + } + + public function testAddANewCriteriaFamilyEmptyLabelField(): void + { + $this->addADataset(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to add a new criteria family'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewCriteriaFamily(): void + { + $fields = array( + 'label' => 'Default criteria family', + 'display' => 10 + ); + $this->addADataset(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields)), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset/obs_cat/criteria-family', array( + 'Content-Type' => 'application/json' + )); + } + + private function addFamilies(): array + { + $dataset = $this->addADataset(); + + $family = new CriteriaFamily($dataset); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $family2 = new CriteriaFamily($dataset); + $family2->setLabel('Another family'); + $family2->setDisplay(20); + $this->entityManager->persist($family2); + + $this->entityManager->flush(); + + return array($family, $family2); + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } +} -- GitLab From 4841581471bf9fd6bfdb0f586c394e358eb31c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 15:24:07 +0100 Subject: [PATCH 20/31] CriteriaFamilyAction => done --- app/dependencies.php | 8 + src/Action/CriteriaFamilyAction.php | 80 +++++++++ tests/Action/CriteriaFamilyActionTest.php | 188 ++++++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 src/Action/CriteriaFamilyAction.php create mode 100644 tests/Action/CriteriaFamilyActionTest.php diff --git a/app/dependencies.php b/app/dependencies.php index 53a5bc3..37c47ad 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -111,6 +111,14 @@ $container->set('App\Action\DatasetAction', function (ContainerInterface $c) { return new App\Action\DatasetAction($c->get('em'), new App\Utils\DBALConnectionFactory()); }); +$container->set('App\Action\CriteriaFamilyListAction', function (ContainerInterface $c) { + return new App\Action\CriteriaFamilyListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); +}); + +$container->set('App\Action\CriteriaFamilyAction', function (ContainerInterface $c) { + return new App\Action\CriteriaFamilyAction($c->get('em'), new App\Utils\DBALConnectionFactory()); +}); + $container->set('App\Action\AttributeListAction', function (ContainerInterface $c) { return new App\Action\AttributeListAction($c->get('em')); }); diff --git a/src/Action/CriteriaFamilyAction.php b/src/Action/CriteriaFamilyAction.php new file mode 100644 index 0000000..c10c088 --- /dev/null +++ b/src/Action/CriteriaFamilyAction.php @@ -0,0 +1,80 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\CriteriaFamily; + +final class CriteriaFamilyAction extends AbstractAction +{ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct criteria family with primary key + $criteriaFamily = $this->em->find('App\Entity\CriteriaFamily', $args['id']); + + // If criteria family is not found 404 + if (is_null($criteriaFamily)) { + throw new HttpNotFoundException( + $request, + 'Criteria family with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($criteriaFamily); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + $fields = array('label', 'display'); + foreach ($fields as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the criteria family' + ); + } + } + + $this->editCriteriaFamily($criteriaFamily, $parsedBody); + $payload = json_encode($criteriaFamily); + } + + if ($request->getMethod() === DELETE) { + $id = $criteriaFamily->getId(); + $this->em->remove($criteriaFamily); + $this->em->flush(); + $payload = json_encode(array( + 'message' => 'Criteria family with id ' . $id . ' is removed!' + )); + } + + $response->getBody()->write($payload); + return $response; + } + + private function editCriteriaFamily(CriteriaFamily $family, array $parsedBody): void + { + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + $this->em->flush(); + } +} diff --git a/tests/Action/CriteriaFamilyActionTest.php b/tests/Action/CriteriaFamilyActionTest.php new file mode 100644 index 0000000..0575261 --- /dev/null +++ b/tests/Action/CriteriaFamilyActionTest.php @@ -0,0 +1,188 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; +use App\Entity\CriteriaFamily; + +final class CriteriaFamilyActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\CriteriaFamilyAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testCriteriaFamilyIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Criteria family with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetACriteriaFamilyById(): void + { + $criteriaFamily = $this->addACriteriaFamily(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode($criteriaFamily), (string) $response->getBody()); + } + + public function testEditACriteriaFamilyEmptyLabelField(): void + { + $this->addACriteriaFamily(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the criteria family'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditACriteriaFamily(): void + { + $fields = array( + 'label' => 'Modfied family', + 'display' => 20 + ); + $this->addACriteriaFamily(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode(array_merge(['id' => 1], $fields)), (string) $response->getBody()); + } + + public function testDeleteACriteriaFamily(): void + { + $this->addACriteriaFamily(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame( + json_encode(array('message' => 'Criteria family with id 1 is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/criteria-family/1', array( + 'Content-Type' => 'application/json' + )); + } + + private function addACriteriaFamily(): CriteriaFamily + { + $dataset = $this->addADataset(); + + $family = new CriteriaFamily($dataset); + $family->setLabel('Default criteria'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + + return $family; + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } +} -- GitLab From 824612460f1eb990d8866623780c1158237646c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 15:30:39 +0100 Subject: [PATCH 21/31] OutputFamilyListAction => done --- app/dependencies.php | 4 + src/Action/OutputFamilyListAction.php | 88 +++++++++ src/Entity/OutputFamily.php | 3 +- tests/Action/OutputFamilyListActionTest.php | 189 ++++++++++++++++++++ 4 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 src/Action/OutputFamilyListAction.php create mode 100644 tests/Action/OutputFamilyListActionTest.php diff --git a/app/dependencies.php b/app/dependencies.php index 37c47ad..7f3f4ac 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -119,6 +119,10 @@ $container->set('App\Action\CriteriaFamilyAction', function (ContainerInterface return new App\Action\CriteriaFamilyAction($c->get('em'), new App\Utils\DBALConnectionFactory()); }); +$container->set('App\Action\OutputFamilyListAction', function (ContainerInterface $c) { + return new App\Action\OutputFamilyListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); +}); + $container->set('App\Action\AttributeListAction', function (ContainerInterface $c) { return new App\Action\AttributeListAction($c->get('em')); }); diff --git a/src/Action/OutputFamilyListAction.php b/src/Action/OutputFamilyListAction.php new file mode 100644 index 0000000..16a4e82 --- /dev/null +++ b/src/Action/OutputFamilyListAction.php @@ -0,0 +1,88 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Dataset; +use App\Entity\OutputFamily; + +final class OutputFamilyListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all output family for a given dataset + * `POST` Add a new dataset output family to a given dataset + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + $dataset = $this->em->find('App\Entity\Dataset', $args['name']); + + // Returns HTTP 404 if the dataset is not found + if (is_null($dataset)) { + throw new HttpNotFoundException( + $request, + 'Dataset with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $families = $this->em->getRepository('App\Entity\OutputFamily')->findByDataset($dataset); + $payload = json_encode($families); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs information + foreach (array('label', 'display') as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new output family' + ); + } + } + + $family = $this->postOutputFamily($parsedBody, $dataset); + $payload = json_encode($family); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + private function postOutputFamily(array $parsedBody, Dataset $dataset): OutputFamily + { + $family = new OutputFamily($dataset); + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + + $this->em->persist($family); + $this->em->flush(); + + return $family; + } +} diff --git a/src/Entity/OutputFamily.php b/src/Entity/OutputFamily.php index 6f54613..e09e809 100644 --- a/src/Entity/OutputFamily.php +++ b/src/Entity/OutputFamily.php @@ -84,8 +84,7 @@ class OutputFamily implements \JsonSerializable return [ 'id' => $this->getId(), 'label' => $this->getLabel(), - 'display' => $this->getDisplay(), - 'type' => 'output' + 'display' => $this->getDisplay() ]; } } diff --git a/tests/Action/OutputFamilyListActionTest.php b/tests/Action/OutputFamilyListActionTest.php new file mode 100644 index 0000000..e41648a --- /dev/null +++ b/tests/Action/OutputFamilyListActionTest.php @@ -0,0 +1,189 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; +use App\Entity\OutputFamily; + +final class OutputFamilyListActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\OutputFamilyListAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); + } + + public function testDatasetIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Dataset with name obs_cat is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAllOutputFamiliesOfADataset(): void + { + $families = $this->addFamilies(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame( + json_encode($families), + (string) $response->getBody() + ); + } + + public function testAddANewOutputFamilyEmptyLabelField(): void + { + $this->addADataset(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to add a new output family'); + $request = $this->getRequest('POST')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testAddANewOutputFamily(): void + { + $fields = array( + 'label' => 'Default output family', + 'display' => 10 + ); + $this->addADataset(); + $request = $this->getRequest('POST')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame( + json_encode(array_merge(['id' => 1], $fields)), + (string) $response->getBody() + ); + $this->assertEquals(201, (int) $response->getStatusCode()); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset/obs_cat/output-family', array( + 'Content-Type' => 'application/json' + )); + } + + private function addFamilies(): array + { + $dataset = $this->addADataset(); + + $family = new OutputFamily($dataset); + $family->setLabel('Default output'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $family2 = new OutputFamily($dataset); + $family2->setLabel('Another family'); + $family2->setDisplay(20); + $this->entityManager->persist($family2); + + $this->entityManager->flush(); + + return array($family, $family2); + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } +} -- GitLab From e0967e192d46522d05f715d205938fc5a6623d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 15:37:17 +0100 Subject: [PATCH 22/31] AttributeListAction => done --- src/Entity/Attribute.php | 44 ++++++++++++------------ tests/Action/AttributeListActionTest.php | 6 ++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Entity/Attribute.php b/src/Entity/Attribute.php index 4bcfa35..3ca9f4f 100644 --- a/src/Entity/Attribute.php +++ b/src/Entity/Attribute.php @@ -314,7 +314,7 @@ class Attribute implements \JsonSerializable $this->formLabel = $formLabel; } - public function getDescription(): string + public function getDescription() { return $this->description; } @@ -344,7 +344,7 @@ class Attribute implements \JsonSerializable return $this->criteriaDisplay; } - public function getSearchFlag(): string + public function getSearchFlag() { return $this->searchFlag; } @@ -354,7 +354,7 @@ class Attribute implements \JsonSerializable $this->searchFlag = $searchFlag; } - public function getSearchType(): string + public function getSearchType() { return $this->searchType; } @@ -364,7 +364,7 @@ class Attribute implements \JsonSerializable $this->searchType = $searchType; } - public function getOperator(): string + public function getOperator() { return $this->operator; } @@ -384,7 +384,7 @@ class Attribute implements \JsonSerializable $this->type = $type; } - public function getMin(): string + public function getMin() { return $this->min; } @@ -394,7 +394,7 @@ class Attribute implements \JsonSerializable $this->min = $min; } - public function getMax(): string + public function getMax() { return $this->max; } @@ -404,7 +404,7 @@ class Attribute implements \JsonSerializable $this->max = $max; } - public function getPlaceholderMin(): string + public function getPlaceholderMin() { return $this->placeholderMin; } @@ -414,7 +414,7 @@ class Attribute implements \JsonSerializable $this->placeholderMin = $placeholderMin; } - public function getPlaceholderMax(): string + public function getPlaceholderMax() { return $this->placeholderMax; } @@ -424,7 +424,7 @@ class Attribute implements \JsonSerializable $this->placeholderMax = $placeholderMax; } - public function getUriAction(): string + public function getUriAction() { return $this->uriAction; } @@ -434,7 +434,7 @@ class Attribute implements \JsonSerializable $this->uriAction = $uriAction; } - public function getRenderer(): string + public function getRenderer() { return $this->renderer; } @@ -454,7 +454,7 @@ class Attribute implements \JsonSerializable $this->displayDetail = $displayDetail; } - public function getVoUtype(): string + public function getVoUtype() { return $this->voUtype; } @@ -464,7 +464,7 @@ class Attribute implements \JsonSerializable $this->voUtype = $voUtype; } - public function getVoUcd(): string + public function getVoUcd() { return $this->voUcd; } @@ -474,7 +474,7 @@ class Attribute implements \JsonSerializable $this->voUcd = $voUcd; } - public function getVoUnit(): string + public function getVoUnit() { return $this->voUnit; } @@ -484,7 +484,7 @@ class Attribute implements \JsonSerializable $this->voUnit = $voUnit; } - public function getVoDescription(): string + public function getVoDescription() { return $this->voDescription; } @@ -494,7 +494,7 @@ class Attribute implements \JsonSerializable $this->voDescription = $voDescription; } - public function getVoDatatype(): string + public function getVoDatatype() { return $this->voDatatype; } @@ -504,7 +504,7 @@ class Attribute implements \JsonSerializable $this->voDatatype = $voDatatype; } - public function getVoSize(): int + public function getVoSize() { return $this->voSize; } @@ -524,7 +524,7 @@ class Attribute implements \JsonSerializable $this->selected = $selected; } - public function getOrderBy(): bool + public function getOrderBy() { return $this->orderBy; } @@ -544,7 +544,7 @@ class Attribute implements \JsonSerializable $this->orderDisplay = $orderDisplay; } - public function getDetail(): bool + public function getDetail() { return $this->detail; } @@ -554,7 +554,7 @@ class Attribute implements \JsonSerializable $this->detail = $detail; } - public function getRendererDetail(): string + public function getRendererDetail() { return $this->rendererDetail; } @@ -564,7 +564,7 @@ class Attribute implements \JsonSerializable $this->rendererDetail = $rendererDetail; } - public function getOptions(): string + public function getOptions() { return $this->options; } @@ -574,7 +574,7 @@ class Attribute implements \JsonSerializable $this->options = $options; } - public function getCriteriaFamily(): CriteriaFamily + public function getCriteriaFamily() { return $this->criteriaFamily; } @@ -584,7 +584,7 @@ class Attribute implements \JsonSerializable $this->criteriaFamily = $criteriaFamily; } - public function getOutputCategory(): OutputCategory + public function getOutputCategory() { return $this->outputCategory; } diff --git a/tests/Action/AttributeListActionTest.php b/tests/Action/AttributeListActionTest.php index 295327c..aee4113 100644 --- a/tests/Action/AttributeListActionTest.php +++ b/tests/Action/AttributeListActionTest.php @@ -109,7 +109,9 @@ final class AttributeListActionTest extends TestCase private function addDatasetFamily(): DatasetFamily { - $family = new DatasetFamily(); + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); $family->setLabel('Default dataset'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -120,7 +122,6 @@ final class AttributeListActionTest extends TestCase private function addADataset(): Dataset { $project = $this->addProject(); - $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset = new Dataset('obs_cat'); @@ -133,7 +134,6 @@ final class AttributeListActionTest extends TestCase $dataset->setDataPath('/mnt/obs_cat'); $dataset->setSelectableRow(false); $dataset->setProject($project); - $dataset->setInstance($instance); $dataset->setDatasetFamily($family); $this->entityManager->persist($dataset); $this->entityManager->flush(); -- GitLab From 83ba9e12a72212e431d40849803668812ee96599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 15:55:36 +0100 Subject: [PATCH 23/31] AttributeAction => done --- src/Entity/Attribute.php | 86 ++++++++++++++-------------- tests/Action/AttributeActionTest.php | 23 ++++---- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/Entity/Attribute.php b/src/Entity/Attribute.php index 3ca9f4f..9a2a18d 100644 --- a/src/Entity/Attribute.php +++ b/src/Entity/Attribute.php @@ -269,47 +269,47 @@ class Attribute implements \JsonSerializable $this->dataset = $dataset; } - public function getId(): int + public function getId() { return $this->id; } - public function getName(): string + public function getName() { return $this->name; } - public function setName(string $name): void + public function setName($name) { $this->name = $name; } - public function getTableName(): string + public function getTableName() { return $this->tableName; } - public function setTableName(string $tableName): void + public function setTableName($tableName) { $this->tableName = $tableName; } - public function getLabel(): string + public function getLabel() { return $this->label; } - public function setLabel(string $label): void + public function setLabel($label) { $this->label = $label; } - public function getFormLabel(): string + public function getFormLabel() { return $this->formLabel; } - public function setFormLabel(string $formLabel): void + public function setFormLabel($formLabel) { $this->formLabel = $formLabel; } @@ -319,27 +319,27 @@ class Attribute implements \JsonSerializable return $this->description; } - public function setDescription(string $description): void + public function setDescription($description) { $this->description = $description; } - public function getOutputDisplay(): int + public function getOutputDisplay() { return $this->outputDisplay; } - public function setOutputDisplay(int $outputDisplay): void + public function setOutputDisplay($outputDisplay) { $this->outputDisplay = $outputDisplay; } - public function setCriteriaDisplay(int $criteriaDisplay): void + public function setCriteriaDisplay($criteriaDisplay) { $this->criteriaDisplay = $criteriaDisplay; } - public function getCriteriaDisplay(): int + public function getCriteriaDisplay() { return $this->criteriaDisplay; } @@ -349,7 +349,7 @@ class Attribute implements \JsonSerializable return $this->searchFlag; } - public function setSearchFlag(string $searchFlag): void + public function setSearchFlag($searchFlag) { $this->searchFlag = $searchFlag; } @@ -359,7 +359,7 @@ class Attribute implements \JsonSerializable return $this->searchType; } - public function setSearchType(string $searchType): void + public function setSearchType($searchType) { $this->searchType = $searchType; } @@ -369,17 +369,17 @@ class Attribute implements \JsonSerializable return $this->operator; } - public function setOperator(string $operator): void + public function setOperator($operator) { $this->operator = $operator; } - public function getType(): string + public function getType() { return $this->type; } - public function setType(string $type): void + public function setType($type) { $this->type = $type; } @@ -389,7 +389,7 @@ class Attribute implements \JsonSerializable return $this->min; } - public function setMin(string $min): void + public function setMin($min) { $this->min = $min; } @@ -399,7 +399,7 @@ class Attribute implements \JsonSerializable return $this->max; } - public function setMax(string $max): void + public function setMax($max) { $this->max = $max; } @@ -409,7 +409,7 @@ class Attribute implements \JsonSerializable return $this->placeholderMin; } - public function setPlaceholderMin(string $placeholderMin): void + public function setPlaceholderMin($placeholderMin) { $this->placeholderMin = $placeholderMin; } @@ -419,7 +419,7 @@ class Attribute implements \JsonSerializable return $this->placeholderMax; } - public function setPlaceholderMax(string $placeholderMax): void + public function setPlaceholderMax($placeholderMax) { $this->placeholderMax = $placeholderMax; } @@ -429,7 +429,7 @@ class Attribute implements \JsonSerializable return $this->uriAction; } - public function setUriAction(string $uriAction): void + public function setUriAction($uriAction) { $this->uriAction = $uriAction; } @@ -439,17 +439,17 @@ class Attribute implements \JsonSerializable return $this->renderer; } - public function setRenderer(string $renderer): void + public function setRenderer($renderer) { $this->renderer = $renderer; } - public function getDisplayDetail(): int + public function getDisplayDetail() { return $this->displayDetail; } - public function setDisplayDetail(int $displayDetail): void + public function setDisplayDetail($displayDetail) { $this->displayDetail = $displayDetail; } @@ -459,7 +459,7 @@ class Attribute implements \JsonSerializable return $this->voUtype; } - public function setVoUtype(string $voUtype): void + public function setVoUtype($voUtype) { $this->voUtype = $voUtype; } @@ -469,7 +469,7 @@ class Attribute implements \JsonSerializable return $this->voUcd; } - public function setVoUcd(string $voUcd): void + public function setVoUcd($voUcd) { $this->voUcd = $voUcd; } @@ -479,7 +479,7 @@ class Attribute implements \JsonSerializable return $this->voUnit; } - public function setVoUnit(string $voUnit): void + public function setVoUnit($voUnit) { $this->voUnit = $voUnit; } @@ -489,7 +489,7 @@ class Attribute implements \JsonSerializable return $this->voDescription; } - public function setVoDescription(string $voDescription): void + public function setVoDescription($voDescription) { $this->voDescription = $voDescription; } @@ -499,7 +499,7 @@ class Attribute implements \JsonSerializable return $this->voDatatype; } - public function setVoDatatype(string $voDatatype): void + public function setVoDatatype($voDatatype) { $this->voDatatype = $voDatatype; } @@ -509,17 +509,17 @@ class Attribute implements \JsonSerializable return $this->voSize; } - public function setVoSize(int $voSize): void + public function setVoSize($voSize) { $this->voSize = $voSize; } - public function getSelected(): bool + public function getSelected() { return $this->selected; } - public function setSelected(bool $selected): void + public function setSelected($selected) { $this->selected = $selected; } @@ -529,17 +529,17 @@ class Attribute implements \JsonSerializable return $this->orderBy; } - public function setOrderBy(bool $orderBy): void + public function setOrderBy($orderBy) { $this->orderBy = $orderBy; } - public function getOrderDisplay(): int + public function getOrderDisplay() { return $this->orderDisplay; } - public function setOrderDisplay(int $orderDisplay): void + public function setOrderDisplay($orderDisplay) { $this->orderDisplay = $orderDisplay; } @@ -549,7 +549,7 @@ class Attribute implements \JsonSerializable return $this->detail; } - public function setDetail(bool $detail): void + public function setDetail($detail) { $this->detail = $detail; } @@ -559,7 +559,7 @@ class Attribute implements \JsonSerializable return $this->rendererDetail; } - public function setRendererDetail(string $rendererDetail): void + public function setRendererDetail($rendererDetail) { $this->rendererDetail = $rendererDetail; } @@ -569,7 +569,7 @@ class Attribute implements \JsonSerializable return $this->options; } - public function setOptions(string $options): void + public function setOptions($options) { $this->options = $options; } @@ -579,7 +579,7 @@ class Attribute implements \JsonSerializable return $this->criteriaFamily; } - public function setCriteriaFamily(CriteriaFamily $criteriaFamily): void + public function setCriteriaFamily($criteriaFamily) { $this->criteriaFamily = $criteriaFamily; } @@ -589,7 +589,7 @@ class Attribute implements \JsonSerializable return $this->outputCategory; } - public function setOutputCategory(OutputCategory $outputCategory): void + public function setOutputCategory($outputCategory) { $this->outputCategory = $outputCategory; } diff --git a/tests/Action/AttributeActionTest.php b/tests/Action/AttributeActionTest.php index 97d423d..97708d6 100644 --- a/tests/Action/AttributeActionTest.php +++ b/tests/Action/AttributeActionTest.php @@ -71,8 +71,6 @@ final class AttributeActionTest extends TestCase public function testEditADatabase(): void { $this->addAnAttribute(); - $this->addCriteriaFamily(); - $this->addOutputCategory(); $fields = $this->getEditAttributeFields(); $request = $this->getRequest('PUT')->withParsedBody($fields); $response = ($this->action)($request, new Response(), array('name' => 'obs_cat', 'id' => 1)); @@ -173,7 +171,9 @@ final class AttributeActionTest extends TestCase private function addDatasetFamily(): DatasetFamily { - $family = new DatasetFamily(); + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); $family->setLabel('Default dataset'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -181,9 +181,9 @@ final class AttributeActionTest extends TestCase return $family; } - private function addCriteriaFamily(): CriteriaFamily + private function addCriteriaFamily(Dataset $dataset): CriteriaFamily { - $family = new CriteriaFamily(); + $family = new CriteriaFamily($dataset); $family->setLabel('Default criteria'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -192,9 +192,9 @@ final class AttributeActionTest extends TestCase return $family; } - private function addOutputCategory(): OutputCategory + private function addOutputCategory(Dataset $dataset): OutputCategory { - $outputFamily = $this->addOutputFamily(); + $outputFamily = $this->addOutputFamily($dataset); $outputCategory = new OutputCategory(); $outputCategory->setLabel('Default output category'); @@ -205,9 +205,9 @@ final class AttributeActionTest extends TestCase return $outputCategory; } - private function addOutputFamily(): OutputFamily + private function addOutputFamily(Dataset $dataset): OutputFamily { - $family = new OutputFamily(); + $family = new OutputFamily($dataset); $family->setLabel('Default output'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -218,7 +218,6 @@ final class AttributeActionTest extends TestCase private function addADataset(): Dataset { $project = $this->addProject(); - $instance = $this->addInstance(); $family = $this->addDatasetFamily(); $dataset = new Dataset('obs_cat'); @@ -231,7 +230,6 @@ final class AttributeActionTest extends TestCase $dataset->setDataPath('/mnt/obs_cat'); $dataset->setSelectableRow(false); $dataset->setProject($project); - $dataset->setInstance($instance); $dataset->setDatasetFamily($family); $this->entityManager->persist($dataset); $this->entityManager->flush(); @@ -243,6 +241,9 @@ final class AttributeActionTest extends TestCase { $dataset = $this->addADataset(); + $this->addCriteriaFamily($dataset); + $this->addOutputCategory($dataset); + $attribute = new Attribute(1, $dataset); $attribute->setName('id'); $attribute->setTableName($dataset->getTableRef()); -- GitLab From 7635354dab83658ac8697eabea7c4c024c82be37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 16:10:56 +0100 Subject: [PATCH 24/31] OutputFamilyAction => done --- app/dependencies.php | 20 ++- src/Action/OutputFamilyAction.php | 80 ++++++++++ tests/Action/OutputFamilyActionTest.php | 188 ++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 src/Action/OutputFamilyAction.php create mode 100644 tests/Action/OutputFamilyActionTest.php diff --git a/app/dependencies.php b/app/dependencies.php index 7f3f4ac..f81c995 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -75,14 +75,6 @@ $container->set('App\Action\FamilyAction', function (ContainerInterface $c) { return new App\Action\FamilyAction($c->get('em')); }); -$container->set('App\Action\OutputCategoryListAction', function (ContainerInterface $c) { - return new App\Action\OutputCategoryListAction($c->get('em')); -}); - -$container->set('App\Action\OutputCategoryAction', function (ContainerInterface $c) { - return new App\Action\OutputCategoryAction($c->get('em')); -}); - $container->set('App\Action\InstanceListAction', function (ContainerInterface $c) { return new App\Action\InstanceListAction($c->get('em')); }); @@ -123,6 +115,18 @@ $container->set('App\Action\OutputFamilyListAction', function (ContainerInterfac return new App\Action\OutputFamilyListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); }); +$container->set('App\Action\OutputFamilyAction', function (ContainerInterface $c) { + return new App\Action\OutputFamilyAction($c->get('em'), new App\Utils\DBALConnectionFactory()); +}); + +$container->set('App\Action\OutputCategoryListAction', function (ContainerInterface $c) { + return new App\Action\OutputCategoryListAction($c->get('em')); +}); + +$container->set('App\Action\OutputCategoryAction', function (ContainerInterface $c) { + return new App\Action\OutputCategoryAction($c->get('em')); +}); + $container->set('App\Action\AttributeListAction', function (ContainerInterface $c) { return new App\Action\AttributeListAction($c->get('em')); }); diff --git a/src/Action/OutputFamilyAction.php b/src/Action/OutputFamilyAction.php new file mode 100644 index 0000000..f6c12d2 --- /dev/null +++ b/src/Action/OutputFamilyAction.php @@ -0,0 +1,80 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\OutputFamily; + +final class OutputFamilyAction extends AbstractAction +{ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct output family with primary key + $outputFamily = $this->em->find('App\Entity\OutputFamily', $args['id']); + + // If output family is not found 404 + if (is_null($outputFamily)) { + throw new HttpNotFoundException( + $request, + 'Output family with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($outputFamily); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + $fields = array('label', 'display'); + foreach ($fields as $a) { + if ($this->isEmptyField($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the output family' + ); + } + } + + $this->editOutputFamily($outputFamily, $parsedBody); + $payload = json_encode($outputFamily); + } + + if ($request->getMethod() === DELETE) { + $id = $outputFamily->getId(); + $this->em->remove($outputFamily); + $this->em->flush(); + $payload = json_encode(array( + 'message' => 'Output family with id ' . $id . ' is removed!' + )); + } + + $response->getBody()->write($payload); + return $response; + } + + private function editOutputFamily(OutputFamily $family, array $parsedBody): void + { + $family->setLabel($parsedBody['label']); + $family->setDisplay($parsedBody['display']); + $this->em->flush(); + } +} diff --git a/tests/Action/OutputFamilyActionTest.php b/tests/Action/OutputFamilyActionTest.php new file mode 100644 index 0000000..06fad23 --- /dev/null +++ b/tests/Action/OutputFamilyActionTest.php @@ -0,0 +1,188 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpBadRequestException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; +use App\Entity\OutputFamily; + +final class OutputFamilyActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\OutputFamilyAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); + } + + public function testOutputFamilyIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Output family with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAnOutputFamilyById(): void + { + $outputFamily = $this->addAnOutputFamily(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode($outputFamily), (string) $response->getBody()); + } + + public function testEditAnOutputFamilyEmptyLabelField(): void + { + $this->addAnOutputFamily(); + $this->expectException(HttpBadRequestException::class); + $this->expectExceptionMessage('Param label needed to edit the output family'); + $request = $this->getRequest('PUT')->withParsedBody(array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(400, (int) $response->getStatusCode()); + } + + public function testEditAnOutputFamily(): void + { + $fields = array( + 'label' => 'Modfied family', + 'display' => 20 + ); + $this->addAnOutputFamily(); + $request = $this->getRequest('PUT')->withParsedBody($fields); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame(json_encode(array_merge(['id' => 1], $fields)), (string) $response->getBody()); + } + + public function testDeleteAnOutputFamily(): void + { + $this->addAnOutputFamily(); + $request = $this->getRequest('DELETE'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertSame( + json_encode(array('message' => 'Output family with id 1 is removed!')), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/output-family/1', array( + 'Content-Type' => 'application/json' + )); + } + + private function addAnOutputFamily(): OutputFamily + { + $dataset = $this->addADataset(); + + $family = new OutputFamily($dataset); + $family->setLabel('Default criteria'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + + return $family; + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } +} -- GitLab From c34ac693b72d59fd95ca95af43bbd3f39ca976e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 16:29:10 +0100 Subject: [PATCH 25/31] OutputCategoryListAction => done --- src/Action/OutputCategoryListAction.php | 26 ++-- tests/Action/OutputCategoryListActionTest.php | 114 ++++++++++++++---- 2 files changed, 107 insertions(+), 33 deletions(-) diff --git a/src/Action/OutputCategoryListAction.php b/src/Action/OutputCategoryListAction.php index d818322..072b208 100644 --- a/src/Action/OutputCategoryListAction.php +++ b/src/Action/OutputCategoryListAction.php @@ -14,6 +14,7 @@ namespace App\Action; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpBadRequestException; use App\Entity\OutputCategory; use App\Entity\OutputFamily; @@ -36,8 +37,20 @@ final class OutputCategoryListAction extends AbstractAction return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); } + $outputFamily = $this->em->find('App\Entity\OutputFamily', $args['id']); + + // Returns HTTP 404 if the output family is not found + if (is_null($outputFamily)) { + throw new HttpNotFoundException( + $request, + 'Output family with id ' . $args['id'] . ' is not found' + ); + } + if ($request->getMethod() === GET) { - $outputCategories = $this->em->getRepository('App\Entity\OutputCategory')->findAll(); + $outputCategories = $this->em->getRepository('App\Entity\OutputCategory')->findByOutputFamily( + $outputFamily + ); $payload = json_encode($outputCategories); } @@ -45,7 +58,7 @@ final class OutputCategoryListAction extends AbstractAction $parsedBody = $request->getParsedBody(); // Verif mandatories fields - foreach (array('label', 'display', 'id_output_family') as $a) { + foreach (array('label', 'display') as $a) { if ($this->isEmptyField($a, $parsedBody)) { throw new HttpBadRequestException( $request, @@ -54,15 +67,6 @@ final class OutputCategoryListAction extends AbstractAction } } - // Output family is mandatory - $outputFamily = $this->em->find('App\Entity\OutputFamily', $parsedBody['id_output_family']); - if (is_null($outputFamily)) { - throw new HttpBadRequestException( - $request, - 'Output family with id ' . $parsedBody['id_output_family'] . ' is not found' - ); - } - $outputCategory = $this->postOutputCategory($parsedBody, $outputFamily); $payload = json_encode($outputCategory); $response = $response->withStatus(201); diff --git a/tests/Action/OutputCategoryListActionTest.php b/tests/Action/OutputCategoryListActionTest.php index 3c396e9..930499e 100644 --- a/tests/Action/OutputCategoryListActionTest.php +++ b/tests/Action/OutputCategoryListActionTest.php @@ -16,7 +16,13 @@ use PHPUnit\Framework\TestCase; use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Response; use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; use App\Entity\OutputFamily; use App\Entity\OutputCategory; @@ -38,11 +44,20 @@ final class OutputCategoryListActionTest extends TestCase $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); } + public function testOutputFamilyIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Output family with id 1 is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('id' => 1)); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + public function testGetAllOutputCategories(): void { $outputCategories = $this->addOutputCategories(); $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertSame( json_encode($outputCategories), (string) $response->getBody() @@ -51,24 +66,11 @@ final class OutputCategoryListActionTest extends TestCase public function testAddANewOutputCategoryEmptyLabelField(): void { + $this->addOutputFamily(); $this->expectException(HttpBadRequestException::class); $this->expectExceptionMessage('Param label needed to add a new output category'); $request = $this->getRequest('POST')->withParsedBody(array()); - $response = ($this->action)($request, new Response(), array()); - $this->assertEquals(400, (int) $response->getStatusCode()); - } - - public function testAddANewOutputCategoryOutputFamilyIsNotFound(): void - { - $this->expectException(HttpBadRequestException::class); - $this->expectExceptionMessage('Output family with id 1 is not found'); - $fields = array( - 'label' => 'Default output category', - 'display' => 10, - 'id_output_family' => 1 - ); - $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertEquals(400, (int) $response->getStatusCode()); } @@ -77,13 +79,12 @@ final class OutputCategoryListActionTest extends TestCase $this->addOutputFamily(); $fields = array( 'label' => 'Default output category', - 'display' => 10, - 'id_output_family' => 1 + 'display' => 10 ); $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array()); + $response = ($this->action)($request, new Response(), array('id' => 1)); $this->assertSame( - json_encode(array_merge(['id' => 1], $fields)), + json_encode(array_merge(['id' => 1], $fields, ['id_output_family' => 1])), (string) $response->getBody() ); $this->assertEquals(201, (int) $response->getStatusCode()); @@ -96,14 +97,16 @@ final class OutputCategoryListActionTest extends TestCase private function getRequest(string $method): ServerRequest { - return new ServerRequest($method, '/output-category', array( + return new ServerRequest($method, '/output-family/1/output-category', array( 'Content-Type' => 'application/json' )); } private function addOutputFamily(): OutputFamily { - $family = new OutputFamily(); + $dataset = $this->addADataset(); + + $family = new OutputFamily($dataset); $family->setLabel('Default output family'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -111,6 +114,73 @@ final class OutputCategoryListActionTest extends TestCase return $family; } + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } + private function addOutputCategories(): array { $outputFamily = $this->addOutputFamily(); -- GitLab From 4c5e4ef80e7833262088186daac51624b21191f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 16:34:27 +0100 Subject: [PATCH 26/31] OutputCategoryAction => done --- src/Action/FamilyAction.php | 103 ---------------- src/Action/FamilyListAction.php | 100 --------------- tests/Action/FamilyActionTest.php | 124 ------------------- tests/Action/FamilyListActionTest.php | 141 ---------------------- tests/Action/OutputCategoryActionTest.php | 78 +++++++++++- 5 files changed, 76 insertions(+), 470 deletions(-) delete mode 100644 src/Action/FamilyAction.php delete mode 100644 src/Action/FamilyListAction.php delete mode 100644 tests/Action/FamilyActionTest.php delete mode 100644 tests/Action/FamilyListActionTest.php diff --git a/src/Action/FamilyAction.php b/src/Action/FamilyAction.php deleted file mode 100644 index 6201b2e..0000000 --- a/src/Action/FamilyAction.php +++ /dev/null @@ -1,103 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Action; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Slim\Exception\HttpBadRequestException; -use Slim\Exception\HttpNotFoundException; -use App\Entity\Family; - -final class FamilyAction extends AbstractAction -{ - public function __invoke(Request $request, Response $response, array $args): Response - { - if ($request->getMethod() === OPTIONS) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); - } - - $type = $this->verifyType($args['type']); - if (empty($type)) { - throw new HttpBadRequestException( - $request, - 'Type ' . $args['type'] . ' is not defined' - ); - } - - $entityClass = $this->getEntityClass($type); - $family = $this->em->find($entityClass, $args['id']); - if (is_null($family)) { - throw new HttpNotFoundException( - $request, - ucfirst($type) . ' family with id ' . $args['id'] . ' is not found' - ); - } - - if ($request->getMethod() === GET) { - $payload = json_encode($family); - } - - if ($request->getMethod() === PUT) { - $parsedBody = $request->getParsedBody(); - - // Vérification des champs vides - $fields = array('label', 'display'); - foreach ($fields as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - throw new HttpBadRequestException( - $request, - 'Param ' . $a . ' needed to edit the ' . $type . ' family' - ); - } - } - - $this->editFamily($family, $parsedBody); - $payload = json_encode($family); - } - - if ($request->getMethod() === DELETE) { - $id = $family->getId(); - $this->em->remove($family); - $this->em->flush(); - $payload = json_encode(array( - 'message' => ucfirst($type) . ' family with id ' . $id . ' is removed!' - )); - } - - $response->getBody()->write($payload); - return $response; - } - - private function verifyType(string $type): string - { - $t = strtolower($type); - - if ($t == 'dataset' || $t == 'output' || $t == 'criteria') { - return $t; - } else { - return ''; - } - } - - private function getEntityClass(string $type): string - { - return 'App\Entity\\' . ucfirst($type) . 'Family'; - } - - private function editFamily(object $family, array $parsedBody): void - { - $family->setLabel($parsedBody['label']); - $family->setDisplay($parsedBody['display']); - $this->em->flush(); - } -} diff --git a/src/Action/FamilyListAction.php b/src/Action/FamilyListAction.php deleted file mode 100644 index 8a81b59..0000000 --- a/src/Action/FamilyListAction.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Action; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use Slim\Exception\HttpBadRequestException; -use App\Entity\Family; - -final class FamilyListAction extends AbstractAction -{ - /** - * `GET` Returns a list of all families by type listed in the metamodel database - * `POST` Add a new family - * - * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request - * @param ResponseInterface $response PSR-7 This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke(Request $request, Response $response, array $args): Response - { - if ($request->getMethod() === OPTIONS) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - } - - $type = $this->verifyType($args['type']); - if (empty($type)) { - throw new HttpBadRequestException( - $request, - 'Type ' . $args['type'] . ' is not defined' - ); - } - - if ($request->getMethod() === GET) { - $families = $this->em->getRepository($this->getEntityClass($type))->findAll(); - $payload = json_encode($families); - } - - if ($request->getMethod() === POST) { - $parsedBody = $request->getParsedBody(); - - // To work this action needs information to update - foreach (array('label', 'display') as $a) { - if ($this->isEmptyField($a, $parsedBody)) { - throw new HttpBadRequestException( - $request, - 'Param ' . $a . ' needed to add a new family' - ); - } - } - - $family = $this->postFamily($parsedBody, $this->getEntityClass($type)); - $payload = json_encode($family); - $response = $response->withStatus(201); - } - - $response->getBody()->write($payload); - return $response; - } - - private function verifyType(string $type): string - { - $t = strtolower($type); - - if ($t == 'dataset' || $t == 'output' || $t == 'criteria') { - return $t; - } else { - return ''; - } - } - - private function getEntityClass(string $type): string - { - return 'App\Entity\\' . ucfirst($type) . 'Family'; - } - - private function postFamily(array $parsedBody, string $class): object - { - $family = new $class(); - $family->setLabel($parsedBody['label']); - $family->setDisplay($parsedBody['display']); - - $this->em->persist($family); - $this->em->flush(); - - return $family; - } -} diff --git a/tests/Action/FamilyActionTest.php b/tests/Action/FamilyActionTest.php deleted file mode 100644 index 84ba23c..0000000 --- a/tests/Action/FamilyActionTest.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Tests\Action; - -use PHPUnit\Framework\TestCase; -use Nyholm\Psr7\ServerRequest; -use Nyholm\Psr7\Response; -use Slim\Exception\HttpNotFoundException; -use Slim\Exception\HttpBadRequestException; -use App\tests\EntityManagerBuilder; -use App\Entity\DatasetFamily; - -final class FamilyActionTest extends TestCase -{ - private $action; - private $entityManager; - - protected function setUp(): void - { - $this->entityManager = EntityManagerBuilder::getInstance(); - $this->action = new \App\Action\FamilyAction($this->entityManager); - } - - public function testOptionsHttpMethod(): void - { - $request = $this->getRequest('OPTIONS'); - $response = ($this->action)($request, new Response(), array()); - $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS'); - } - - public function testTypeIsNotDefined(): void - { - $this->expectException(HttpBadRequestException::class); - $this->expectExceptionMessage('Type undifined is not defined'); - $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array('type' => 'undifined')); - $this->assertEquals(400, (int) $response->getStatusCode()); - } - - public function testDatasetFamilyIsNotFound(): void - { - $this->expectException(HttpNotFoundException::class); - $this->expectExceptionMessage('Dataset family with id 1 is not found'); - $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); - $this->assertEquals(404, (int) $response->getStatusCode()); - } - - public function testGetADatasetFamilyById(): void - { - $family = $this->addADatasetFamily(); - $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); - $this->assertSame(json_encode($family), (string) $response->getBody()); - } - - public function testEditADatasetFamilyEmptyLabelField(): void - { - $this->addADatasetFamily(); - $this->expectException(HttpBadRequestException::class); - $this->expectExceptionMessage('Param label needed to edit the dataset family'); - $request = $this->getRequest('PUT')->withParsedBody(array()); - $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); - $this->assertEquals(400, (int) $response->getStatusCode()); - } - - public function testEditADatasetFamily(): void - { - $fields = array( - 'label' => 'New_label', - 'display' => 20 - ); - $this->addADatasetFamily(); - $request = $this->getRequest('PUT')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); - $this->assertSame( - json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset'])), - (string) $response->getBody() - ); - } - - public function testDeleteADatasetFamily(): void - { - $this->addADatasetFamily(); - $request = $this->getRequest('DELETE'); - $response = ($this->action)($request, new Response(), array('type' => 'dataset', 'id' => 1)); - $this->assertSame( - json_encode(array('message' => 'Dataset family with id 1 is removed!')), - (string) $response->getBody() - ); - } - - protected function tearDown(): void - { - $this->entityManager->getConnection()->close(); - } - - private function getRequest(string $method): ServerRequest - { - return new ServerRequest($method, '/family/dataset/1', array( - 'Content-Type' => 'application/json' - )); - } - - private function addADatasetFamily(): DatasetFamily - { - $family = new DatasetFamily(); - $family->setLabel('Default dataset'); - $family->setDisplay(10); - $this->entityManager->persist($family); - $this->entityManager->flush(); - return $family; - } -} diff --git a/tests/Action/FamilyListActionTest.php b/tests/Action/FamilyListActionTest.php deleted file mode 100644 index 8206c58..0000000 --- a/tests/Action/FamilyListActionTest.php +++ /dev/null @@ -1,141 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Tests\Action; - -use PHPUnit\Framework\TestCase; -use Nyholm\Psr7\ServerRequest; -use Nyholm\Psr7\Response; -use Slim\Exception\HttpBadRequestException; -use App\tests\EntityManagerBuilder; -use App\Entity\DatasetFamily; - -final class FamilyListActionTest extends TestCase -{ - private $action; - private $entityManager; - - protected function setUp(): void - { - $this->entityManager = EntityManagerBuilder::getInstance(); - $this->action = new \App\Action\FamilyListAction($this->entityManager); - } - - public function testOptionsHttpMethod(): void - { - $request = $this->getRequest('OPTIONS'); - $response = ($this->action)($request, new Response(), array()); - $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS'); - } - - public function testTypeIsNotDefined(): void - { - $this->expectException(HttpBadRequestException::class); - $this->expectExceptionMessage('Type undifined is not defined'); - $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array('type' => 'undifined')); - $this->assertEquals(400, (int) $response->getStatusCode()); - } - - public function testGetAllDatasetFamilies(): void - { - $families = $this->addDatasetFamilies(); - $request = $this->getRequest('GET'); - $response = ($this->action)($request, new Response(), array('type' => 'dataset')); - $this->assertSame( - json_encode($families), - (string) $response->getBody() - ); - } - - public function testAddANewFamilyEmptyLabelField(): void - { - $this->expectException(HttpBadRequestException::class); - $this->expectExceptionMessage('Param label needed to add a new family'); - $request = $this->getRequest('POST')->withParsedBody(array()); - $response = ($this->action)($request, new Response(), array('type' => 'dataset')); - $this->assertEquals(400, (int) $response->getStatusCode()); - } - - public function testAddANewDatasetFamily(): void - { - $fields = array( - 'label' => 'Default family', - 'display' => 10 - ); - $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array('type' => 'dataset')); - $this->assertSame( - json_encode(array_merge(['id' => 1], $fields, ['type' => 'dataset'])), - (string) $response->getBody() - ); - $this->assertEquals(201, (int) $response->getStatusCode()); - } - - public function testAddANewCriteriaFamily(): void - { - $fields = array( - 'label' => 'Default criteria', - 'display' => 10 - ); - $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array('type' => 'criteria')); - $this->assertSame( - json_encode(array_merge(['id' => 1], $fields, ['type' => 'criteria'])), - (string) $response->getBody() - ); - $this->assertEquals(201, (int) $response->getStatusCode()); - } - - public function testAddANewOutputFamily(): void - { - $fields = array( - 'label' => 'Default output', - 'display' => 10 - ); - $request = $this->getRequest('POST')->withParsedBody($fields); - $response = ($this->action)($request, new Response(), array('type' => 'output')); - $this->assertSame( - json_encode(array_merge(['id' => 1], $fields, ['type' => 'output'])), - (string) $response->getBody() - ); - $this->assertEquals(201, (int) $response->getStatusCode()); - } - - protected function tearDown(): void - { - $this->entityManager->getConnection()->close(); - } - - private function getRequest(string $method): ServerRequest - { - return new ServerRequest($method, '/family/dataset', array( - 'Content-Type' => 'application/json' - )); - } - - private function addDatasetFamilies(): array - { - $family1 = new DatasetFamily(); - $family1->setLabel('Default dataset'); - $family1->setDisplay(10); - $this->entityManager->persist($family1); - - $family2 = new DatasetFamily(); - $family2->setLabel('My family dataset'); - $family2->setDisplay(20); - $this->entityManager->persist($family2); - - $this->entityManager->flush(); - return array($family1, $family2); - } -} diff --git a/tests/Action/OutputCategoryActionTest.php b/tests/Action/OutputCategoryActionTest.php index 277962e..ad292d0 100644 --- a/tests/Action/OutputCategoryActionTest.php +++ b/tests/Action/OutputCategoryActionTest.php @@ -18,8 +18,13 @@ use Nyholm\Psr7\Response; use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpBadRequestException; use App\tests\EntityManagerBuilder; -use App\Entity\OutputCategory; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; use App\Entity\OutputFamily; +use App\Entity\OutputCategory; final class OutputCategoryActionTest extends TestCase { @@ -123,7 +128,9 @@ final class OutputCategoryActionTest extends TestCase private function addOutputFamily(): OutputFamily { - $family = new OutputFamily(); + $dataset = $this->addADataset(); + + $family = new OutputFamily($dataset); $family->setLabel('Default output family'); $family->setDisplay(10); $this->entityManager->persist($family); @@ -131,6 +138,73 @@ final class OutputCategoryActionTest extends TestCase return $family; } + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } + private function addAnOutputCategory(): OutputCategory { $outputFamily = $this->addOutputFamily(); -- GitLab From b314077ea1f99480a71d0f78465eeae060882a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 16:36:01 +0100 Subject: [PATCH 27/31] Fixed bug: phpcs --- src/Action/SearchAction.php | 7 +++++-- tests/Middleware/CorsMiddlewareTest.php | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Action/SearchAction.php b/src/Action/SearchAction.php index 3905ec3..342aa07 100644 --- a/src/Action/SearchAction.php +++ b/src/Action/SearchAction.php @@ -42,8 +42,11 @@ final class SearchAction extends AbstractAction * @param EntityManagerInterface $em Doctrine Entity Manager Interface * @param DBALConnectionFactory $connectionFactory Factory used to construct connection to business database */ - public function __construct(EntityManagerInterface $em, DBALConnectionFactory $connectionFactory, OperatorFactory $operatorFactory) - { + public function __construct( + EntityManagerInterface $em, + DBALConnectionFactory $connectionFactory, + OperatorFactory $operatorFactory + ) { parent::__construct($em); $this->connectionFactory = $connectionFactory; $this->operatorFactory = $operatorFactory; diff --git a/tests/Middleware/CorsMiddlewareTest.php b/tests/Middleware/CorsMiddlewareTest.php index d74c690..7e76f23 100644 --- a/tests/Middleware/CorsMiddlewareTest.php +++ b/tests/Middleware/CorsMiddlewareTest.php @@ -35,7 +35,10 @@ final class CorsMiddlewareTest extends TestCase $corsMiddleware = new \App\Middleware\CorsMiddleware(); $response = $corsMiddleware->process($request, $requestHandler); $this->assertSame((string) $response->getHeaderLine('Access-Control-Allow-Origin'), '*'); - $this->assertSame('Content-Type, Authorization', (string) $response->getHeaderLine('Access-Control-Allow-Headers')); + $this->assertSame( + 'Content-Type, Authorization', + (string) $response->getHeaderLine('Access-Control-Allow-Headers') + ); } public function testCorsHeadersForGetMethod() -- GitLab From 794c2a8c73076bca4630a061b82402ce248adb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 19 Dec 2019 16:48:59 +0100 Subject: [PATCH 28/31] Change url into create-db shell script --- conf-dev/create-db.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/conf-dev/create-db.sh b/conf-dev/create-db.sh index 72f2746..48d6712 100644 --- a/conf-dev/create-db.sh +++ b/conf-dev/create-db.sh @@ -7,11 +7,12 @@ set -e curl -d '{"label":"Test","dbname":"anis_test","dbtype":"pdo_pgsql","dbhost":"db","dbport":5432,"dblogin":"anis","dbpassword":"anis"}' -H "Content-Type: application/json" -X POST http://localhost/database curl -d '{"name":"anis_project","label":"Anis Project Test","description":"Project used for testing","link":"http://project.com","manager":"M. Durand","id_database":1}' -H "Content-Type: application/json" -X POST http://localhost/project -curl -d '{"label":"Default dataset family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/dataset curl -d '{"name":"default","label":"Default instance","client_url":"http://localhost:4200"}' -H "Content-Type: application/json" -X POST http://localhost/instance -curl -d '{"name":"obs_cat","table_ref":"obs_cat","label":"ObsCat dataset","description":"ObsCat","display":"10","count":"10000","vo":false,"data_path":"/mnt/mount","selectable_row":true,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/instance/default/dataset -curl -d '{"name":"observations","table_ref":"observations_info","label":"Observations dataset","description":"Observations","display":"20","count":"177454","vo":false,"data_path":"/mnt/mount","selectable_row":false,"project_name":"anis_project","id_dataset_family":1}' -H "Content-Type: application/json" -X POST http://localhost/instance/default/dataset -curl -d '{"label":"Default criteria family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/criteria -curl -d '{"label":"Default output family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/output -curl -d '{"label":"Default output category","display":10,"id_output_family":1}' -H "Content-Type: application/json" -X POST http://localhost/output-category +curl -d '{"label":"Default dataset family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/instance/default/dataset-family +curl -d '{"name":"obs_cat","table_ref":"obs_cat","label":"ObsCat dataset","description":"ObsCat","display":10,"count":10000,"vo":false,"data_path":"/mnt/mount","selectable_row":true,"project_name":"anis_project"}' -H "Content-Type: application/json" -X POST http://localhost/dataset-family/1/dataset +curl -d '{"name":"observations","table_ref":"observations_info","label":"Observations dataset","description":"Observations","display":20,"count":177454,"vo":false,"data_path":"/mnt/mount","selectable_row":false,"project_name":"anis_project"}' -H "Content-Type: application/json" -X POST http://localhost/dataset-family/1/dataset + +#curl -d '{"label":"Default criteria family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/criteria +#curl -d '{"label":"Default output family","display":10}' -H "Content-Type: application/json" -X POST http://localhost/family/output +#curl -d '{"label":"Default output category","display":10,"id_output_family":1}' -H "Content-Type: application/json" -X POST http://localhost/output-category -- GitLab From 5baac85843c0683f4f2a68a13d469930f3fbb71b Mon Sep 17 00:00:00 2001 From: Tifenn GUILLAS <tifenn.guillas@lam.fr> Date: Fri, 20 Dec 2019 11:51:46 +0100 Subject: [PATCH 29/31] Fix conflict with getAttributes() returned type --- docker-compose.yml | 5 +++++ src/Entity/Dataset.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 42f384a..67e2516 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,11 @@ services: - ./conf-dev/obs_cat.sql:/sql/obs_cat.sql - ./conf-dev/observations_info.sql:/sql/observations_info.sql - ./conf-dev/init-postgres.sh:/docker-entrypoint-initdb.d/init-postgres.sh + + adminer: + image: adminer + ports: + - 8083:8080 volumes: pgdata: \ No newline at end of file diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index ff6da07..5780627 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace App\Entity; -use Doctrine\Common\Collections\ArrayCollection; +// use Doctrine\Common\Collections\Pers; /** * @Entity @@ -218,7 +218,7 @@ class Dataset implements \JsonSerializable $this->datasetFamily = $datasetFamily; } - public function getAttributes(): ArrayCollection + public function getAttributes() { return $this->attributes; } -- GitLab From 2a8a23ef74025665c4f8c794a1c2f035321c5f2a Mon Sep 17 00:00:00 2001 From: Tifenn GUILLAS <tifenn.guillas@lam.fr> Date: Fri, 20 Dec 2019 11:53:06 +0100 Subject: [PATCH 30/31] Fix typo --- src/Entity/Dataset.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index 5780627..33c61c1 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -12,8 +12,6 @@ declare(strict_types=1); namespace App\Entity; -// use Doctrine\Common\Collections\Pers; - /** * @Entity * @Table(name="dataset") -- GitLab From 254be201a39b0f5b36999a703bfc30f9b172b352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Fri, 20 Dec 2019 14:29:21 +0100 Subject: [PATCH 31/31] OutputCategoryListByDatasetAction => done --- app/dependencies.php | 14 +- app/routes.php | 1 + .../OutputCategoryListByDatasetAction.php | 60 ++++++ src/Entity/Dataset.php | 2 + .../OutputCategoryListByDatasetActionTest.php | 177 ++++++++++++++++++ 5 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 src/Action/OutputCategoryListByDatasetAction.php create mode 100644 tests/Action/OutputCategoryListByDatasetActionTest.php diff --git a/app/dependencies.php b/app/dependencies.php index f81c995..3a782ce 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -100,23 +100,27 @@ $container->set('App\Action\DatasetListAction', function (ContainerInterface $c) }); $container->set('App\Action\DatasetAction', function (ContainerInterface $c) { - return new App\Action\DatasetAction($c->get('em'), new App\Utils\DBALConnectionFactory()); + return new App\Action\DatasetAction($c->get('em')); }); $container->set('App\Action\CriteriaFamilyListAction', function (ContainerInterface $c) { - return new App\Action\CriteriaFamilyListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); + return new App\Action\CriteriaFamilyListAction($c->get('em')); }); $container->set('App\Action\CriteriaFamilyAction', function (ContainerInterface $c) { - return new App\Action\CriteriaFamilyAction($c->get('em'), new App\Utils\DBALConnectionFactory()); + return new App\Action\CriteriaFamilyAction($c->get('em')); }); $container->set('App\Action\OutputFamilyListAction', function (ContainerInterface $c) { - return new App\Action\OutputFamilyListAction($c->get('em'), new App\Utils\DBALConnectionFactory()); + return new App\Action\OutputFamilyListAction($c->get('em')); }); $container->set('App\Action\OutputFamilyAction', function (ContainerInterface $c) { - return new App\Action\OutputFamilyAction($c->get('em'), new App\Utils\DBALConnectionFactory()); + return new App\Action\OutputFamilyAction($c->get('em')); +}); + +$container->set('App\Action\OutputCategoryListByDatasetAction', function (ContainerInterface $c) { + return new App\Action\OutputCategoryListByDatasetAction($c->get('em')); }); $container->set('App\Action\OutputCategoryListAction', function (ContainerInterface $c) { diff --git a/app/routes.php b/app/routes.php index 57a2bed..f21606c 100644 --- a/app/routes.php +++ b/app/routes.php @@ -29,6 +29,7 @@ $app->map([OPTIONS, GET, POST], '/dataset/{name}/criteria-family', App\Action\Cr $app->map([OPTIONS, GET, PUT, DELETE], '/criteria-family/{id}', App\Action\CriteriaFamilyAction::class); $app->map([OPTIONS, GET, POST], '/dataset/{name}/output-family', App\Action\OutputFamilyListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/output-family/{id}', App\Action\OutputFamilyAction::class); +$app->map([OPTIONS, GET], '/dataset/{name}/output-category', App\Action\OutputCategoryListByDatasetAction::class); $app->map([OPTIONS, GET, POST], '/output-family/{id}/output-category', App\Action\OutputCategoryListAction::class); $app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class); $app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class); diff --git a/src/Action/OutputCategoryListByDatasetAction.php b/src/Action/OutputCategoryListByDatasetAction.php new file mode 100644 index 0000000..9e0fed0 --- /dev/null +++ b/src/Action/OutputCategoryListByDatasetAction.php @@ -0,0 +1,60 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpNotFoundException; + +final class OutputCategoryListByDatasetAction extends AbstractAction +{ + /** + * `GET` Returns a list of all output categories for a given dataset + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + } + + $dataset = $this->em->find('App\Entity\Dataset', $args['name']); + + // Returns HTTP 404 if the dataset is not found + if (is_null($dataset)) { + throw new HttpNotFoundException( + $request, + 'Dataset with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $qb = $this->em->createQueryBuilder(); + $qb->select('o') + ->from('App\Entity\OutputCategory', 'o') + ->join('o.outputFamily', 'f') + ->where($qb->expr()->eq('IDENTITY(f.dataset)', ':datasetName')) + ->setParameter('datasetName', $dataset->getName()); + $datasets = $qb->getQuery()->getResult(); + $payload = json_encode($datasets); + } + + $response->getBody()->write($payload); + return $response; + } +} diff --git a/src/Entity/Dataset.php b/src/Entity/Dataset.php index 33c61c1..47d7741 100644 --- a/src/Entity/Dataset.php +++ b/src/Entity/Dataset.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace App\Entity; +use Doctrine\Common\Collections\ArrayCollection; + /** * @Entity * @Table(name="dataset") diff --git a/tests/Action/OutputCategoryListByDatasetActionTest.php b/tests/Action/OutputCategoryListByDatasetActionTest.php new file mode 100644 index 0000000..afab9ee --- /dev/null +++ b/tests/Action/OutputCategoryListByDatasetActionTest.php @@ -0,0 +1,177 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Tests\Action; + +use PHPUnit\Framework\TestCase; +use Nyholm\Psr7\ServerRequest; +use Nyholm\Psr7\Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\tests\EntityManagerBuilder; +use App\Entity\Database; +use App\Entity\Project; +use App\Entity\Instance; +use App\Entity\DatasetFamily; +use App\Entity\Dataset; +use App\Entity\OutputFamily; +use App\Entity\OutputCategory; + +final class OutputCategoryListByDatasetActionTest extends TestCase +{ + private $action; + private $entityManager; + + protected function setUp(): void + { + $this->entityManager = EntityManagerBuilder::getInstance(); + $this->action = new \App\Action\OutputCategoryListByDatasetAction($this->entityManager); + } + + public function testOptionsHttpMethod(): void + { + $request = $this->getRequest('OPTIONS'); + $response = ($this->action)($request, new Response(), array()); + $this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, OPTIONS'); + } + + public function testDatasetIsNotFound(): void + { + $this->expectException(HttpNotFoundException::class); + $this->expectExceptionMessage('Dataset with name obs_cat is not found'); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertEquals(404, (int) $response->getStatusCode()); + } + + public function testGetAllOutputCategoriesForADataset(): void + { + $outputCategories = $this->addOutputCategories(); + $request = $this->getRequest('GET'); + $response = ($this->action)($request, new Response(), array('name' => 'obs_cat')); + $this->assertSame( + json_encode($outputCategories), + (string) $response->getBody() + ); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->close(); + } + + private function getRequest(string $method): ServerRequest + { + return new ServerRequest($method, '/dataset/obs_cat/output-category', array( + 'Content-Type' => 'application/json' + )); + } + + private function addOutputFamily(): OutputFamily + { + $dataset = $this->addADataset(); + + $family = new OutputFamily($dataset); + $family->setLabel('Default output family'); + $family->setDisplay(10); + $this->entityManager->persist($family); + $this->entityManager->flush(); + return $family; + } + + private function addProject(): Project + { + $database = new Database(); + $database->setLabel('Test1'); + $database->setDbName('test1'); + $database->setType('pgsql'); + $database->setHost('db'); + $database->setPort(5432); + $database->setLogin('test'); + $database->setPassword('test'); + $this->entityManager->persist($database); + + $project = new Project('anis_project'); + $project->setLabel('Test project'); + $project->setDescription('Test description'); + $project->setLink('http://test.com'); + $project->setManager('User1'); + $project->setDatabase($database); + $this->entityManager->persist($project); + + $this->entityManager->flush(); + return $project; + } + + private function addInstance(): Instance + { + $instance = new Instance('aspic', 'Aspic'); + $instance->setClientUrl('http://cesam.lam.fr/aspic'); + $this->entityManager->persist($instance); + $this->entityManager->flush(); + return $instance; + } + + private function addDatasetFamily(): DatasetFamily + { + $instance = $this->addInstance(); + + $family = new DatasetFamily($instance); + $family->setLabel('Default dataset'); + $family->setDisplay(10); + $this->entityManager->persist($family); + + $this->entityManager->flush(); + return $family; + } + + private function addADataset(): Dataset + { + $project = $this->addProject(); + $family = $this->addDatasetFamily(); + + $dataset = new Dataset('obs_cat'); + $dataset->setTableRef('v_obs_cat'); + $dataset->setLabel('Obscat label'); + $dataset->setDescription('Obscat description'); + $dataset->setDisplay(10); + $dataset->setCount(10000); + $dataset->setVo(false); + $dataset->setDataPath('/mnt/obs_cat'); + $dataset->setSelectableRow(false); + $dataset->setProject($project); + $dataset->setDatasetFamily($family); + $this->entityManager->persist($dataset); + $this->entityManager->flush(); + return $dataset; + } + + private function addOutputCategories(): array + { + $outputFamily = $this->addOutputFamily(); + + $outputCategory1 = new OutputCategory(); + $outputCategory1->setLabel('Default output category'); + $outputCategory1->setDisplay(10); + $outputCategory1->setOutputFamily($outputFamily); + $this->entityManager->persist($outputCategory1); + + $outputCategory2 = new OutputCategory(); + $outputCategory2->setLabel('My output category'); + $outputCategory2->setDisplay(20); + $outputCategory2->setOutputFamily($outputFamily); + $this->entityManager->persist($outputCategory2); + + $this->entityManager->flush(); + return array($outputCategory1, $outputCategory2); + } +} -- GitLab