From 30ac5e4c24cff1be9653b0e2573e2f9edbbb1840 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Mon, 8 Mar 2021 16:06:22 +0100 Subject: [PATCH] Drop SQLite support (#2405) --- .env.dist | 56 +++--- .github/workflows/coverage.yaml | 15 +- .github/workflows/linting.yaml | 2 +- .github/workflows/testing.yaml | 25 ++- .gitignore | 1 - README.md | 5 +- config/packages/doctrine.yaml | 125 +++++++++++++- config/packages/doctrine/mysql.yaml | 118 ------------- config/packages/doctrine/sqlite.yaml | 55 ------ config/services.yaml | 6 - phpunit.xml.dist | 20 ++- ...etCommand.php => AbstractResetCommand.php} | 34 ++-- src/Command/CreateReleaseCommand.php | 159 ------------------ src/Command/InstallCommand.php | 7 +- src/Command/ResetDevelopmentCommand.php | 38 +++++ src/Command/ResetTestCommand.php | 137 +++++++++++++++ .../Compiler/DoctrineCompilerPass.php | 134 --------------- src/Doctrine/AbstractMigration.php | 72 +++----- src/Doctrine/SqliteSessionInitSubscriber.php | 40 ----- src/Kernel.php | 2 - src/Migrations/Version20180701120000.php | 44 ++--- src/Migrations/Version20180715160326.php | 32 +--- src/Migrations/Version20180730044139.php | 32 +--- src/Migrations/Version20180924111853.php | 27 +-- src/Migrations/Version20181031220003.php | 100 +++-------- src/Migrations/Version20190201150324.php | 6 +- src/Migrations/Version20190305152308.php | 105 +----------- src/Migrations/Version20190321181243.php | 9 +- src/Migrations/Version20190605171157.php | 15 -- src/Migrations/Version20190706224219.php | 15 -- src/Migrations/Version20190729162655.php | 15 -- src/Migrations/Version20190813162649.php | 15 -- src/Migrations/Version20191024100951.php | 15 -- src/Migrations/Version20191108151534.php | 15 -- src/Migrations/Version20191113132640.php | 15 -- src/Migrations/Version20191116110124.php | 15 -- src/Migrations/Version20200125123942.php | 15 -- src/Migrations/Version20200204124425.php | 15 -- src/Migrations/Version20200705152310.php | 15 -- tests/API/ActivityControllerTest.php | 72 +++++--- tests/API/CustomerControllerTest.php | 15 ++ tests/API/ProjectControllerTest.php | 74 +++++--- tests/API/RateControllerTestTrait.php | 9 +- tests/API/TagControllerTest.php | 29 ++-- tests/API/TeamControllerTest.php | 23 ++- tests/API/TimesheetControllerTest.php | 152 +++++++++-------- tests/Command/CreateReleaseCommandTest.php | 38 ----- tests/Command/InstallCommandTest.php | 28 --- tests/Command/InvoiceCreateCommandTest.php | 23 +-- ...st.php => ResetDevelopmentCommandTest.php} | 13 +- tests/Command/ResetTestCommandTest.php | 38 +++++ tests/ConsoleApplicationTest.php | 29 ++++ tests/Controller/ActivityControllerTest.php | 28 +-- tests/Controller/CustomerControllerTest.php | 44 +++-- tests/Controller/InvoiceControllerTest.php | 99 +++++------ tests/Controller/PermissionControllerTest.php | 27 ++- tests/Controller/ProjectControllerTest.php | 45 +++-- tests/Controller/TagControllerTest.php | 26 +-- tests/Controller/TeamControllerTest.php | 11 +- tests/Controller/TimesheetControllerTest.php | 17 +- .../TimesheetTeamControllerTest.php | 9 +- tests/Controller/UserControllerTest.php | 9 +- tests/DataFixtures/ActivityFixtures.php | 16 +- tests/DataFixtures/CustomerFixtures.php | 13 +- ...xtures.php => InvoiceTemplateFixtures.php} | 20 +-- tests/DataFixtures/ProjectFixtures.php | 14 +- tests/DataFixtures/TagFixtures.php | 13 +- tests/DataFixtures/TeamFixtures.php | 13 +- tests/DataFixtures/TestFixture.php | 23 +++ tests/DataFixtures/TimesheetFixtures.php | 43 +++-- .../SqliteSessionInitSubscriberTest.php | 71 -------- tests/Doctrine/UTCDateTimeTypeTest.php | 23 ++- tests/KernelTest.php | 26 +++ tests/KernelTestTrait.php | 14 +- tests/Repository/TagRepositoryTest.php | 7 - tests/Repository/TimesheetRepositoryTest.php | 12 +- tests/bootstrap.php | 24 +++ var/data/kimai_test.sqlite | Bin 770048 -> 0 bytes 78 files changed, 1165 insertions(+), 1596 deletions(-) delete mode 100644 config/packages/doctrine/mysql.yaml delete mode 100644 config/packages/doctrine/sqlite.yaml rename src/Command/{ResetCommand.php => AbstractResetCommand.php} (84%) delete mode 100644 src/Command/CreateReleaseCommand.php create mode 100644 src/Command/ResetDevelopmentCommand.php create mode 100644 src/Command/ResetTestCommand.php delete mode 100644 src/DependencyInjection/Compiler/DoctrineCompilerPass.php delete mode 100644 src/Doctrine/SqliteSessionInitSubscriber.php delete mode 100644 tests/Command/CreateReleaseCommandTest.php rename tests/Command/{ResetCommandTest.php => ResetDevelopmentCommandTest.php} (58%) create mode 100644 tests/Command/ResetTestCommandTest.php create mode 100644 tests/ConsoleApplicationTest.php rename tests/DataFixtures/{InvoiceFixtures.php => InvoiceTemplateFixtures.php} (83%) create mode 100644 tests/DataFixtures/TestFixture.php delete mode 100644 tests/Doctrine/SqliteSessionInitSubscriberTest.php create mode 100644 tests/KernelTest.php create mode 100644 tests/bootstrap.php delete mode 100644 var/data/kimai_test.sqlite diff --git a/.env.dist b/.env.dist index 6a714de3..3e7a4dea 100644 --- a/.env.dist +++ b/.env.dist @@ -1,36 +1,30 @@ -# This file is a "template" of which env vars need to be defined for your application -# Copy this file to .env file for development, create environment variables when deploying to production -# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration +### DATABASE CONFIGURATION +# Replace "user", "password" and "database" with your database connection. +# Configure the server version, MariaDB requires the "mariadb-" prefix, eg: +# for MySQL "serverVersion=5.7" and for MariaDB "serverVersion=mariadb-10.5.8" +DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=5.7 -###> symfony/framework-bundle ### +### EMAIL CONFIGURATION +# Emails will be sent "from": +MAILER_FROM=kimai@example.com + +# Email connection (disabled by default with MAILER_URL=null://null) +# SMTP: smtp://localhost:25?encryption=&auth_mode= +# Google: gmail://username:password@default +# Amazon: ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 +# Mailchimp: mandrill://KEY@default +# Mailgun: mailgun://KEY:DOMAIN@default +# Postmark: postmark://ID@default +# Sendgrid: sendgrid://KEY@default +# Disable emails: null://null +MAILER_URL=null://null + +### APPLICATION CONFIGURATION APP_ENV=prod APP_SECRET=change_this_to_something_unique -#TRUSTED_PROXIES=127.0.0.1,127.0.0.2 -#TRUSTED_HOSTS=localhost,example.com -###< symfony/framework-bundle ### -###> doctrine/doctrine-bundle ### -# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url -# For a MySQL database, use: "mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=10.2.12&charset=utf8" -# For a MariaDB database, use: "mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=mariadb-10.2.12" -# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data/kimai.sqlite" -# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml -# DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name -DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai.sqlite -###< doctrine/doctrine-bundle ### - -###> nelmio/cors-bundle ### +# Running in a "special" environment, eg. behind reverse proxies? +# Check those: +# TRUSTED_PROXIES=127.0.0.1,127.0.0.2 +# TRUSTED_HOSTS=localhost,example.com CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$ -###< nelmio/cors-bundle ### - -### Email configuration -# SMTP: smtp://localhost:25?encryption=&auth_mode= -# Google: gmail://username:password@default -# Amazon: ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 -# Mailchimp: mandrill://KEY@default -# Mailgun: mailgun://KEY:DOMAIN@default -# Postmark: postmark://ID@default -# Sendgrid: sendgrid://KEY@default -# Disable emails: null://null -MAILER_URL=null://null -MAILER_FROM=kimai@example.com diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 9e20e5e3..037346c8 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -7,6 +7,16 @@ on: jobs: tests: runs-on: ubuntu-latest + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: false + MYSQL_ROOT_PASSWORD: kimai + MYSQL_DATABASE: kimai + ports: + - 3306/tcp + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 strategy: matrix: php: ['7.3'] @@ -18,7 +28,7 @@ jobs: with: php-version: ${{ matrix.php }} coverage: pcov - extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, zip, gd, ldap + extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap - run: | composer install --no-progress composer require laminas/laminas-ldap @@ -27,6 +37,9 @@ jobs: - name: Run tests run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml env: + DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7 + APP_ENV: dev + MAILER_URL: null://localhost TEST_WITH_BUNDLES: 1 - name: Upload code coverage uses: codecov/codecov-action@v1 diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index dd6b24e7..b564db2a 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -18,7 +18,7 @@ jobs: with: php-version: ${{ matrix.php }} coverage: none - extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, zip, gd, ldap + extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap tools: cs2pr:1.1.0 - run: composer install --no-progress - run: composer validate --strict diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 73dc59f0..77bfb82f 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -28,7 +28,7 @@ jobs: with: php-version: ${{ matrix.php }} coverage: none - extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, zip, gd, ldap + extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap - run: | composer install --no-progress composer require laminas/laminas-ldap @@ -36,23 +36,22 @@ jobs: run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Run unit tests run: composer kimai:tests-unit + env: + DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7 + APP_ENV: dev + MAILER_URL: null://localhost + - name: Run integration tests + run: composer kimai:tests-integration + env: + DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7 + APP_ENV: dev + MAILER_URL: null://localhost - name: Run migrations on MySQL run: | bin/console doctrine:database:create --if-not-exists -n bin/console doctrine:migrations:migrate -n bin/console doctrine:migrations:migrate first -n env: - DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai + DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7 APP_ENV: dev MAILER_URL: null://localhost - - name: Run migrations on SQLite - run: | - bin/console doctrine:database:create -n - bin/console doctrine:migrations:migrate -n - bin/console doctrine:migrations:migrate first -n - env: - DATABASE_URL: sqlite:///%kernel.project_dir%/var/data/kimai_migrations.sqlite - APP_ENV: dev - MAILER_URL: null://localhost - - name: Run integration tests - run: composer kimai:tests-integration diff --git a/.gitignore b/.gitignore index 51fbae3d..5f50362e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,6 @@ translations/branding.en.xlf /var/data/* !/var/data/.gitkeep -!/var/data/kimai_test.sqlite /var/coverage/ /var/cache/* !var/cache/.gitkeep diff --git a/README.md b/README.md index 6176028c..1245b538 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Do ### Requirements - PHP 7.2.9 or higher -- Database (MySQL/MariaDB, SQLite for development) +- MariaDB or MySQL - Webserver (nginx, Apache) - A modern browser -- [Other libraries](https://www.kimai.org/download/) +- Some PHP extensions, see [composer.json](composer.json) or [here](https://www.kimai.org/download/) ### About @@ -47,7 +47,6 @@ user specific rates, advanced search & filtering, money and time budgets with re - [Docker](https://www.kimai.org/documentation/docker.html) - containerized - [Development](https://www.kimai.org/documentation/installation.html#development-installation) - on your local machine - [1-click installer](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations) - hosted environments -- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation) - unfortunately still widely used ;-) ### Updating Kimai diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 17b60cca..58dbf864 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -13,8 +13,7 @@ doctrine: default: # existing migrations will fail if the schema filter is activated #schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~ - # Removing "resolve:" would fail eg. SQLite URLs - url: '%env(resolve:DATABASE_URL)%' + url: '%env(DATABASE_URL)%' driver: 'pdo_mysql' # this setting prevents automatic database detection and finds a lot of false-negatives on doctrine:migrations:diff # for null columns with MariaDB. Each migration tries to convert EVERY nullable column. @@ -40,3 +39,125 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: Kimai + dql: + datetime_functions: + addtime: DoctrineExtensions\Query\Mysql\AddTime + convert_tz: DoctrineExtensions\Query\Mysql\ConvertTz + date: DoctrineExtensions\Query\Mysql\Date + date_format: DoctrineExtensions\Query\Mysql\DateFormat + dateadd: DoctrineExtensions\Query\Mysql\DateAdd + datesub: DoctrineExtensions\Query\Mysql\DateSub + datediff: DoctrineExtensions\Query\Mysql\DateDiff + day: DoctrineExtensions\Query\Mysql\Day + dayname: DoctrineExtensions\Query\Mysql\DayName + dayofweek: DoctrineExtensions\Query\Mysql\DayOfWeek + dayofyear: DoctrineExtensions\Query\Mysql\DayOfYear + div: DoctrineExtensions\Query\Mysql\Div + from_unixtime: DoctrineExtensions\Query\Mysql\FromUnixtime + hour: DoctrineExtensions\Query\Mysql\Hour + last_day: DoctrineExtensions\Query\Mysql\LastDay + makedate: DoctrineExtensions\Query\Mysql\MakeDate + minute: DoctrineExtensions\Query\Mysql\Minute + now: DoctrineExtensions\Query\Mysql\Now + month: DoctrineExtensions\Query\Mysql\Month + monthname: DoctrineExtensions\Query\Mysql\MonthName + period_diff: DoctrineExtensions\Query\Mysql\PeriodDiff + second: DoctrineExtensions\Query\Mysql\Second + sectotime: DoctrineExtensions\Query\Mysql\SecToTime + strtodate: DoctrineExtensions\Query\Mysql\StrToDate + time: DoctrineExtensions\Query\Mysql\Time + timediff: DoctrineExtensions\Query\Mysql\TimeDiff + timestampadd: DoctrineExtensions\Query\Mysql\TimestampAdd + timestampdiff: DoctrineExtensions\Query\Mysql\TimestampDiff + timetosec: DoctrineExtensions\Query\Mysql\TimeToSec + truncate: DoctrineExtensions\Query\Mysql\Truncate + week: DoctrineExtensions\Query\Mysql\Week + weekday: DoctrineExtensions\Query\Mysql\WeekDay + year: DoctrineExtensions\Query\Mysql\Year + yearmonth: DoctrineExtensions\Query\Mysql\YearMonth + yearweek: DoctrineExtensions\Query\Mysql\YearWeek + unix_timestamp: DoctrineExtensions\Query\Mysql\UnixTimestamp + utc_timestamp: DoctrineExtensions\Query\Mysql\UtcTimestamp + extract: DoctrineExtensions\Query\Mysql\Extract + + numeric_functions: + acos: DoctrineExtensions\Query\Mysql\Acos + asin: DoctrineExtensions\Query\Mysql\Asin + atan2: DoctrineExtensions\Query\Mysql\Atan2 + atan: DoctrineExtensions\Query\Mysql\Atan + bit_count: DoctrineExtensions\Query\Mysql\BitCount + bit_xor: DoctrineExtensions\Query\Mysql\BitXor + ceil: DoctrineExtensions\Query\Mysql\Ceil + cos: DoctrineExtensions\Query\Mysql\Cos + cot: DoctrineExtensions\Query\Mysql\Cot + degrees: DoctrineExtensions\Query\Mysql\Degrees + exp: DoctrineExtensions\Query\Mysql\Exp + floor: DoctrineExtensions\Query\Mysql\Floor + json_contains: DoctrineExtensions\Query\Mysql\JsonContains + json_depth: DoctrineExtensions\Query\Mysql\JsonDepth + json_length: DoctrineExtensions\Query\Mysql\JsonLength + log: DoctrineExtensions\Query\Mysql\Log + log10: DoctrineExtensions\Query\Mysql\Log10 + log2: DoctrineExtensions\Query\Mysql\Log2 + pi: DoctrineExtensions\Query\Mysql\Pi + power: DoctrineExtensions\Query\Mysql\Power + quarter: DoctrineExtensions\Query\Mysql\Quarter + radians: DoctrineExtensions\Query\Mysql\Radians + rand: DoctrineExtensions\Query\Mysql\Rand + round: DoctrineExtensions\Query\Mysql\Round + stddev: DoctrineExtensions\Query\Mysql\StdDev + sin: DoctrineExtensions\Query\Mysql\Sin + std: DoctrineExtensions\Query\Mysql\Std + tan: DoctrineExtensions\Query\Mysql\Tan + variance: DoctrineExtensions\Query\Mysql\Variance + + string_functions: + aes_decrypt: DoctrineExtensions\Query\Mysql\AesDecrypt + aes_encrypt: DoctrineExtensions\Query\Mysql\AesEncrypt + any_value: DoctrineExtensions\Query\Mysql\AnyValue + ascii: DoctrineExtensions\Query\Mysql\Ascii + binary: DoctrineExtensions\Query\Mysql\Binary + cast: DoctrineExtensions\Query\Mysql\Cast + char_length: DoctrineExtensions\Query\Mysql\CharLength + collate: DoctrineExtensions\Query\Mysql\Collate + concat_ws: DoctrineExtensions\Query\Mysql\ConcatWs + countif: DoctrineExtensions\Query\Mysql\CountIf + crc32: DoctrineExtensions\Query\Mysql\Crc32 + degrees: DoctrineExtensions\Query\Mysql\Degrees + field: DoctrineExtensions\Query\Mysql\Field + find_in_set: DoctrineExtensions\Query\Mysql\FindInSet + format: DoctrineExtensions\Query\Mysql\Format + from_base64: DoctrineExtensions\Query\Mysql\FromBase64 + greatest: DoctrineExtensions\Query\Mysql\Greatest + group_concat: DoctrineExtensions\Query\Mysql\GroupConcat + hex: DoctrineExtensions\Query\Mysql\Hex + ifelse: DoctrineExtensions\Query\Mysql\IfElse + ifnull: DoctrineExtensions\Query\Mysql\IfNull + inet_aton: DoctrineExtensions\Query\Mysql\InetAton + inet_ntoa: DoctrineExtensions\Query\Mysql\InetNtoa + inet6_aton: DoctrineExtensions\Query\Mysql\Inet6Aton + inet6_ntoa: DoctrineExtensions\Query\Mysql\Inet6Ntoa + instr: DoctrineExtensions\Query\Mysql\Instr + is_ipv4: DoctrineExtensions\Query\Mysql\IsIpv4 + is_ipv4_compat: DoctrineExtensions\Query\Mysql\IsIpv4Compat + is_ipv4_mapped: DoctrineExtensions\Query\Mysql\IsIpv4Mapped + is_ipv6: DoctrineExtensions\Query\Mysql\IsIpv6 + lag: DoctrineExtensions\Query\Mysql\Lag + lead: DoctrineExtensions\Query\Mysql\Lead + least: DoctrineExtensions\Query\Mysql\Least + lpad: DoctrineExtensions\Query\Mysql\Lpad + match: DoctrineExtensions\Query\Mysql\MatchAgainst + md5: DoctrineExtensions\Query\Mysql\Md5 + nullif: DoctrineExtensions\Query\Mysql\NullIf + over: DoctrineExtensions\Query\Mysql\Over + radians: DoctrineExtensions\Query\Mysql\Radians + regexp: DoctrineExtensions\Query\Mysql\Regexp + replace: DoctrineExtensions\Query\Mysql\Replace + rpad: DoctrineExtensions\Query\Mysql\Rpad + sha1: DoctrineExtensions\Query\Mysql\Sha1 + sha2: DoctrineExtensions\Query\Mysql\Sha2 + soundex: DoctrineExtensions\Query\Mysql\Soundex + str_to_date: DoctrineExtensions\Query\Mysql\StrToDate + substring_index: DoctrineExtensions\Query\Mysql\SubstringIndex + unhex: DoctrineExtensions\Query\Mysql\Unhex + uuid_short: DoctrineExtensions\Query\Mysql\UuidShort diff --git a/config/packages/doctrine/mysql.yaml b/config/packages/doctrine/mysql.yaml deleted file mode 100644 index d152932d..00000000 --- a/config/packages/doctrine/mysql.yaml +++ /dev/null @@ -1,118 +0,0 @@ -doctrine: - orm: - dql: - datetime_functions: - addtime: DoctrineExtensions\Query\Mysql\AddTime - date: DoctrineExtensions\Query\Mysql\Date - date_format: DoctrineExtensions\Query\Mysql\DateFormat - dateadd: DoctrineExtensions\Query\Mysql\DateAdd - datesub: DoctrineExtensions\Query\Mysql\DateSub - datediff: DoctrineExtensions\Query\Mysql\DateDiff - day: DoctrineExtensions\Query\Mysql\Day - dayname: DoctrineExtensions\Query\Mysql\DayName - dayofweek: DoctrineExtensions\Query\Mysql\DayOfWeek - dayofyear: DoctrineExtensions\Query\Mysql\DayOfYear - div: DoctrineExtensions\Query\Mysql\Div - from_unixtime: DoctrineExtensions\Query\Mysql\FromUnixtime - hour: DoctrineExtensions\Query\Mysql\Hour - last_day: DoctrineExtensions\Query\Mysql\LastDay - makedate: DoctrineExtensions\Query\Mysql\MakeDate - minute: DoctrineExtensions\Query\Mysql\Minute - now: DoctrineExtensions\Query\Mysql\Now - month: DoctrineExtensions\Query\Mysql\Month - monthname: DoctrineExtensions\Query\Mysql\MonthName - period_diff: DoctrineExtensions\Query\Mysql\PeriodDiff - second: DoctrineExtensions\Query\Mysql\Second - sectotime: DoctrineExtensions\Query\Mysql\SecToTime - strtodate: DoctrineExtensions\Query\Mysql\StrToDate - time: DoctrineExtensions\Query\Mysql\Time - timediff: DoctrineExtensions\Query\Mysql\TimeDiff - timestampadd: DoctrineExtensions\Query\Mysql\TimestampAdd - timestampdiff: DoctrineExtensions\Query\Mysql\TimestampDiff - timetosec: DoctrineExtensions\Query\Mysql\TimeToSec - week: DoctrineExtensions\Query\Mysql\Week - weekday: DoctrineExtensions\Query\Mysql\WeekDay - year: DoctrineExtensions\Query\Mysql\Year - yearmonth: DoctrineExtensions\Query\Mysql\YearMonth - yearweek: DoctrineExtensions\Query\Mysql\YearWeek - unix_timestamp: DoctrineExtensions\Query\Mysql\UnixTimestamp - utc_timestamp: DoctrineExtensions\Query\Mysql\UtcTimestamp - extract: DoctrineExtensions\Query\Mysql\Extract - - numeric_functions: - acos: DoctrineExtensions\Query\Mysql\Acos - asin: DoctrineExtensions\Query\Mysql\Asin - atan2: DoctrineExtensions\Query\Mysql\Atan2 - atan: DoctrineExtensions\Query\Mysql\Atan - bit_count: DoctrineExtensions\Query\Mysql\BitCount - bit_xor: DoctrineExtensions\Query\Mysql\BitXor - ceil: DoctrineExtensions\Query\Mysql\Ceil - cos: DoctrineExtensions\Query\Mysql\Cos - cot: DoctrineExtensions\Query\Mysql\Cot - degrees: DoctrineExtensions\Query\Mysql\Degrees - exp: DoctrineExtensions\Query\Mysql\Exp - floor: DoctrineExtensions\Query\Mysql\Floor - log: DoctrineExtensions\Query\Mysql\Log - log10: DoctrineExtensions\Query\Mysql\Log10 - log2: DoctrineExtensions\Query\Mysql\Log2 - pi: DoctrineExtensions\Query\Mysql\Pi - power: DoctrineExtensions\Query\Mysql\Power - quarter: DoctrineExtensions\Query\Mysql\Quarter - radians: DoctrineExtensions\Query\Mysql\Radians - rand: DoctrineExtensions\Query\Mysql\Rand - round: DoctrineExtensions\Query\Mysql\Round - stddev: DoctrineExtensions\Query\Mysql\StdDev - sin: DoctrineExtensions\Query\Mysql\Sin - std: DoctrineExtensions\Query\Mysql\Std - tan: DoctrineExtensions\Query\Mysql\Tan - variance: DoctrineExtensions\Query\Mysql\Variance - - string_functions: - aes_decrypt: DoctrineExtensions\Query\Mysql\AesDecrypt - aes_encrypt: DoctrineExtensions\Query\Mysql\AesEncrypt - any_value: DoctrineExtensions\Query\Mysql\AnyValue - ascii: DoctrineExtensions\Query\Mysql\Ascii - binary: DoctrineExtensions\Query\Mysql\Binary - cast: DoctrineExtensions\Query\Mysql\Cast - char_length: DoctrineExtensions\Query\Mysql\CharLength - collate: DoctrineExtensions\Query\Mysql\Collate - concat_ws: DoctrineExtensions\Query\Mysql\ConcatWs - countif: DoctrineExtensions\Query\Mysql\CountIf - crc32: DoctrineExtensions\Query\Mysql\Crc32 - degrees: DoctrineExtensions\Query\Mysql\Degrees - field: DoctrineExtensions\Query\Mysql\Field - find_in_set: DoctrineExtensions\Query\Mysql\FindInSet - format: DoctrineExtensions\Query\Mysql\Format - greatest: DoctrineExtensions\Query\Mysql\Greatest - group_concat: DoctrineExtensions\Query\Mysql\GroupConcat - hex: DoctrineExtensions\Query\Mysql\Hex - ifelse: DoctrineExtensions\Query\Mysql\IfElse - ifnull: DoctrineExtensions\Query\Mysql\IfNull - inet_aton: DoctrineExtensions\Query\Mysql\InetAton - inet_ntoa: DoctrineExtensions\Query\Mysql\InetNtoa - inet6_aton: DoctrineExtensions\Query\Mysql\Inet6Aton - inet6_ntoa: DoctrineExtensions\Query\Mysql\Inet6Ntoa - instr: DoctrineExtensions\Query\Mysql\Instr - is_ipv4: DoctrineExtensions\Query\Mysql\IsIpv4 - is_ipv4_compat: DoctrineExtensions\Query\Mysql\IsIpv4Compat - is_ipv4_mapped: DoctrineExtensions\Query\Mysql\IsIpv4Mapped - is_ipv6: DoctrineExtensions\Query\Mysql\IsIpv6 - lag: DoctrineExtensions\Query\Mysql\Lag - lead: DoctrineExtensions\Query\Mysql\Lead - least: DoctrineExtensions\Query\Mysql\Least - lpad: DoctrineExtensions\Query\Mysql\Lpad - match: DoctrineExtensions\Query\Mysql\MatchAgainst - md5: DoctrineExtensions\Query\Mysql\Md5 - nullif: DoctrineExtensions\Query\Mysql\NullIf - over: DoctrineExtensions\Query\Mysql\Over - radians: DoctrineExtensions\Query\Mysql\Radians - regexp: DoctrineExtensions\Query\Mysql\Regexp - replace: DoctrineExtensions\Query\Mysql\Replace - rpad: DoctrineExtensions\Query\Mysql\Rpad - sha1: DoctrineExtensions\Query\Mysql\Sha1 - sha2: DoctrineExtensions\Query\Mysql\Sha2 - soundex: DoctrineExtensions\Query\Mysql\Soundex - str_to_date: DoctrineExtensions\Query\Mysql\StrToDate - substring_index: DoctrineExtensions\Query\Mysql\SubstringIndex - unhex: DoctrineExtensions\Query\Mysql\Unhex - uuid_short: DoctrineExtensions\Query\Mysql\UuidShort diff --git a/config/packages/doctrine/sqlite.yaml b/config/packages/doctrine/sqlite.yaml deleted file mode 100644 index 6388865e..00000000 --- a/config/packages/doctrine/sqlite.yaml +++ /dev/null @@ -1,55 +0,0 @@ -doctrine: - orm: - dql: - datetime_functions: - date: DoctrineExtensions\Query\Sqlite\Date - date_format: DoctrineExtensions\Query\Sqlite\DateFormat - #dateadd: DoctrineExtensions\Query\Sqlite\DateAdd - #datediff: DoctrineExtensions\Query\Sqlite\DateDiff - day: DoctrineExtensions\Query\Sqlite\Day - dayname: DoctrineExtensions\Query\Sqlite\DayName - hour: DoctrineExtensions\Query\Sqlite\Hour - julianday: DoctrineExtensions\Query\Sqlite\JulianDay - minute: DoctrineExtensions\Query\Sqlite\Minute - #month: DoctrineExtensions\Query\Sqlite\Month - #monthname: DoctrineExtensions\Query\Sqlite\MonthName - second: DoctrineExtensions\Query\Sqlite\Second - month: DoctrineExtensions\Query\Sqlite\Month - strftime: DoctrineExtensions\Query\Sqlite\StrfTime - #strtodate: DoctrineExtensions\Query\Sqlite\StrToDate - #time: DoctrineExtensions\Query\Sqlite\Time - #timestampadd: DoctrineExtensions\Query\Sqlite\TimestampAdd - #timestampdiff: DoctrineExtensions\Query\Sqlite\TimestampDiff - week: DoctrineExtensions\Query\Sqlite\Week - weekday: DoctrineExtensions\Query\Sqlite\WeekDay - year: DoctrineExtensions\Query\Sqlite\Year - - - string_functions: -# binary: DoctrineExtensions\Query\Sqlite\Binary -# char_length: DoctrineExtensions\Query\Sqlite\CharLength - concat_ws: DoctrineExtensions\Query\Sqlite\ConcatWs -# countif: DoctrineExtensions\Query\Sqlite\CountIf -# crc32: DoctrineExtensions\Query\Sqlite\Crc32 -# degrees: DoctrineExtensions\Query\Sqlite\Degrees -# field: DoctrineExtensions\Query\Sqlite\Field -# find_in_set: DoctrineExtensions\Query\Sqlite\FindInSet -# group_concat: DoctrineExtensions\Query\Sqlite\GroupConcat - greatest: DoctrineExtensions\Query\Sqlite\Greatest - ifelse: DoctrineExtensions\Query\Sqlite\IfElse - ifnull: DoctrineExtensions\Query\Sqlite\IfNull - least: DoctrineExtensions\Query\Sqlite\Least -# match: DoctrineExtensions\Query\Sqlite\MatchAgainst -# md5: DoctrineExtensions\Query\Sqlite\Md5 -# nullif: DoctrineExtensions\Query\Sqlite\NullIf -# radians: DoctrineExtensions\Query\Sqlite\Radians - random: DoctrineExtensions\Query\Sqlite\Random -# regexp: DoctrineExtensions\Query\Sqlite\Regexp - replace: DoctrineExtensions\Query\Sqlite\Replace -# sha1: DoctrineExtensions\Query\Sqlite\Sha1 -# sha2: DoctrineExtensions\Query\Sqlite\Sha2 -# soundex: DoctrineExtensions\Query\Sqlite\Soundex -# uuid_short: DoctrineExtensions\Query\Sqlite\UuidShort - - numeric_functions: - round: DoctrineExtensions\Query\Sqlite\Round diff --git a/config/services.yaml b/config/services.yaml index afd331ee..0912c8db 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -104,12 +104,6 @@ services: calls: - [ setAnnotationReader, [ "@annotation_reader" ] ] - # make sure, that sqlite supports foreign keys and cascade deletes - App\Doctrine\SqliteSessionInitSubscriber: - class: App\Doctrine\SqliteSessionInitSubscriber - tags: - - { name: doctrine.event_listener, event: postConnect } - # store and retrieve sessions in and from database App\Security\SessionHandler: arguments: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7acbf535..e5fe6f2f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,7 +5,7 @@ xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.1/phpunit.xsd" backupGlobals="false" colors="true" - bootstrap="vendor/autoload.php" + bootstrap="tests/bootstrap.php" > @@ -16,9 +16,25 @@ - + + + + + diff --git a/src/Command/ResetCommand.php b/src/Command/AbstractResetCommand.php similarity index 84% rename from src/Command/ResetCommand.php rename to src/Command/AbstractResetCommand.php index 32056597..2d1e0a32 100644 --- a/src/Command/ResetCommand.php +++ b/src/Command/AbstractResetCommand.php @@ -9,6 +9,7 @@ namespace App\Command; +use Doctrine\ORM\EntityManagerInterface; use Exception; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; @@ -20,23 +21,24 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; /** - * Command used to execute all the basic application bootstrapping AFTER "composer install" was executed. - * - * This command is NOT used during runtime and only meant for developers on their local machines. - * I am too lazy to think about how this could be tested ... and this is one of the rare edge cases where I don't - * feel like it is necessary, so I "cheat" with: + * Base class for all re-installation commands, which are not used during application runtime. * @codeCoverageIgnore */ -class ResetCommand extends Command +abstract class AbstractResetCommand extends Command { /** * @var string */ private $environment; + /** + * @var EntityManagerInterface + */ + protected $entityManager; - public function __construct(string $kernelEnvironment) + public function __construct(string $kernelEnvironment, EntityManagerInterface $entityManager) { $this->environment = $kernelEnvironment; + $this->entityManager = $entityManager; parent::__construct(); } @@ -46,11 +48,11 @@ class ResetCommand extends Command protected function configure() { $this - ->setName('kimai:reset-dev') + ->setName('kimai:reset-' . $this->getEnvName()) ->setDescription('Resets the dev environment') ->setHelp( <<-n switch to skip the question. EOT ) @@ -81,7 +83,8 @@ EOT if ($this->askConfirmation($input, $output, 'Do you want to create the database y/N ?')) { try { $command = $this->getApplication()->find('doctrine:database:create'); - $command->run(new ArrayInput([]), $output); + $options = ['--if-not-exists' => true]; + $command->run(new ArrayInput($options), $output); } catch (Exception $ex) { $io->error('Failed to create database: ' . $ex->getMessage()); @@ -130,12 +133,9 @@ EOT } try { - $command = $this->getApplication()->find('doctrine:fixtures:load'); - $cmdInput = new ArrayInput([]); - $cmdInput->setInteractive(false); - $command->run($cmdInput, $output); + $this->loadData($input, $output); } catch (Exception $ex) { - $io->error('Failed to import fixtures: ' . $ex->getMessage()); + $io->error('Failed to import data: ' . $ex->getMessage()); return 6; } @@ -173,4 +173,8 @@ EOT return $questionHelper->ask($input, $output, $question); } + + abstract protected function getEnvName(): string; + + abstract protected function loadData(InputInterface $input, OutputInterface $output): void; } diff --git a/src/Command/CreateReleaseCommand.php b/src/Command/CreateReleaseCommand.php deleted file mode 100644 index 04041b08..00000000 --- a/src/Command/CreateReleaseCommand.php +++ /dev/null @@ -1,159 +0,0 @@ -rootDir = realpath($projectDirectory); - $this->environment = $kernelEnvironment; - parent::__construct(); - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName('kimai:create-release') - ->setDescription('Create a pre-installed release package') - ->setHelp('This command will create a release package with pre-installed composer, SQLite database and user.') - ->addOption('directory', null, InputOption::VALUE_OPTIONAL, 'Directory where the release package will be stored', '/tmp/') - ->addOption('release', null, InputOption::VALUE_OPTIONAL, 'The version that should be zipped', Constants::VERSION) - ; - } - - /** - * Make sure that this command CANNOT be executed in production. - * - * @return bool - */ - public function isEnabled() - { - return $this->environment !== 'prod'; - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int|null - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $io = new SymfonyStyle($input, $output); - - $directory = $input->getOption('directory'); - - if ($directory[0] === '/') { - $directory = realpath($directory); - } else { - $directory = realpath($this->rootDir . '/' . $directory); - } - - $tmpDir = $directory . '/' . uniqid('kimai_release_'); - - if (!is_dir($directory)) { - $io->error('Given directory is not existing: ' . $directory); - - return 1; - } - - if (is_dir($directory) && !is_writable($directory)) { - $io->error('Cannot write in directory: ' . $directory); - - return 1; - } - - $version = $input->getOption('release'); - - $io->success('Prepare new packages for Kimai ' . $version . ' in ' . $tmpDir); - - $gitCmd = sprintf(self::CLONE_CMD, $version); - $zip = 'kimai-release-' . $version . '.zip'; - - $prefix = 'APP_ENV=prod DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai.sqlite'; - - $commands = [ - 'Clone repository' => $gitCmd . ' ' . $tmpDir, - 'Install composer dependencies' => sprintf('cd %s && %s composer install --no-dev --optimize-autoloader', $tmpDir, $prefix), - 'Create database' => sprintf('cd %s && %s bin/console kimai:install -n', $tmpDir, $prefix), - ]; - - $filesToDelete = [ - '.git*', - '.codecov.yml', - '.editorconfig', - '.php_cs.dist', - 'phpstan.neon', - 'phpunit.xml.dist', - 'webpack.config.js', - // this seems to be required, see https://github.com/kevinpapst/kimai2/issues/1586 - //'assets/', - 'tests/', - 'var/cache/*', - 'var/data/kimai_test.sqlite', - 'var/log/*.log', - 'var/sessions/*', - ]; - - foreach ($filesToDelete as $deleteMe) { - $commands['Delete ' . $deleteMe] = 'cd ' . $tmpDir . ' && rm -rf ' . $deleteMe; - } - - $commands = array_merge($commands, [ - 'Create release zip' => 'cd ' . $tmpDir . ' && zip -q -r ' . $directory . '/' . $zip . ' .', - 'Remove tmp directory' => 'rm -rf ' . $tmpDir, - ]); - - $exitCode = 0; - foreach ($commands as $title => $command) { - passthru($command, $exitCode); - if ($exitCode !== 0) { - $io->error('Failed with command: ' . $command); - - return -1; - } else { - $io->success($title); - } - } - - $io->success( - 'New release package available at: ' . PHP_EOL . - $directory . '/' . $zip - ); - - return 0; - } -} diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 83912c7b..72ff048f 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -24,6 +24,8 @@ use Symfony\Component\HttpKernel\KernelInterface; /** * Command used to do the basic installation steps for Kimai. + * + * @codeCoverageIgnore */ final class InstallCommand extends Command { @@ -156,10 +158,7 @@ final class InstallCommand extends Command throw new \Exception('Skipped database creation, aborting installation'); } - $options = []; - if ($this->connection->getDatabasePlatform()->getName() !== 'sqlite') { - $options = ['--if-not-exists' => true]; - } + $options = ['--if-not-exists' => true]; $command = $this->getApplication()->find('doctrine:database:create'); $result = $command->run(new ArrayInput($options), $output); diff --git a/src/Command/ResetDevelopmentCommand.php b/src/Command/ResetDevelopmentCommand.php new file mode 100644 index 00000000..7269568f --- /dev/null +++ b/src/Command/ResetDevelopmentCommand.php @@ -0,0 +1,38 @@ +getApplication()->find('doctrine:fixtures:load'); + $cmdInput = new ArrayInput([]); + $cmdInput->setInteractive(false); + $command->run($cmdInput, $output); + } +} diff --git a/src/Command/ResetTestCommand.php b/src/Command/ResetTestCommand.php new file mode 100644 index 00000000..d017be91 --- /dev/null +++ b/src/Command/ResetTestCommand.php @@ -0,0 +1,137 @@ +setName('Test'); + $activity->setComment('Test comment'); + $activity->setVisible(true); + $activity->setTimeBudget(100000); + $activity->setBudget(1000); + $this->entityManager->persist($activity); + + $customer = new Customer(); + $customer->setNumber('1'); + $customer->setComment('Test comment'); + $customer->setContact('Test'); + $customer->setAddress('Test'); + $customer->setCompany('Test'); + $customer->setName('Test'); + $customer->setCountry('DE'); + $customer->setCurrency('EUR'); + $customer->setPhone('111'); + $customer->setFax('222'); + $customer->setMobile('333'); + $customer->setEmail('test@example.com'); + $customer->setTimeBudget(100000); + $customer->setBudget(1000); + $customer->setTimezone('Europe/Berlin'); + $this->entityManager->persist($customer); + + $project = new Project(); + $project->setComment('Test comment'); + $project->setName('Test'); + $project->setOrderNumber('111'); + $project->setTimeBudget(100000); + $project->setBudget(1000); + $project->setCustomer($customer); + $this->entityManager->persist($project); + + $users = [ + // 0=id, 1=hourly rate, 2=Alias, 3=registration date, 4=title, 5=avatar, 6=enabled, 7=password, 8=roles, 9=username, 10=username canonical, 11=email, 12=email canonical, 13=salt, 14=last login, 15=confirmation token, 16=password requested at, 17=api_token + [1, 53, 'Clara Haynes', '2018-02-06 23:28:57', 'CFO', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y', 1, '$2y$04$kKBYJ8sKCOhhakCjm9sCp.TQdwLTS1FPkPiWn2KBmaCA7xFL0NA42', ['ROLE_CUSTOMER'], 'clara_customer', 'clara_customer', 'clara_customer@example.com', 'clara_customer@example.com', null, null, null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + [2, 82, 'John Doe', '2018-02-06 23:28:57', 'Developer', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', 1, '$2y$04$36P/xyhP6FbnfFYbXy7V0.ioSe8HjMlJQFYnlIzz2T6Agfi8ob6jK', [], 'john_user', 'john_user', 'john_user@example.com', 'john_user@example.com', null, null, null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + [3, 35, 'Chris Deactive', '2018-02-06 23:28:57', 'Developer (left company)', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', 0, '$2y$04$MLtQBZ9JLzWu1Y01QnNjsuoLm8qC9XRkpUywf6DIbpd9OAL1mEcCi', [], 'chris_user', 'chris_user', 'chris_user@example.com', 'chris_user@example.com', null, null, null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + [4, 35, 'Tony Maier', '2018-02-06 23:28:57', 'Head of Development', 'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg', 1, '$2y$04$rqxiiExfUVzIYRVL2x4JJumQWNPIG6PazXwrSJm/VQFEesR08Uj5i', ['ROLE_TEAMLEAD'], 'tony_teamlead', 'tony_teamlead', 'tony_teamlead@example.com', 'tony_teamlead@example.com', null, null, null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + [5, 81, 'Anna Smith', '2018-02-06 23:28:57', 'Administrator', null, 1, '$2y$04$ct/rVb.naDzYZECnvfTJ2uns/zPHv8.8KcunhTjYFwWQeg1dywI8G', ['ROLE_ADMIN'], 'anna_admin', 'anna_admin', 'anna_admin@example.com', 'anna_admin@example.com', null, null, null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + [6, 46, null, '2018-02-06 23:28:57', 'Super Administrator', '/bundles/avanzuadmintheme/img/avatar.png', 1, '$2y$04$kuhEEPw/CBMYc3x7SOv27eC1hQSmrtFvgJI2ULRuJeddAVDyrPKJ2', ['ROLE_SUPER_ADMIN'], 'susan_super', 'susan_super', 'susan_super@example.com', 'susan_super@example.com', null, '2020-04-14 09:50:38', null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + [7, null, 'Test User 1', null, 'Quality Tester 1', null, 1, '$2y$04$kuhEEPw/CBMYc3x7SOv27eC1hQSmrtFvgJI2ULRuJeddAVDyrPKJ2', [], 'test_user_1', 'test_user_1', 'test_user_1@example.com', 'test_user_1@example.com', null, null, null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + [8, null, 'Test User 2', null, 'Quality Tester 2', null, 1, '$2y$04$kuhEEPw/CBMYc3x7SOv27eC1hQSmrtFvgJI2ULRuJeddAVDyrPKJ2', [], 'test_user_2', 'test_user_2', 'test_user_2@example.com', 'test_user_2@example.com', null, null, null, null, '$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO'], + ]; + + $userEntities = []; + foreach ($users as $userConf) { + $user = new User(); + if ($userConf[1] !== null) { + $user->setPreferenceValue(UserPreference::HOURLY_RATE, $userConf[1]); + } + if ($userConf[2] !== null) { + $user->setAlias($userConf[2]); + } + if ($userConf[3] !== null) { + $user->setRegisteredAt(new \DateTime($userConf[3])); + } + if ($userConf[4] !== null) { + $user->setTitle($userConf[4]); + } + if ($userConf[5] !== null) { + $user->setAvatar($userConf[5]); + } + if ($userConf[6] !== null) { + $user->setEnabled((bool) $userConf[6]); + } + $user->setPassword($userConf[7]); + if ($userConf[8] !== null && !empty($userConf[8])) { + $user->setRoles($userConf[8]); + } else { + $user->setRoles(['ROLE_USER']); + } + $user->setUsername($userConf[9]); + if ($userConf[10] !== null) { + $user->setUsernameCanonical($userConf[10]); + } + if ($userConf[11] !== null) { + $user->setEmail($userConf[11]); + } + if ($userConf[12] !== null) { + $user->setEmailCanonical($userConf[12]); + } + if ($userConf[17] !== null) { + $user->setApiToken($userConf[17]); + } + + $this->entityManager->persist($user); + $userEntities[] = $user; + } + + $team = new Team(); + $team->setName('Test team'); + $team->setTeamLead($userEntities[6]); + $team->addUser($userEntities[7]); + $this->entityManager->persist($team); + + $this->entityManager->flush(); + } +} diff --git a/src/DependencyInjection/Compiler/DoctrineCompilerPass.php b/src/DependencyInjection/Compiler/DoctrineCompilerPass.php deleted file mode 100644 index cb0e67a4..00000000 --- a/src/DependencyInjection/Compiler/DoctrineCompilerPass.php +++ /dev/null @@ -1,134 +0,0 @@ - 'mysql', - 'sqlite' => 'sqlite', - ]; - - private function getEnvVar(string $name): ?string - { - $envVarValue = null; - - if (isset($_ENV[$name])) { - $envVarValue = $_ENV[$name]; - } - - if ($envVarValue === null && isset($_SERVER[$name])) { - $envVarValue = $_SERVER[$name]; - } - - if ($envVarValue === null) { - $envVarValue = getenv($name); - } - - if ($envVarValue === false || empty($envVarValue)) { - return null; - } - - return $envVarValue; - } - - /** - * @return string - * @throws \Exception - */ - private function findEngine(): string - { - $engine = null; - - if (null !== ($databaseUrl = $this->getEnvVar('DATABASE_URL'))) { - $urlParts = explode('://', $databaseUrl); - $engine = $urlParts[0] ?: null; - } - - if ($engine === null) { - $engine = $this->getEnvVar('DATABASE_ENGINE'); - } - - if ($engine === null) { - throw new \Exception( - 'Could not detect database engine, make sure DATABASE_URL is available from $_SERVER or $_ENV. Check your .env file.' - ); - } - - if (!\array_key_exists($engine, $this->allowedEngines)) { - throw new \Exception( - 'Unsupported database engine: ' . $engine . '. Kimai only supports one of: ' . - implode(', ', array_keys($this->allowedEngines)) - ); - } - - return $this->allowedEngines[$engine]; - } - - /** - * @param ContainerBuilder $container - * @return string - * @throws \Exception - */ - protected function getConfigFile(ContainerBuilder $container) - { - $engine = $this->findEngine(); - - $configDir = realpath( - $container->getParameter('kernel.project_dir') . '/config/packages/doctrine/' - ); - - $configFile = $configDir . '/' . $engine . '.yaml'; - - if (!file_exists($configFile)) { - throw new \Exception('Could not find config file for database engine. Looked at ' . $configFile); - } - - return $configFile; - } - - /** - * @param ContainerBuilder $container - * @throws \Exception - */ - public function process(ContainerBuilder $container) - { - $configFile = $this->getConfigFile($container); - $config = Yaml::parse(file_get_contents($configFile)); - - if (!isset($config['doctrine']['orm']['dql']) || empty($config['doctrine']['orm']['dql'])) { - throw new \Exception('could not load custom Doctrine functions from: ' . $configFile); - } - - $sql = $config['doctrine']['orm']['dql']; - - $ormConfig = $container->getDefinition('doctrine.orm.default_configuration'); - - foreach ($sql['string_functions'] as $name => $function) { - $ormConfig->addMethodCall('addCustomStringFunction', [$name, $function]); - } - foreach ($sql['numeric_functions'] as $name => $function) { - $ormConfig->addMethodCall('addCustomNumericFunction', [$name, $function]); - } - foreach ($sql['datetime_functions'] as $name => $function) { - $ormConfig->addMethodCall('addCustomDatetimeFunction', [$name, $function]); - } - } -} diff --git a/src/Doctrine/AbstractMigration.php b/src/Doctrine/AbstractMigration.php index e85d49f8..81504135 100644 --- a/src/Doctrine/AbstractMigration.php +++ b/src/Doctrine/AbstractMigration.php @@ -55,29 +55,29 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai } /** - * Whether we should deactivate foreign key support for SQLite. - * This is required, if columns are changed. - * SQLite will drop the table and all referenced data, if we don't deactivate this. - * - * @return bool + * @deprecated since 1.14 - will be removed with 2.0 */ protected function isSupportingForeignKeys(): bool { + @trigger_error('isSupportingForeignKeys() is deprecated and will be removed with 2.0', E_USER_DEPRECATED); + return true; } + /** + * @deprecated since 1.14 - will be removed with 2.0 + */ protected function deactivateForeignKeysOnSqlite() { - if ($this->isPlatformSqlite() && !$this->isSupportingForeignKeys()) { - $this->addSql('PRAGMA foreign_keys = OFF;'); - } + @trigger_error('deactivateForeignKeysOnSqlite() is deprecated and will be removed with 2.0', E_USER_DEPRECATED); } + /** + * @deprecated since 1.14 - will be removed with 2.0 + */ private function activateForeignKeysOnSqlite() { - if ($this->isPlatformSqlite() && !$this->isSupportingForeignKeys()) { - $this->addSql('PRAGMA foreign_keys = ON;'); - } + @trigger_error('activateForeignKeysOnSqlite() is deprecated and will be removed with 2.0', E_USER_DEPRECATED); } /** @@ -87,16 +87,6 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai public function preUp(Schema $schema): void { $this->abortIfPlatformNotSupported(); - $this->deactivateForeignKeysOnSqlite(); - } - - /** - * @param Schema $schema - * @throws DBALException - */ - public function postUp(Schema $schema): void - { - $this->activateForeignKeysOnSqlite(); } /** @@ -106,16 +96,6 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai public function preDown(Schema $schema): void { $this->abortIfPlatformNotSupported(); - $this->deactivateForeignKeysOnSqlite(); - } - - /** - * @param Schema $schema - * @throws DBALException - */ - public function postDown(Schema $schema): void - { - $this->activateForeignKeysOnSqlite(); } /** @@ -126,25 +106,22 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai protected function abortIfPlatformNotSupported() { $platform = $this->getPlatform(); - if (!\in_array($platform, ['sqlite', 'mysql'])) { + if (!$this->isPlatformMysql()) { $this->abortIf(true, 'Unsupported database platform: ' . $platform); } } /** - * @return bool - * @throws DBALException + * @deprecated since 1.14 - will be removed with 2.0 */ - protected function isPlatformSqlite() + protected function isPlatformSqlite(): bool { + @trigger_error('isPlatformSqlite() is deprecated and will be removed with 2.0', E_USER_DEPRECATED); + return ($this->getPlatform() === 'sqlite'); } - /** - * @return bool - * @throws DBALException - */ - protected function isPlatformMysql() + protected function isPlatformMysql(): bool { return ($this->getPlatform() === 'mysql'); } @@ -173,19 +150,12 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai } /** - * we do it via addSql instead of $schema->getTable($users)->dropIndex() - * otherwise the commands will be executed as last ones. - * - * @param string $indexName - * @param string $tableName - * @throws DBALException + * @deprecated since 1.14 - will be removed with 2.0 */ protected function addSqlDropIndex($indexName, $tableName) { - $dropSql = 'DROP INDEX ' . $indexName; - if (!$this->isPlatformSqlite()) { - $dropSql .= ' ON ' . $tableName; - } - $this->addSql($dropSql); + @trigger_error('addSqlDropIndex() is deprecated and will be removed with 2.0', E_USER_DEPRECATED); + + $this->addSql('DROP INDEX ' . $indexName . ' ON ' . $tableName); } } diff --git a/src/Doctrine/SqliteSessionInitSubscriber.php b/src/Doctrine/SqliteSessionInitSubscriber.php deleted file mode 100644 index 1e05b606..00000000 --- a/src/Doctrine/SqliteSessionInitSubscriber.php +++ /dev/null @@ -1,40 +0,0 @@ -getConnection()->getDatabasePlatform()->getName())) { - return; - } - - $args->getConnection()->exec('PRAGMA foreign_keys = ON;'); - } -} diff --git a/src/Kernel.php b/src/Kernel.php index 0d0b8bb6..edbfa62a 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -10,7 +10,6 @@ namespace App; use App\DependencyInjection\AppExtension; -use App\DependencyInjection\Compiler\DoctrineCompilerPass; use App\DependencyInjection\Compiler\ExportServiceCompilerPass; use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass; use App\DependencyInjection\Compiler\TwigContextCompilerPass; @@ -198,7 +197,6 @@ class Kernel extends BaseKernel $loader->load($confDir . '/services-*' . self::CONFIG_EXTS, 'glob'); $loader->load($confDir . '/services_' . $this->environment . self::CONFIG_EXTS, 'glob'); - $container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000); $container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000); $container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000); $container->addCompilerPass(new ExportServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000); diff --git a/src/Migrations/Version20180701120000.php b/src/Migrations/Version20180701120000.php index a02749fc..6ed54128 100644 --- a/src/Migrations/Version20180701120000.php +++ b/src/Migrations/Version20180701120000.php @@ -30,38 +30,18 @@ final class Version20180701120000 extends AbstractMigration $timesheets = 'kimai2_timesheet'; $invoiceTemplates = 'kimai2_invoice_templates'; - if ($this->isPlatformSqlite()) { - $this->addSql('CREATE TABLE ' . $users . ' (id INTEGER NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active BOOLEAN NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles CLOB NOT NULL --(DC2Type:array) - , PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 ON ' . $users . ' (name)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 ON ' . $users . ' (mail)'); - $this->addSql('CREATE TABLE ' . $userPreferences . ' (id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_8D08F631A76ED395 ON ' . $userPreferences . ' (user_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 ON ' . $userPreferences . ' (user_id, name)'); - $this->addSql('CREATE TABLE ' . $customers . ' (id INTEGER NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address CLOB DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER NOT NULL, customer_id INTEGER DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)'); - $this->addSql('CREATE TABLE ' . $activities . ' (id INTEGER NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(150) NOT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $activities . ' (project_id)'); - $this->addSql('CREATE TABLE ' . $timesheets . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheets . ' (user)'); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheets . ' (activity_id)'); - $this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INTEGER NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address CLOB DEFAULT NULL, due_days INTEGER NOT NULL, vat INTEGER DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms CLOB DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1626CFE95E237E06 ON ' . $invoiceTemplates . ' (name)'); - } else { - $this->addSql('CREATE TABLE ' . $users . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('CREATE TABLE ' . $userPreferences . ' (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('CREATE TABLE ' . $customers . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('CREATE TABLE ' . $projects . ' (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('CREATE TABLE ' . $activities . ' (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('CREATE TABLE ' . $timesheets . ' (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('ALTER TABLE ' . $userPreferences . ' ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES ' . $users . ' (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE ' . $activities . ' ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id)'); - $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE'); - } + $this->addSql('CREATE TABLE ' . $users . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE TABLE ' . $userPreferences . ' (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE TABLE ' . $customers . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE TABLE ' . $projects . ' (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE TABLE ' . $activities . ' (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE TABLE ' . $timesheets . ' (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('ALTER TABLE ' . $userPreferences . ' ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES ' . $users . ' (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE ' . $activities . ' ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id)'); + $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE'); } public function down(Schema $schema): void diff --git a/src/Migrations/Version20180715160326.php b/src/Migrations/Version20180715160326.php index bdb6fcfe..f8ebe923 100644 --- a/src/Migrations/Version20180715160326.php +++ b/src/Migrations/Version20180715160326.php @@ -44,22 +44,13 @@ final class Version20180715160326 extends AbstractMigration foreach ($indexesOld as $index) { if (\in_array('name', $index->getColumns()) || \in_array('mail', $index->getColumns())) { $this->indexesOld[] = $index; - $this->addSqlDropIndex($index->getName(), $users); + $this->addSql('DROP INDEX ' . $index->getName() . ' ON ' . $users); } } - if ($this->isPlatformSqlite()) { - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $users . ' AS SELECT id, name, mail, password, alias, active, registration_date, title, avatar, roles FROM ' . $users); - $this->addSql('DROP TABLE ' . $users); - $this->addSql('CREATE TABLE ' . $users . ' (id INTEGER NOT NULL, alias VARCHAR(60) DEFAULT NULL COLLATE BINARY, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL COLLATE BINARY, avatar VARCHAR(255) DEFAULT NULL COLLATE BINARY, enabled BOOLEAN NOT NULL, password VARCHAR(255) NOT NULL, roles CLOB NOT NULL --(DC2Type:array) - , username VARCHAR(180) NOT NULL, username_canonical VARCHAR(180) NOT NULL, email VARCHAR(180) NOT NULL, email_canonical VARCHAR(180) NOT NULL, salt VARCHAR(255) DEFAULT NULL, last_login DATETIME DEFAULT NULL, confirmation_token VARCHAR(180) DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('INSERT INTO ' . $users . ' (id, username, username_canonical, email, email_canonical, password, alias, enabled, registration_date, title, avatar, roles) SELECT id, name, name, mail, mail, password, alias, active, registration_date, title, avatar, roles FROM __temp__' . $users); - $this->addSql('DROP TABLE __temp__' . $users); - } else { - $this->addSql('ALTER TABLE ' . $users . ' CHANGE name username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, CHANGE mail email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, CHANGE password password VARCHAR(255) NOT NULL, CHANGE alias alias VARCHAR(60) DEFAULT NULL, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE active enabled TINYINT(1) NOT NULL'); - $this->addSql('UPDATE ' . $users . ' set username_canonical = username'); - $this->addSql('UPDATE ' . $users . ' set email_canonical = email'); - } + $this->addSql('ALTER TABLE ' . $users . ' CHANGE name username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, CHANGE mail email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, CHANGE password password VARCHAR(255) NOT NULL, CHANGE alias alias VARCHAR(60) DEFAULT NULL, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE active enabled TINYINT(1) NOT NULL'); + $this->addSql('UPDATE ' . $users . ' set username_canonical = username'); + $this->addSql('UPDATE ' . $users . ' set email_canonical = email'); $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:16:"ROLE_SUPER_ADMIN";}\' WHERE roles LIKE "%ROLE_SUPER_ADMIN%"'); $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:10:"ROLE_ADMIN";}\' WHERE roles LIKE "%ROLE_ADMIN%"'); @@ -84,20 +75,11 @@ final class Version20180715160326 extends AbstractMigration $users = 'kimai2_users'; $indexToDelete = ['UNIQ_B9AC5BCE92FC23A8', 'UNIQ_B9AC5BCEA0D96FBF', 'UNIQ_B9AC5BCEC05FB297', 'UNIQ_B9AC5BCEF85E0677', 'UNIQ_B9AC5BCEE7927C74']; - foreach ($indexToDelete as $index) { - $this->addSqlDropIndex($index, $users); + foreach ($indexToDelete as $indexName) { + $this->addSql('DROP INDEX ' . $indexName . ' ON ' . $users); } - if ($this->isPlatformSqlite()) { - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $users . ' AS SELECT id, username, email, enabled, password, roles, alias, registration_date, title, avatar FROM ' . $users); - $this->addSql('DROP TABLE ' . $users); - $this->addSql('CREATE TABLE ' . $users . ' (id INTEGER NOT NULL, alias VARCHAR(60) DEFAULT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, active BOOLEAN NOT NULL, password VARCHAR(254) DEFAULT NULL COLLATE BINARY, roles CLOB NOT NULL COLLATE BINARY --(DC2Type:array) - , name VARCHAR(60) NOT NULL COLLATE BINARY, mail VARCHAR(160) NOT NULL COLLATE BINARY, PRIMARY KEY(id))'); - $this->addSql('INSERT INTO ' . $users . ' (id, name, mail, active, password, roles, alias, registration_date, title, avatar) SELECT id, username, email, enabled, password, roles, alias, registration_date, title, avatar FROM __temp__' . $users); - $this->addSql('DROP TABLE __temp__' . $users); - } else { - $this->addSql('ALTER TABLE ' . $users . ' CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE alias alias VARCHAR(60) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL'); - } + $this->addSql('ALTER TABLE ' . $users . ' CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE alias alias VARCHAR(60) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL'); $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_SUPER_ADMIN"]\' WHERE roles LIKE "%ROLE_SUPER_ADMIN%"'); $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_ADMIN"]\' WHERE roles LIKE "%ROLE_ADMIN%"'); diff --git a/src/Migrations/Version20180730044139.php b/src/Migrations/Version20180730044139.php index 6059a8ef..737ded9b 100644 --- a/src/Migrations/Version20180730044139.php +++ b/src/Migrations/Version20180730044139.php @@ -37,20 +37,8 @@ final class Version20180730044139 extends AbstractMigration $user = 'kimai2_users'; $activity = 'kimai2_activities'; - if ($this->isPlatformSqlite()) { - $this->addSql('DROP INDEX IDX_4F60C6B181C06096'); - $this->addSql('DROP INDEX IDX_4F60C6B18D93D649'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM ' . $timesheet); - $this->addSql('DROP TABLE ' . $timesheet); - $this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activity . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM __temp__' . $timesheet); - $this->addSql('DROP TABLE __temp__' . $timesheet); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); - } else { - $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649'); - $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE'); - } + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE'); } /** @@ -62,19 +50,7 @@ final class Version20180730044139 extends AbstractMigration $timesheet = 'kimai2_timesheet'; $user = 'kimai2_users'; - if ($this->isPlatformSqlite()) { - $this->addSql('DROP INDEX IDX_4F60C6B18D93D649'); - $this->addSql('DROP INDEX IDX_4F60C6B181C06096'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM ' . $timesheet); - $this->addSql('DROP TABLE ' . $timesheet); - $this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM __temp__' . $timesheet); - $this->addSql('DROP TABLE __temp__' . $timesheet); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); - } else { - $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649'); - $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id)'); - } + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id)'); } } diff --git a/src/Migrations/Version20180924111853.php b/src/Migrations/Version20180924111853.php index 2efc268a..8844411d 100644 --- a/src/Migrations/Version20180924111853.php +++ b/src/Migrations/Version20180924111853.php @@ -25,35 +25,14 @@ final class Version20180924111853 extends AbstractMigration { $invoiceTemplates = 'kimai2_invoice_templates'; - if ($this->isPlatformSqlite()) { - $this->addSql('UPDATE ' . $invoiceTemplates . ' SET name=substr(name, 1, 60)'); - $this->addSql('DROP INDEX UNIQ_1626CFE95E237E06'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $invoiceTemplates . ' AS SELECT id, name, title, company, address, due_days, vat, calculator, number_generator, renderer, payment_terms FROM ' . $invoiceTemplates); - $this->addSql('DROP TABLE ' . $invoiceTemplates); - $this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL COLLATE BINARY, company VARCHAR(255) NOT NULL COLLATE BINARY, address CLOB DEFAULT NULL COLLATE BINARY, due_days INTEGER NOT NULL, calculator VARCHAR(20) NOT NULL COLLATE BINARY, number_generator VARCHAR(20) NOT NULL COLLATE BINARY, renderer VARCHAR(20) NOT NULL COLLATE BINARY, payment_terms CLOB DEFAULT NULL COLLATE BINARY, name VARCHAR(60) NOT NULL, vat DOUBLE PRECISION DEFAULT 0)'); - $this->addSql('INSERT INTO ' . $invoiceTemplates . ' (id, name, title, company, address, due_days, vat, calculator, number_generator, renderer, payment_terms) SELECT id, name, title, company, address, due_days, vat, calculator, number_generator, renderer, payment_terms FROM __temp__' . $invoiceTemplates); - $this->addSql('DROP TABLE __temp__' . $invoiceTemplates); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1626CFE95E237E06 ON ' . $invoiceTemplates . ' (name)'); - } else { - $this->addSql('UPDATE ' . $invoiceTemplates . ' SET name=SUBSTRING(name, 1, 60)'); - $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(60) NOT NULL, CHANGE vat vat DOUBLE PRECISION DEFAULT 0'); - } + $this->addSql('UPDATE ' . $invoiceTemplates . ' SET name=SUBSTRING(name, 1, 60)'); + $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(60) NOT NULL, CHANGE vat vat DOUBLE PRECISION DEFAULT 0'); } public function down(Schema $schema): void { $invoiceTemplates = 'kimai2_invoice_templates'; - if ($this->isPlatformSqlite()) { - $this->addSql('DROP INDEX UNIQ_1626CFE95E237E06'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $invoiceTemplates . ' AS SELECT id, name, title, company, address, due_days, vat, calculator, number_generator, renderer, payment_terms FROM ' . $invoiceTemplates); - $this->addSql('DROP TABLE ' . $invoiceTemplates); - $this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address CLOB DEFAULT NULL, due_days INTEGER NOT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms CLOB DEFAULT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, vat INTEGER DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $invoiceTemplates . ' (id, name, title, company, address, due_days, vat, calculator, number_generator, renderer, payment_terms) SELECT id, name, title, company, address, due_days, vat, calculator, number_generator, renderer, payment_terms FROM __temp__' . $invoiceTemplates); - $this->addSql('DROP TABLE __temp__' . $invoiceTemplates); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1626CFE95E237E06 ON ' . $invoiceTemplates . ' (name)'); - } else { - $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(255) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE vat vat INT DEFAULT NULL'); - } + $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(255) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE vat vat INT DEFAULT NULL'); } } diff --git a/src/Migrations/Version20181031220003.php b/src/Migrations/Version20181031220003.php index 18cfabc1..307aee78 100644 --- a/src/Migrations/Version20181031220003.php +++ b/src/Migrations/Version20181031220003.php @@ -28,60 +28,24 @@ final class Version20181031220003 extends AbstractMigration $users = 'kimai2_users'; $customers = 'kimai2_customers'; - if ($this->isPlatformSqlite()) { - // project table - $this->addSql('DROP INDEX IDX_407F12069395C3F3'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $projects . ' AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects); - $this->addSql('DROP TABLE ' . $projects); - $this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, order_number CLOB DEFAULT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__' . $projects); - $this->addSql('DROP TABLE __temp__' . $projects); - $this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)'); - // timesheet table - $this->addSql('DROP INDEX IDX_4F60C6B18D93D649'); - $this->addSql('DROP INDEX IDX_4F60C6B181C06096'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet); - $this->addSql('DROP TABLE ' . $timesheet); - $this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, project_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet); - $this->addSql('DROP TABLE __temp__' . $timesheet); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); - $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); - } else { - // project table - $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3'); - $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL'); - $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); - // timesheet table - $this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id'); - $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); - } + // project table + $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3'); + $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL'); + $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); + // timesheet table + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id'); + $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); // update timesheet table and insert project_id from activity table $this->addSql('UPDATE ' . $timesheet . ' SET project_id = (SELECT project_id FROM ' . $activities . ' WHERE id = activity_id)'); // now update the timesheet table and disallow null values for all required columns (that was a bug before) - if ($this->isPlatformSqlite()) { - $this->addSql('DROP INDEX IDX_4F60C6B181C06096'); - $this->addSql('DROP INDEX IDX_4F60C6B18D93D649'); - $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet); - $this->addSql('DROP TABLE ' . $timesheet); - $this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet); - $this->addSql('DROP TABLE __temp__' . $timesheet); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); - $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); - } else { - $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649'); - $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B181C06096'); - $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL'); - $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE'); - } + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649'); + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B181C06096'); + $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE'); } public function down(Schema $schema): void @@ -90,35 +54,13 @@ final class Version20181031220003 extends AbstractMigration $projects = 'kimai2_projects'; $customers = 'kimai2_customers'; - if ($this->isPlatformSqlite()) { - // project table - $this->addSql('DROP INDEX IDX_407F12069395C3F3'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $projects . ' AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects); - $this->addSql('DROP TABLE ' . $projects); - $this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, customer_id INTEGER DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__' . $projects); - $this->addSql('DROP TABLE __temp__' . $projects); - $this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)'); - // timesheet table - $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C'); - $this->addSql('DROP INDEX IDX_4F60C6B18D93D649'); - $this->addSql('DROP INDEX IDX_4F60C6B181C06096'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet); - $this->addSql('DROP TABLE ' . $timesheet); - $this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet); - $this->addSql('DROP TABLE __temp__' . $timesheet); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); - } else { - // project table - $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3'); - $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); - // timesheet table - $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C'); - $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet); - $this->addSql('ALTER TABLE ' . $timesheet . ' DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL'); - } + // project table + $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3'); + $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); + // timesheet table + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C'); + $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet); + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL'); } } diff --git a/src/Migrations/Version20190201150324.php b/src/Migrations/Version20190201150324.php index 876bdb0a..8db8e7bd 100644 --- a/src/Migrations/Version20190201150324.php +++ b/src/Migrations/Version20190201150324.php @@ -26,11 +26,7 @@ final class Version20190201150324 extends AbstractMigration { $timezone = date_default_timezone_get(); - if ($this->isPlatformSqlite()) { - $this->addSql('ALTER TABLE kimai2_timesheet ADD COLUMN timezone VARCHAR(64) DEFAULT NULL'); - } else { - $this->addSql('ALTER TABLE kimai2_timesheet ADD timezone VARCHAR(64) NOT NULL'); - } + $this->addSql('ALTER TABLE kimai2_timesheet ADD timezone VARCHAR(64) NOT NULL'); $this->addSql('UPDATE kimai2_timesheet SET timezone = "' . $timezone . '"'); } diff --git a/src/Migrations/Version20190305152308.php b/src/Migrations/Version20190305152308.php index 02f7e267..d5481a37 100644 --- a/src/Migrations/Version20190305152308.php +++ b/src/Migrations/Version20190305152308.php @@ -16,7 +16,6 @@ use Doctrine\DBAL\Schema\Schema; /** * - rename mail to email in customer table - * - introducing foreign keys in SQLite tables * - converts all decimal to float values, as decimals are treated as string in PHP: * https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#decimal * @@ -32,54 +31,10 @@ final class Version20190305152308 extends AbstractMigration $timesheet = 'kimai2_timesheet'; $users = 'kimai2_users'; - if ($this->isPlatformSqlite()) { - // first backup of ALL tables - $this->addSql('DROP INDEX IDX_4F60C6B181C06096'); - $this->addSql('DROP INDEX IDX_4F60C6B18D93D649'); - $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C'); - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_timesheet AS SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM ' . $timesheet); - - $this->addSql('DROP INDEX IDX_8811FE1C166D1F9C'); - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_activities AS SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM ' . $activities); - - $this->addSql('DROP INDEX IDX_407F12069395C3F3'); - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_projects AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects); - - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_customers AS SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixed_rate, hourly_rate FROM ' . $customers); - - // now we can drop and re-create the tables - $this->addSql('DROP TABLE ' . $customers); - $this->addSql('CREATE TABLE ' . $customers . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, number VARCHAR(50) DEFAULT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL COLLATE BINARY, contact VARCHAR(255) DEFAULT NULL COLLATE BINARY, address CLOB DEFAULT NULL COLLATE BINARY, country VARCHAR(2) NOT NULL COLLATE BINARY, currency VARCHAR(3) NOT NULL COLLATE BINARY, phone VARCHAR(255) DEFAULT NULL COLLATE BINARY, fax VARCHAR(255) DEFAULT NULL COLLATE BINARY, mobile VARCHAR(255) DEFAULT NULL COLLATE BINARY, email VARCHAR(255) DEFAULT NULL COLLATE BINARY, homepage VARCHAR(255) DEFAULT NULL COLLATE BINARY, timezone VARCHAR(255) NOT NULL COLLATE BINARY, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $customers . ' (id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixed_rate, hourly_rate) SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixed_rate, hourly_rate FROM __temp__kimai2_customers'); - $this->addSql('DROP TABLE __temp__kimai2_customers'); - - $this->addSql('DROP TABLE ' . $projects); - $this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, order_number CLOB DEFAULT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, budget DOUBLE PRECISION NOT NULL, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL, CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__kimai2_projects'); - $this->addSql('DROP TABLE __temp__kimai2_projects'); - $this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)'); - - $this->addSql('DROP TABLE ' . $activities); - $this->addSql('CREATE TABLE ' . $activities . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL, CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $activities . ' (id, project_id, name, comment, visible, fixed_rate, hourly_rate) SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM __temp__kimai2_activities'); - $this->addSql('DROP TABLE __temp__kimai2_activities'); - $this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $activities . ' (project_id)'); - - $this->addSql('DROP TABLE ' . $timesheet); - $this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL --(DC2Type:datetime) - , timezone VARCHAR(64) NOT NULL COLLATE BINARY, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, exported BOOLEAN NOT NULL, end_time DATETIME DEFAULT NULL --(DC2Type:datetime) - , rate DOUBLE PRECISION NOT NULL, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported) SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM __temp__kimai2_timesheet'); - $this->addSql('DROP TABLE __temp__kimai2_timesheet'); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); - $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); - } else { - $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); - $this->addSql('ALTER TABLE ' . $customers . ' CHANGE mail email VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); - $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); - $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); - } + $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $customers . ' CHANGE mail email VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL'); } public function down(Schema $schema): void @@ -89,53 +44,9 @@ final class Version20190305152308 extends AbstractMigration $activities = 'kimai2_activities'; $timesheet = 'kimai2_timesheet'; - if ($this->isPlatformSqlite()) { - // first backup of ALL tables - $this->addSql('DROP INDEX IDX_8811FE1C166D1F9C'); - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_activities AS SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM ' . $activities); - - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_customers AS SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixed_rate, hourly_rate FROM ' . $customers); - - $this->addSql('DROP INDEX IDX_407F12069395C3F3'); - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_projects AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects); - - $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C'); - $this->addSql('DROP INDEX IDX_4F60C6B18D93D649'); - $this->addSql('DROP INDEX IDX_4F60C6B181C06096'); - $this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_timesheet AS SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM ' . $timesheet); - - // now we can drop and re-create the tables - $this->addSql('DROP TABLE ' . $activities); - $this->addSql('CREATE TABLE ' . $activities . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $activities . ' (id, project_id, name, comment, visible, fixed_rate, hourly_rate) SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM __temp__kimai2_activities'); - $this->addSql('DROP TABLE __temp__kimai2_activities'); - $this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $activities . ' (project_id)'); - - $this->addSql('DROP TABLE ' . $customers); - $this->addSql('CREATE TABLE ' . $customers . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address CLOB DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $customers . ' (id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixed_rate, hourly_rate) SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixed_rate, hourly_rate FROM __temp__kimai2_customers'); - $this->addSql('DROP TABLE __temp__kimai2_customers'); - - $this->addSql('DROP TABLE ' . $projects); - $this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__kimai2_projects'); - $this->addSql('DROP TABLE __temp__kimai2_projects'); - $this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)'); - - $this->addSql('DROP TABLE ' . $timesheet); - $this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL --(DC2Type:datetime) - , timezone VARCHAR(64) NOT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, exported BOOLEAN NOT NULL, end_time DATETIME DEFAULT NULL --(DC2Type:datetime) - , rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)'); - $this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported) SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM __temp__kimai2_timesheet'); - $this->addSql('DROP TABLE __temp__kimai2_timesheet'); - $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); - $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); - $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); - } else { - $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); - $this->addSql('ALTER TABLE ' . $customers . ' CHANGE email mail VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); - $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); - $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); - } + $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $customers . ' CHANGE email mail VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL'); } } diff --git a/src/Migrations/Version20190321181243.php b/src/Migrations/Version20190321181243.php index 9add572e..240195c2 100644 --- a/src/Migrations/Version20190321181243.php +++ b/src/Migrations/Version20190321181243.php @@ -23,17 +23,12 @@ final class Version20190321181243 extends AbstractMigration { public function getDescription(): string { - return ''; + return 'Create system configuration table'; } public function up(Schema $schema): void { - if ($this->isPlatformSqlite()) { - $this->addSql('CREATE TABLE kimai2_configuration (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(100) NOT NULL, value VARCHAR(255) DEFAULT NULL)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1C5D63D85E237E06 ON kimai2_configuration (name)'); - } else { - $this->addSql('CREATE TABLE kimai2_configuration (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(100) NOT NULL, value VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_1C5D63D85E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - } + $this->addSql('CREATE TABLE kimai2_configuration (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(100) NOT NULL, value VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_1C5D63D85E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); } public function down(Schema $schema): void diff --git a/src/Migrations/Version20190605171157.php b/src/Migrations/Version20190605171157.php index 0cca9a68..7cd8a272 100644 --- a/src/Migrations/Version20190605171157.php +++ b/src/Migrations/Version20190605171157.php @@ -26,21 +26,6 @@ final class Version20190605171157 extends AbstractMigration return 'Creates the budget columns on: customer, project, activity'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $customers = $schema->getTable('kimai2_customers'); diff --git a/src/Migrations/Version20190706224219.php b/src/Migrations/Version20190706224219.php index 9ea49d9d..ee9c572c 100644 --- a/src/Migrations/Version20190706224219.php +++ b/src/Migrations/Version20190706224219.php @@ -26,21 +26,6 @@ final class Version20190706224219 extends AbstractMigration return 'Creates several indices to improve speed for default queries.'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $timesheet = $schema->getTable('kimai2_timesheet'); diff --git a/src/Migrations/Version20190729162655.php b/src/Migrations/Version20190729162655.php index f44f1187..3e4672ee 100644 --- a/src/Migrations/Version20190729162655.php +++ b/src/Migrations/Version20190729162655.php @@ -26,21 +26,6 @@ final class Version20190729162655 extends AbstractMigration return 'Adds missing foreign keys on tag table'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $timesheetTags = $schema->getTable('kimai2_timesheet_tags'); diff --git a/src/Migrations/Version20190813162649.php b/src/Migrations/Version20190813162649.php index 7fb3b955..ad317187 100644 --- a/src/Migrations/Version20190813162649.php +++ b/src/Migrations/Version20190813162649.php @@ -26,21 +26,6 @@ final class Version20190813162649 extends AbstractMigration return 'Changing column sizes to prevent index length errors'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $activity = $schema->getTable('kimai2_activities'); diff --git a/src/Migrations/Version20191024100951.php b/src/Migrations/Version20191024100951.php index 4a226b51..566d1ae8 100644 --- a/src/Migrations/Version20191024100951.php +++ b/src/Migrations/Version20191024100951.php @@ -26,21 +26,6 @@ final class Version20191024100951 extends AbstractMigration return 'Adds the order_date column to the projects table'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $projects = $schema->getTable('kimai2_projects'); diff --git a/src/Migrations/Version20191108151534.php b/src/Migrations/Version20191108151534.php index 0d52acab..5cd3e5a2 100644 --- a/src/Migrations/Version20191108151534.php +++ b/src/Migrations/Version20191108151534.php @@ -26,21 +26,6 @@ final class Version20191108151534 extends AbstractMigration return 'Adds the user roles and role permissions table'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $roles = $schema->createTable('kimai2_roles'); diff --git a/src/Migrations/Version20191113132640.php b/src/Migrations/Version20191113132640.php index 587ffbe9..7dcd9c59 100644 --- a/src/Migrations/Version20191113132640.php +++ b/src/Migrations/Version20191113132640.php @@ -26,21 +26,6 @@ final class Version20191113132640 extends AbstractMigration return 'Fixes foreign keys on tag table'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $timesheetTags = $schema->getTable('kimai2_timesheet_tags'); diff --git a/src/Migrations/Version20191116110124.php b/src/Migrations/Version20191116110124.php index cb604fb8..329af7ff 100644 --- a/src/Migrations/Version20191116110124.php +++ b/src/Migrations/Version20191116110124.php @@ -26,21 +26,6 @@ final class Version20191116110124 extends AbstractMigration return 'New Vat ID columns and invoice template improvements'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $customers = $schema->getTable('kimai2_customers'); diff --git a/src/Migrations/Version20200125123942.php b/src/Migrations/Version20200125123942.php index 4526af2e..ca7bb855 100644 --- a/src/Migrations/Version20200125123942.php +++ b/src/Migrations/Version20200125123942.php @@ -26,21 +26,6 @@ final class Version20200125123942 extends AbstractMigration return 'Adds a column to the user table to identify authenticator'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $users = $schema->getTable('kimai2_users'); diff --git a/src/Migrations/Version20200204124425.php b/src/Migrations/Version20200204124425.php index 4537d85b..88a258b3 100644 --- a/src/Migrations/Version20200204124425.php +++ b/src/Migrations/Version20200204124425.php @@ -26,21 +26,6 @@ final class Version20200204124425 extends AbstractMigration return 'Adds language and decimal_duration column to invoice template table'; } - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } - public function up(Schema $schema): void { $invoiceTemplates = $schema->getTable('kimai2_invoice_templates'); diff --git a/src/Migrations/Version20200705152310.php b/src/Migrations/Version20200705152310.php index 35e3b60d..9f556074 100644 --- a/src/Migrations/Version20200705152310.php +++ b/src/Migrations/Version20200705152310.php @@ -45,19 +45,4 @@ final class Version20200705152310 extends AbstractMigration $timesheet->dropColumn('category'); $timesheet->dropColumn('modified_at'); } - - protected function isSupportingForeignKeys(): bool - { - return false; - } - - public function isTransactional(): bool - { - if ($this->isPlatformSqlite()) { - // does fail if we use transactions, as tables are re-created and foreign keys would fail - return false; - } - - return true; - } } diff --git a/tests/API/ActivityControllerTest.php b/tests/API/ActivityControllerTest.php index e07a6e85..756b4ef3 100644 --- a/tests/API/ActivityControllerTest.php +++ b/tests/API/ActivityControllerTest.php @@ -15,12 +15,12 @@ use App\Entity\ActivityMeta; use App\Entity\ActivityRate; use App\Entity\Customer; use App\Entity\Project; +use App\Entity\RateInterface; use App\Entity\User; use App\Repository\ActivityRateRepository; use App\Repository\ActivityRepository; use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelBrowser; /** * @group integration @@ -29,6 +29,20 @@ class ActivityControllerTest extends APIControllerBaseTest { use RateControllerTestTrait; + /** + * @param ActivityRate $rate + * @param bool $isCollection + * @return string + */ + protected function getRateUrlByRate(RateInterface $rate, bool $isCollection): string + { + if ($isCollection) { + return $this->getRateUrl($rate->getActivity()->getId()); + } + + return $this->getRateUrl($rate->getActivity()->getId(), $rate->getId()); + } + protected function getRateUrl($id = '1', $rateId = null): string { if (null !== $rateId) { @@ -77,7 +91,7 @@ class ActivityControllerTest extends APIControllerBaseTest $this->assertUrlIsSecured('/api/activities'); } - protected function loadActivityTestData(HttpKernelBrowser $client) + protected function loadActivityTestData(): array { $em = $this->getEntityManager(); @@ -116,15 +130,33 @@ class ActivityControllerTest extends APIControllerBaseTest $em->persist($activity); $em->flush(); + + return [$project, $project2]; } /** * @dataProvider getCollectionTestData */ - public function testGetCollection($url, $parameters, $expected) + public function testGetCollection($url, $project, $parameters, $expected) { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->loadActivityTestData($client); + $imports = $this->loadActivityTestData(); + + $projectId = $project !== null ? $imports[$project]->getId() : null; + if ($projectId !== null) { + if (\array_key_exists('project', $parameters)) { + $parameters['project'] = $projectId; + } + + if (\array_key_exists('projects', $parameters)) { + if (stripos($parameters['projects'], ',') !== false) { + $parameters['projects'] = $projectId . ',' . $projectId; + } else { + $parameters['projects'] = (string) $projectId; + } + } + } + $this->assertAccessIsGranted($client, $url, 'GET', $parameters); $result = json_decode($client->getResponse()->getContent(), true); @@ -135,31 +167,31 @@ class ActivityControllerTest extends APIControllerBaseTest $activity = $result[$i]; $hasProject = $expected[$i][0]; self::assertApiResponseTypeStructure('ActivityCollection', $activity); - if ($hasProject) { - $this->assertEquals($expected[$i][1], $activity['project']); + if ($hasProject && $projectId !== null) { + $this->assertEquals($projectId, $activity['project']); } } } public function getCollectionTestData() { - yield ['/api/activities', [], [[false], [true, 2], [true, 2], [null], [true, 1]]]; + yield ['/api/activities', null, [], [[false], [true, 2], [true, 2], [null], [true, 1]]]; //yield ['/api/activities', [], [[false], [false], [true, 2], [true, 1], [true, 2]]]; - yield ['/api/activities', ['globals' => 'true'], [[false], [false]]]; - yield ['/api/activities', ['globals' => 'true', 'visible' => 3], [[false], [false], [false]]]; - yield ['/api/activities', ['globals' => 'true', 'visible' => '2'], [[false]]]; - yield ['/api/activities', ['globals' => 'true', 'visible' => 1], [[false], [false]]]; - yield ['/api/activities', ['project' => '1'], [[false], [false], [true, 1]]]; - yield ['/api/activities', ['project' => '2', 'projects' => '2', 'visible' => 1], [[false], [true, 2], [true, 2], [false]]]; - yield ['/api/activities', ['project' => '2', 'projects' => '2,2', 'visible' => '3'], [[false], [true, 2], [true, 2], [true, 2], [false], [false]]]; - yield ['/api/activities', ['projects' => '2,2', 'visible' => 2], [[true, 2], [false]]]; - yield ['/api/activities', ['projects' => '2', 'visible' => 2], [[true, 2], [false]]]; + yield ['/api/activities', null, ['globals' => 'true'], [[false], [false]]]; + yield ['/api/activities', null, ['globals' => 'true', 'visible' => 3], [[false], [false], [false]]]; + yield ['/api/activities', null, ['globals' => 'true', 'visible' => '2'], [[false]]]; + yield ['/api/activities', null, ['globals' => 'true', 'visible' => 1], [[false], [false]]]; + yield ['/api/activities', 0, ['project' => '1'], [[false], [false], [true, 1]]]; + yield ['/api/activities', 1, ['project' => '2', 'projects' => '2', 'visible' => 1], [[true, 2], [true, 2], [false], [false]]]; + yield ['/api/activities', 1, ['project' => '2', 'projects' => '2,2', 'visible' => '3'], [[true, 2], [true, 2], [true, 2], [false], [false], [false]]]; + yield ['/api/activities', 1, ['projects' => '2,2', 'visible' => 2], [[true, 2], [false]]]; + yield ['/api/activities', 1, ['projects' => '2', 'visible' => 2], [[true, 2], [false]]]; } public function testGetCollectionWithQuery() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->loadActivityTestData($client); + $imports = $this->loadActivityTestData(); $query = ['order' => 'ASC', 'orderBy' => 'project']; $this->assertAccessIsGranted($client, '/api/activities', 'GET', $query); @@ -169,9 +201,9 @@ class ActivityControllerTest extends APIControllerBaseTest $this->assertNotEmpty($result); $this->assertEquals(5, \count($result)); self::assertApiResponseTypeStructure('ActivityCollection', $result[0]); - $this->assertEquals(1, $result[4]['project']); - $this->assertEquals(2, $result[3]['project']); - $this->assertEquals(2, $result[2]['project']); + $this->assertEquals($imports[0]->getId(), $result[4]['project']); + $this->assertEquals($imports[1]->getId(), $result[3]['project']); + $this->assertEquals($imports[1]->getId(), $result[2]['project']); } public function testGetEntity() diff --git a/tests/API/CustomerControllerTest.php b/tests/API/CustomerControllerTest.php index 292891b4..350e7f8d 100644 --- a/tests/API/CustomerControllerTest.php +++ b/tests/API/CustomerControllerTest.php @@ -15,6 +15,7 @@ use App\Entity\Customer; use App\Entity\CustomerMeta; use App\Entity\CustomerRate; use App\Entity\Project; +use App\Entity\RateInterface; use App\Entity\Team; use App\Entity\User; use App\Repository\CustomerRateRepository; @@ -29,6 +30,20 @@ class CustomerControllerTest extends APIControllerBaseTest { use RateControllerTestTrait; + /** + * @param CustomerRate $rate + * @param bool $isCollection + * @return string + */ + protected function getRateUrlByRate(RateInterface $rate, bool $isCollection): string + { + if ($isCollection) { + return $this->getRateUrl($rate->getCustomer()->getId()); + } + + return $this->getRateUrl($rate->getCustomer()->getId(), $rate->getId()); + } + protected function getRateUrl($id = '1', $rateId = null): string { if (null !== $rateId) { diff --git a/tests/API/ProjectControllerTest.php b/tests/API/ProjectControllerTest.php index 5d07b836..3e09903b 100644 --- a/tests/API/ProjectControllerTest.php +++ b/tests/API/ProjectControllerTest.php @@ -14,6 +14,7 @@ use App\Entity\Customer; use App\Entity\Project; use App\Entity\ProjectMeta; use App\Entity\ProjectRate; +use App\Entity\RateInterface; use App\Entity\Team; use App\Entity\User; use App\Repository\ProjectRateRepository; @@ -30,6 +31,20 @@ class ProjectControllerTest extends APIControllerBaseTest { use RateControllerTestTrait; + /** + * @param ProjectRate $rate + * @param bool $isCollection + * @return string + */ + protected function getRateUrlByRate(RateInterface $rate, bool $isCollection): string + { + if ($isCollection) { + return $this->getRateUrl($rate->getProject()->getId()); + } + + return $this->getRateUrl($rate->getProject()->getId(), $rate->getId()); + } + protected function getRateUrl($id = '1', $rateId = null): string { if (null !== $rateId) { @@ -139,15 +154,32 @@ class ProjectControllerTest extends APIControllerBaseTest $em->persist($project); $em->flush(); + + return [$customer, $customer2, $customer3]; } /** * @dataProvider getCollectionTestData */ - public function testGetCollectionWithParams($url, $parameters, $expected) + public function testGetCollectionWithParams($url, $customer, $parameters, $expected) { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->loadProjectTestData($client); + $imports = $this->loadProjectTestData($client); + + $customerId = $customer !== null ? $imports[$customer]->getId() : null; + + if ($customerId !== null) { + if (\array_key_exists('customer', $parameters)) { + $parameters['customer'] = $customerId; + } elseif (\array_key_exists('customers', $parameters)) { + if (stripos($parameters['customers'], ',') !== false) { + $parameters['customers'] = $customerId . ',' . $customerId; + } else { + $parameters['customers'] = (string) $customerId; + } + } + } + $this->assertAccessIsGranted($client, $url, 'GET', $parameters); $result = json_decode($client->getResponse()->getContent(), true); @@ -156,29 +188,30 @@ class ProjectControllerTest extends APIControllerBaseTest for ($i = 0; $i < \count($expected); $i++) { $project = $result[$i]; - $compare = $expected[$i]; self::assertApiResponseTypeStructure('ProjectCollection', $project); - $this->assertEquals($compare[1], $project['customer']); + if ($customerId !== null) { + $this->assertEquals($customerId, $project['customer']); + } } } public function getCollectionTestData() { - // if you wonder why: SQLite does case-sensitive ordering, so "Title" > "fifth” - yield ['/api/projects', [], [[true, 1], [false, 1], [false, 3]]]; - yield ['/api/projects', ['customer' => '1'], [[true, 1], [false, 1]]]; - yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityInterface::SHOW_VISIBLE], [[true, 1], [false, 1]]]; - yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityInterface::SHOW_BOTH], [[true, 1], [false, 1], [false, 1]]]; - yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityInterface::SHOW_HIDDEN], [[false, 1]]]; + // if you wonder why: case-sensitive ordering feels strange ... "Title" > "fifth” + yield ['/api/projects', null, [], [[true, 1], [false, 1], [false, 3]]]; + yield ['/api/projects', 0, ['customer' => '1'], [[true, 1], [false, 1]]]; + yield ['/api/projects', 0, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_VISIBLE], [[true, 1], [false, 1]]]; + yield ['/api/projects', 0, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_BOTH], [[true, 1], [false, 1], [false, 1]]]; + yield ['/api/projects', 0, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_HIDDEN], [[false, 1]]]; // customer is invisible, so nothing should be returned - yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityInterface::SHOW_VISIBLE], []]; - yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; - yield ['/api/projects', ['customer' => '2', 'customers' => '2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; - yield ['/api/projects', ['customer' => '2', 'customers' => '2,2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; + yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_VISIBLE], []]; + yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; + yield ['/api/projects', 1, ['customer' => '2', 'customers' => '2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; + yield ['/api/projects', 1, ['customer' => '2', 'customers' => '2,2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; // customer is invisible, so nothing should be returned - yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11T23:59:59', 'end' => '2030-12-11T23:59:59'], []]; - yield ['/api/projects', ['customers' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11T23:59:59', 'end' => '2030-12-11T23:59:59'], []]; - yield ['/api/projects', ['customers' => '2,2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11T23:59:59', 'end' => '2030-12-11T23:59:59'], []]; + yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11T23:59:59', 'end' => '2030-12-11T23:59:59'], []]; + yield ['/api/projects', 1, ['customers' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11T23:59:59', 'end' => '2030-12-11T23:59:59'], []]; + yield ['/api/projects', 1, ['customers' => '2,2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11T23:59:59', 'end' => '2030-12-11T23:59:59'], []]; } public function testGetEntity() @@ -207,8 +240,9 @@ class ProjectControllerTest extends APIControllerBaseTest ->setEnd($endDate) ; $em->persist($project); + $em->flush(); - $this->assertAccessIsGranted($client, '/api/projects/2'); + $this->assertAccessIsGranted($client, '/api/projects/' . $project->getId()); $result = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($result); @@ -216,8 +250,8 @@ class ProjectControllerTest extends APIControllerBaseTest $expected = [ 'parentTitle' => 'first one', - 'customer' => 2, - 'id' => 2, + 'customer' => $customer->getId(), + 'id' => $project->getId(), 'name' => 'first', 'orderNumber' => null, // make sure the timezone is properly applied in serializer (see #1858) diff --git a/tests/API/RateControllerTestTrait.php b/tests/API/RateControllerTestTrait.php index fb376fdb..8e7be685 100644 --- a/tests/API/RateControllerTestTrait.php +++ b/tests/API/RateControllerTestTrait.php @@ -9,6 +9,7 @@ namespace App\Tests\API; +use App\Entity\RateInterface; use App\Entity\User; use Symfony\Component\HttpFoundation\Response; @@ -24,9 +25,11 @@ trait RateControllerTestTrait */ abstract protected function getRateUrl($id = '1', $rateId = null): string; + abstract protected function getRateUrlByRate(RateInterface $rate, bool $isCollection): string; + /** * @param string|int $id - * @return array + * @return RateInterface[] */ abstract protected function importTestRates($id): array; @@ -160,12 +163,12 @@ trait RateControllerTestTrait $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $expectedRates = $this->importTestRates(1); - $this->request($client, $this->getRateUrl(1, 1), 'DELETE'); + $this->request($client, $this->getRateUrlByRate($expectedRates[0], false), 'DELETE'); $this->assertTrue($client->getResponse()->isSuccessful()); $this->assertEmpty($client->getResponse()->getContent()); // fetch rates to validate that one was removed - $this->request($client, $this->getRateUrl(1)); + $this->request($client, $this->getRateUrlByRate($expectedRates[0], true)); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); diff --git a/tests/API/TagControllerTest.php b/tests/API/TagControllerTest.php index db827438..05d930d6 100644 --- a/tests/API/TagControllerTest.php +++ b/tests/API/TagControllerTest.php @@ -9,24 +9,28 @@ namespace App\Tests\API; +use App\Entity\Tag; use App\Entity\User; use App\Tests\DataFixtures\TagFixtures; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelBrowser; /** * @group integration */ class TagControllerTest extends APIControllerBaseTest { - protected function importTagFixtures(HttpKernelBrowser $client): void + /** + * @return Tag[] + */ + protected function importTagFixtures(): array { $tagList = ['Test', 'Administration', 'Support', '#2018-001', '#2018-002', '#2018-003', 'Development', 'Marketing', 'First Level Support', 'Bug Fixing']; $fixture = new TagFixtures(); $fixture->setTagArray($tagList); - $this->importFixture($fixture); + + return $this->importFixture($fixture); } public function testIsSecure() @@ -37,7 +41,7 @@ class TagControllerTest extends APIControllerBaseTest public function testGetCollection() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importTagFixtures($client); + $this->importTagFixtures(); $this->assertAccessIsGranted($client, '/api/tags'); $result = json_decode($client->getResponse()->getContent(), true); @@ -50,7 +54,7 @@ class TagControllerTest extends APIControllerBaseTest public function testEmptyCollection() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importTagFixtures($client); + $this->importTagFixtures(); $query = ['name' => 'nothing']; $this->assertAccessIsGranted($client, '/api/tags', 'GET', $query); $result = json_decode($client->getResponse()->getContent(), true); @@ -63,7 +67,7 @@ class TagControllerTest extends APIControllerBaseTest public function testPostAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importTagFixtures($client); + $this->importTagFixtures(); $data = [ 'name' => 'foo', 'color' => '#000FFF' @@ -81,7 +85,7 @@ class TagControllerTest extends APIControllerBaseTest public function testPostActionWithValidationErrors() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importTagFixtures($client); + $this->importTagFixtures(); $data = [ 'name' => '1', 'color' => '11231231231', @@ -95,7 +99,7 @@ class TagControllerTest extends APIControllerBaseTest public function testPostActionWithInvalidUser() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importTagFixtures($client); + $this->importTagFixtures(); $data = [ 'name' => 'foo', ]; @@ -110,7 +114,7 @@ class TagControllerTest extends APIControllerBaseTest public function testPartOfEntries() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importTagFixtures($client); + $this->importTagFixtures(); $query = ['name' => 'in']; $this->assertAccessIsGranted($client, '/api/tags', 'GET', $query); $result = json_decode($client->getResponse()->getContent(), true); @@ -127,9 +131,10 @@ class TagControllerTest extends APIControllerBaseTest public function testDeleteAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importTagFixtures($client); + $tags = $this->importTagFixtures(); + $id = $tags[0]->getId(); - $this->request($client, '/api/tags/1', 'DELETE'); + $this->request($client, '/api/tags/' . $id, 'DELETE'); $this->assertTrue($client->getResponse()->isSuccessful()); $this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); $this->assertEmpty($client->getResponse()->getContent()); @@ -142,6 +147,6 @@ class TagControllerTest extends APIControllerBaseTest public function testDeleteActionWithUnknownTimesheet() { - $this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/tags/255'); + $this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/tags/' . PHP_INT_MAX); } } diff --git a/tests/API/TeamControllerTest.php b/tests/API/TeamControllerTest.php index e2a1070c..7f1951c1 100644 --- a/tests/API/TeamControllerTest.php +++ b/tests/API/TeamControllerTest.php @@ -9,21 +9,25 @@ namespace App\Tests\API; +use App\Entity\Team; use App\Entity\User; use App\Tests\DataFixtures\TeamFixtures; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelBrowser; /** * @group integration */ class TeamControllerTest extends APIControllerBaseTest { - protected function importTeamFixtures(HttpKernelBrowser $client): void + /** + * @return Team[] + */ + protected function importTeamFixtures(): array { $fixture = new TeamFixtures(); $fixture->setAmount(1); - $this->importFixture($fixture); + + return $this->importFixture($fixture); } public function testIsSecure() @@ -50,7 +54,7 @@ class TeamControllerTest extends APIControllerBaseTest public function testGetCollection() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importTeamFixtures($client); + $this->importTeamFixtures(); $this->assertAccessIsGranted($client, '/api/teams'); $result = json_decode($client->getResponse()->getContent(), true); @@ -63,8 +67,10 @@ class TeamControllerTest extends APIControllerBaseTest public function testGetEntity() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importTeamFixtures($client); - $this->assertAccessIsGranted($client, '/api/teams/2'); + $teams = $this->importTeamFixtures(); + $id = $teams[0]->getId(); + + $this->assertAccessIsGranted($client, '/api/teams/' . $id); $result = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($result); @@ -178,8 +184,9 @@ class TeamControllerTest extends APIControllerBaseTest public function testDeleteAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importTeamFixtures($client); - $this->assertAccessIsGranted($client, '/api/teams/2'); + $teams = $this->importTeamFixtures(); + $id = $teams[0]->getId(); + $this->assertAccessIsGranted($client, '/api/teams/' . $id); $result = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($result); diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index 02867739..80b9fae4 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -31,7 +31,11 @@ class TimesheetControllerTest extends APIControllerBaseTest public const DATE_FORMAT_HTML5 = 'Y-m-d\TH:i:s'; public const TEST_TIMEZONE = 'Europe/London'; - protected function importFixtureForUser(string $role) + /** + * @param string $role + * @return Timesheet[] + */ + protected function importFixtureForUser(string $role): array { $fixture = new TimesheetFixtures(); $fixture @@ -43,7 +47,7 @@ class TimesheetControllerTest extends APIControllerBaseTest ->setStartDate((new \DateTime('first day of this month'))->setTime(0, 0, 1)) ; - $this->importFixture($fixture); + return $this->importFixture($fixture); } public function testIsSecure() @@ -300,8 +304,9 @@ class TimesheetControllerTest extends APIControllerBaseTest ->addTag($tag) ; $em->persist($timesheet); + $em->flush(); - $this->assertAccessIsGranted($client, '/api/timesheets/1'); + $this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheet->getId()); $result = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($result); @@ -314,7 +319,6 @@ class TimesheetControllerTest extends APIControllerBaseTest 'tags' => [ 0 => 'test' ], - 'id' => 1, // make sure the timezone is properly applied in serializer (see #1858) // minute and second are different from the above datetime object, because of applied default minute rounding 'begin' => '2020-03-27T14:35:00+1300', @@ -337,16 +341,17 @@ class TimesheetControllerTest extends APIControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_ADMIN); + $timesheets = $this->importFixtureForUser(User::ROLE_ADMIN); + $this->assertCount(10, $timesheets); - $this->assertApiAccessDenied($client, '/api/timesheets/15', 'You are not allowed to view this timesheet'); + $this->assertApiAccessDenied($client, '/api/timesheets/' . $timesheets[0]->getId(), 'You are not allowed to view this timesheet'); } public function testGetEntityAccessAllowedForAdmin() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importFixtureForUser(User::ROLE_USER); - $this->assertAccessIsGranted($client, '/api/timesheets/1'); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheets[0]->getId()); $result = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($result); @@ -355,7 +360,7 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testGetEntityNotFound() { - $this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/20'); + $this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/' . PHP_INT_MAX); } public function testPostAction() @@ -496,7 +501,7 @@ class TimesheetControllerTest extends APIControllerBaseTest { $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE)); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); $data = [ 'activity' => 1, 'project' => 1, @@ -505,7 +510,7 @@ class TimesheetControllerTest extends APIControllerBaseTest 'description' => 'foo', 'exported' => true, ]; - $this->request($client, '/api/timesheets/1', 'PATCH', [], json_encode($data)); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); @@ -530,7 +535,7 @@ class TimesheetControllerTest extends APIControllerBaseTest ->setStartDate(new \DateTime('-10 days')) ->setAllowEmptyDescriptions(false) ; - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); $data = [ 'activity' => 1, @@ -540,7 +545,7 @@ class TimesheetControllerTest extends APIControllerBaseTest 'description' => 'foo', 'exported' => true, ]; - $this->request($client, '/api/timesheets/15', 'PATCH', [], json_encode($data)); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $response = $client->getResponse(); $this->assertFalse($response->isSuccessful()); $this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode()); @@ -556,7 +561,7 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testInvalidPatchAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); $data = [ 'activity' => 10, @@ -565,7 +570,7 @@ class TimesheetControllerTest extends APIControllerBaseTest 'end' => (new \DateTime('- 7 hours'))->format('Y-m-d H:m'), 'description' => 'foo', ]; - $this->request($client, '/api/timesheets/1', 'PATCH', [], json_encode($data)); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $response = $client->getResponse(); $this->assertEquals(400, $response->getStatusCode()); @@ -577,8 +582,8 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testDeleteAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); - $this->assertAccessIsGranted($client, '/api/timesheets/1'); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheets[0]->getId()); $result = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($result); @@ -600,11 +605,9 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testDeleteActionForDifferentUser() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); - $id = 1; - - $this->request($client, '/api/timesheets/' . $id, 'DELETE'); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'DELETE'); $this->assertTrue($client->getResponse()->isSuccessful()); $this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); $this->assertEmpty($client->getResponse()->getContent()); @@ -614,9 +617,9 @@ class TimesheetControllerTest extends APIControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_ADMIN); + $timesheets = $this->importFixtureForUser(User::ROLE_ADMIN); - $this->request($client, '/api/timesheets/15', 'DELETE'); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'DELETE'); $response = $client->getResponse(); $this->assertFalse($response->isSuccessful()); @@ -632,12 +635,13 @@ class TimesheetControllerTest extends APIControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; + $id = $timesheet->getId(); $timesheet->setExported(true); $em->persist($timesheet); $em->flush(); - $this->request($client, '/api/timesheets/1', 'DELETE'); + $this->request($client, '/api/timesheets/' . $id, 'DELETE'); $this->assertApiResponseAccessDenied($client->getResponse(), 'You are not allowed to delete this timesheet'); } @@ -648,12 +652,13 @@ class TimesheetControllerTest extends APIControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; + $id = $timesheet->getId(); $timesheet->setExported(true); $em->persist($timesheet); $em->flush(); - $this->request($client, '/api/timesheets/1', 'DELETE'); + $this->request($client, '/api/timesheets/' . $id, 'DELETE'); $this->assertTrue($client->getResponse()->isSuccessful()); } @@ -731,9 +736,10 @@ class TimesheetControllerTest extends APIControllerBaseTest ->setStartDate($start) ->setAmountRunning(1) ; - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); + $id = $timesheets[0]->getId(); - $this->request($client, '/api/timesheets/11/stop', 'PATCH'); + $this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); @@ -743,15 +749,16 @@ class TimesheetControllerTest extends APIControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertInstanceOf(\DateTime::class, $timesheet->getEnd()); } public function testStopActionFailsOnStoppedEntry() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); - $this->request($client, '/api/timesheets/1/stop', 'PATCH'); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); + $this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH'); $this->assertApiException($client->getResponse(), 'Timesheet entry already stopped'); } @@ -777,9 +784,10 @@ class TimesheetControllerTest extends APIControllerBaseTest ->setStartDate($start) ->setAmountRunning(3) ; - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); + $id = $timesheets[3]->getId(); - $this->request($client, '/api/timesheets/12/stop', 'PATCH'); + $this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH'); $this->assertApiResponseAccessDenied($client->getResponse(), 'You are not allowed to stop this timesheet'); } @@ -806,7 +814,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertIsArray($result); $this->assertNotEmpty($result); - $this->assertEquals(5, \count($result)); + $this->assertEquals(10, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); $query = ['tags' => 'Test,Admin']; @@ -831,15 +839,16 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testRestartAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); $data = [ 'description' => 'foo', 'tags' => 'another,testing,bar' ]; - $this->request($client, '/api/timesheets/1', 'PATCH', [], json_encode($data)); + $this->request($client, '/api/timesheets/' . $id, 'PATCH', [], json_encode($data)); - $this->request($client, '/api/timesheets/1/restart', 'PATCH'); + $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); @@ -861,16 +870,17 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testRestartActionWithBegin() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); $data = [ 'description' => 'foo', 'tags' => 'another,testing,bar' ]; - $this->request($client, '/api/timesheets/1', 'PATCH', [], json_encode($data)); + $this->request($client, '/api/timesheets/' . $id, 'PATCH', [], json_encode($data)); $begin = new \DateTime('2019-11-27 13:55:00'); - $this->request($client, '/api/timesheets/1/restart', 'PATCH', ['begin' => $begin->format(BaseApiController::DATE_FORMAT_PHP)]); + $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['begin' => $begin->format(BaseApiController::DATE_FORMAT_PHP)]); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); @@ -893,11 +903,12 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testRestartActionWithCopyData() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $timesheet->setDescription('foo'); $timesheet->addTag((new Tag())->setName('another')); $timesheet->addTag((new Tag())->setName('testing')); @@ -908,10 +919,10 @@ class TimesheetControllerTest extends APIControllerBaseTest $em->persist($timesheet); $em->flush(); - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertEquals('foo', $timesheet->getDescription()); - $this->request($client, '/api/timesheets/1/restart', 'PATCH', ['copy' => 'all']); + $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['copy' => 'all']); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); @@ -946,9 +957,10 @@ class TimesheetControllerTest extends APIControllerBaseTest ->setStartDate($start) ->setAmountRunning(3) ; - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); + $id = $timesheets[0]->getId(); - $this->request($client, '/api/timesheets/2/restart', 'PATCH'); + $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH'); $this->assertApiResponseAccessDenied($client->getResponse(), 'You are not allowed to re-start this timesheet'); } @@ -999,56 +1011,61 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testExportAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertFalse($timesheet->isExported()); - $this->request($client, '/api/timesheets/1/export', 'PATCH'); + $this->request($client, '/api/timesheets/' . $id . '/export', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); self::assertApiResponseTypeStructure('TimesheetEntity', $result); $em->clear(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertTrue($timesheet->isExported()); - $this->request($client, '/api/timesheets/1/export', 'PATCH'); + $this->request($client, '/api/timesheets/' . $id . '/export', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); $em->clear(); - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertFalse($timesheet->isExported()); } public function testExportNotAllowedForUser() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); - $this->request($client, '/api/timesheets/1/export', 'PATCH'); + $this->request($client, '/api/timesheets/' . $id . '/export', 'PATCH'); $this->assertApiResponseAccessDenied($client->getResponse(), 'You are not allowed to lock this timesheet'); } public function testExportThrowsNotFound() { - $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/42/export', []); + $id = PHP_INT_MAX; + $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/' . $id . '/export', []); } public function testMetaActionThrowsNotFound() { - $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/42/meta', []); + $id = PHP_INT_MAX; + $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/' . $id . '/meta', []); } public function testMetaActionThrowsExceptionOnMissingName() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); - return $this->assertExceptionForMethod($client, '/api/timesheets/1/meta', 'PATCH', ['value' => 'X'], [ + return $this->assertExceptionForMethod($client, '/api/timesheets/' . $id . '/meta', 'PATCH', ['value' => 'X'], [ 'code' => 400, 'message' => 'Parameter "name" of value "NULL" violated a constraint "This value should not be null."' ]); @@ -1057,9 +1074,10 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testMetaActionThrowsExceptionOnMissingValue() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); - return $this->assertExceptionForMethod($client, '/api/timesheets/1/meta', 'PATCH', ['name' => 'X'], [ + return $this->assertExceptionForMethod($client, '/api/timesheets/' . $id . '/meta', 'PATCH', ['name' => 'X'], [ 'code' => 400, 'message' => 'Parameter "value" of value "NULL" violated a constraint "This value should not be null."' ]); @@ -1068,9 +1086,10 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testMetaActionThrowsExceptionOnMissingMetafield() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); - return $this->assertExceptionForMethod($client, '/api/timesheets/1/meta', 'PATCH', ['name' => 'X', 'value' => 'Y'], [ + return $this->assertExceptionForMethod($client, '/api/timesheets/' . $id . '/meta', 'PATCH', ['name' => 'X', 'value' => 'Y'], [ 'code' => 500, 'message' => 'Unknown meta-field requested' ]); @@ -1079,14 +1098,15 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testMetaAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->importFixtureForUser(User::ROLE_USER); + $timesheets = $this->importFixtureForUser(User::ROLE_USER); + $id = $timesheets[0]->getId(); static::$container->get('event_dispatcher')->addSubscriber(new TimesheetTestMetaFieldSubscriberMock()); $data = [ 'name' => 'metatestmock', 'value' => 'another,testing,bar' ]; - $this->request($client, '/api/timesheets/1/meta', 'PATCH', [], json_encode($data)); + $this->request($client, '/api/timesheets/' . $id . '/meta', 'PATCH', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -1096,7 +1116,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue()); } } diff --git a/tests/Command/CreateReleaseCommandTest.php b/tests/Command/CreateReleaseCommandTest.php deleted file mode 100644 index db5fb844..00000000 --- a/tests/Command/CreateReleaseCommandTest.php +++ /dev/null @@ -1,38 +0,0 @@ - 'test']); - $application = new Application($kernel); - $application->add(new CreateReleaseCommand(realpath(__DIR__ . '/../../'), 'test')); - - $command = $application->find('kimai:create-release'); - self::assertTrue($command->isEnabled()); - self::assertInstanceOf(CreateReleaseCommand::class, $command); - } - - public function testCommandNameIsNotAvailableInProd() - { - $command = new CreateReleaseCommand(realpath(__DIR__ . '/../../'), 'prod'); - self::assertFalse($command->isEnabled()); - } -} diff --git a/tests/Command/InstallCommandTest.php b/tests/Command/InstallCommandTest.php index c4b7f252..19ac22e7 100644 --- a/tests/Command/InstallCommandTest.php +++ b/tests/Command/InstallCommandTest.php @@ -10,11 +10,9 @@ namespace App\Tests\Command; use App\Command\InstallCommand; -use App\Constants; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Tester\CommandTester; /** * @covers \App\Command\InstallCommand @@ -40,30 +38,4 @@ class InstallCommandTest extends KernelTestCase return $this->application->find('kimai:install'); } - - public function testFullRunWithEverythingPreInstalled() - { - $command = $this->getCommand(); - $commandTester = new CommandTester($command); - $commandTester->setInputs(['no']); - $commandTester->execute([ - 'command' => $command->getName(), - ]); - - $result = $commandTester->getDisplay(); - - self::assertStringContainsString('Kimai installation running', $result); - // create database is skipped - self::assertStringContainsString('[NOTE] Database is existing and connection could be established', $result); - // make sure migrations run always - self::assertStringContainsString('Application Migrations', $result); - self::assertStringContainsString('No migrations to execute.', $result); - - self::assertStringContainsString( - sprintf('[OK] Congratulations! Successfully installed Kimai 2 version %s (%s)', Constants::VERSION, Constants::STATUS), - $result - ); - - self::assertEquals(0, $commandTester->getStatusCode()); - } } diff --git a/tests/Command/InvoiceCreateCommandTest.php b/tests/Command/InvoiceCreateCommandTest.php index 9e4cf865..ec072991 100644 --- a/tests/Command/InvoiceCreateCommandTest.php +++ b/tests/Command/InvoiceCreateCommandTest.php @@ -21,7 +21,7 @@ use App\Repository\ProjectRepository; use App\Repository\TimesheetRepository; use App\Repository\UserRepository; use App\Tests\DataFixtures\CustomerFixtures; -use App\Tests\DataFixtures\InvoiceFixtures; +use App\Tests\DataFixtures\InvoiceTemplateFixtures; use App\Tests\DataFixtures\ProjectFixtures; use App\Tests\DataFixtures\TimesheetFixtures; use App\Tests\KernelTestTrait; @@ -176,7 +176,7 @@ class InvoiceCreateCommandTest extends KernelTestCase public function testCreateInvoice() { - $fixture = new InvoiceFixtures(); + $fixture = new InvoiceTemplateFixtures(); $this->importFixture($fixture); $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--set-exported' => null, '--customer' => 1, '--template' => 'Invoice', '--start' => '2020-01-01', '--end' => '2020-03-01']); @@ -185,7 +185,7 @@ class InvoiceCreateCommandTest extends KernelTestCase $this->assertStringContainsString('+----+----------+-------+------------- Created 1 invoice(s) --------------------------------------+', $output); $this->assertStringContainsString('| ID | Customer | Total | Filename |', $output); $this->assertStringContainsString('+----+----------+-------+-------------------------------------------------------------------------+', $output); - $this->assertStringContainsString('| 1 | Test | 0 EUR | /', $output); + $this->assertStringContainsString('| Test | 0 EUR | /', $output); $this->assertStringContainsString('/tests/_data/invoices/' . ((new \DateTime())->format('Y')) . '-001-Test.html |', $output); } @@ -201,22 +201,24 @@ class InvoiceCreateCommandTest extends KernelTestCase $meta->setValue('Invoice'); $customer->setMetaField($meta); }); - $this->importFixture($fixture); + $customer = $this->importFixture($fixture)[0]; $fixture = new ProjectFixtures(); - $fixture->setCustomers([$em->getRepository(Customer::class)->find(2)]); + $fixture->setCustomers([$customer]); $fixture->setAmount(1); - $this->importFixture($fixture); + $projects = $this->importFixture($fixture); $fixture = new TimesheetFixtures(); $fixture->setUser($this->getUserByName(UserFixtures::USERNAME_SUPER_ADMIN)); $fixture->setAmount(20); $fixture->setStartDate($start); - $fixture->setProjects([$em->getRepository(Project::class)->find(2)]); + $fixture->setProjects($projects); $this->importFixture($fixture); - $fixture = new InvoiceFixtures(); + $fixture = new InvoiceTemplateFixtures(); $this->importFixture($fixture); + + return [$customer]; } public function testCreateInvoiceByCustomer() @@ -237,9 +239,10 @@ class InvoiceCreateCommandTest extends KernelTestCase $start = new \DateTime('-2 months'); $end = new \DateTime(); - $this->prepareFixtures($start); + $imports = $this->prepareFixtures($start); + $customer = $imports[0]->getId(); - $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => '2,1', '--template-meta' => 'template', '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); + $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => $customer . ',1', '--template-meta' => 'template', '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); $output = $commandTester->getDisplay(); $this->assertStringContainsString('Created 1 invoice(s) ', $output); diff --git a/tests/Command/ResetCommandTest.php b/tests/Command/ResetDevelopmentCommandTest.php similarity index 58% rename from tests/Command/ResetCommandTest.php rename to tests/Command/ResetDevelopmentCommandTest.php index 13a94436..f9750fa0 100644 --- a/tests/Command/ResetCommandTest.php +++ b/tests/Command/ResetDevelopmentCommandTest.php @@ -9,29 +9,30 @@ namespace App\Tests\Command; -use App\Command\ResetCommand; +use App\Command\ResetDevelopmentCommand; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** - * @covers \App\Command\ResetCommand + * @covers \App\Command\ResetDevelopmentCommand * @group integration */ -class ResetCommandTest extends KernelTestCase +class ResetDevelopmentCommandTest extends KernelTestCase { public function testCommandName() { $kernel = self::bootKernel(); $application = new Application($kernel); - $application->add(new ResetCommand('test')); + $application->add(new ResetDevelopmentCommand('test', $this->createMock(EntityManagerInterface::class))); $command = $application->find('kimai:reset-dev'); - self::assertInstanceOf(ResetCommand::class, $command); + self::assertInstanceOf(ResetDevelopmentCommand::class, $command); } public function testCommandNameIsNotEnabledInProd() { - $command = new ResetCommand('prod'); + $command = new ResetDevelopmentCommand('prod', $this->createMock(EntityManagerInterface::class)); self::assertFalse($command->isEnabled()); } } diff --git a/tests/Command/ResetTestCommandTest.php b/tests/Command/ResetTestCommandTest.php new file mode 100644 index 00000000..366e85fe --- /dev/null +++ b/tests/Command/ResetTestCommandTest.php @@ -0,0 +1,38 @@ +add(new ResetTestCommand('test', $this->createMock(EntityManagerInterface::class))); + + $command = $application->find('kimai:reset-test'); + self::assertInstanceOf(ResetTestCommand::class, $command); + } + + public function testCommandNameIsNotEnabledInProd() + { + $command = new ResetTestCommand('prod', $this->createMock(EntityManagerInterface::class)); + self::assertFalse($command->isEnabled()); + } +} diff --git a/tests/ConsoleApplicationTest.php b/tests/ConsoleApplicationTest.php new file mode 100644 index 00000000..db7de04c --- /dev/null +++ b/tests/ConsoleApplicationTest.php @@ -0,0 +1,29 @@ +createMock(KernelInterface::class); + $sut = new ConsoleApplication($kernel); + self::assertEquals(Constants::SOFTWARE, $sut->getName()); + self::assertEquals(Constants::VERSION, $sut->getVersion()); + } +} diff --git a/tests/Controller/ActivityControllerTest.php b/tests/Controller/ActivityControllerTest.php index 2e86a8e2..cbacf262 100644 --- a/tests/Controller/ActivityControllerTest.php +++ b/tests/Controller/ActivityControllerTest.php @@ -180,7 +180,11 @@ class ActivityControllerTest extends ControllerBaseTest $client->followRedirect(); $this->assertHasDataTable($client); - $this->request($client, '/admin/activity/2/edit'); + $activities = $this->getEntityManager()->getRepository(Activity::class)->findAll(); + $activity = array_pop($activities); + $id = $activity->getId(); + + $this->request($client, '/admin/activity/' . $id . '/edit'); $editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form(); $this->assertEquals('An AcTiVitY Name', $editForm->get('activity_edit_form[name]')->getValue()); // make sure customer and project are pre-selected for none global activities @@ -240,15 +244,16 @@ class ActivityControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Activity $activity */ - $activity = $em->getRepository(Activity::class)->find(1); + $activity = $em->getRepository(Activity::class)->findAll()[0]; self::assertEquals(0, $activity->getTeams()->count()); + $id = $activity->getId(); $fixture = new TeamFixtures(); $fixture->setAmount(2); $fixture->setAddCustomer(false); $this->importFixture($fixture); - $this->assertAccessIsGranted($client, '/admin/activity/1/permissions'); + $this->assertAccessIsGranted($client, '/admin/activity/' . $id . '/permissions'); $form = $client->getCrawler()->filter('form[name=activity_team_permission_form]')->form(); /** @var ChoiceFormField $team1 */ $team1 = $form->get('activity_team_permission_form[teams][0]'); @@ -263,7 +268,7 @@ class ActivityControllerTest extends ControllerBaseTest $this->assertHasDataTable($client); /** @var Activity $activity */ - $activity = $em->getRepository(Activity::class)->find(1); + $activity = $em->getRepository(Activity::class)->find($id); self::assertEquals(2, $activity->getTeams()->count()); } @@ -342,10 +347,9 @@ class ActivityControllerTest extends ControllerBaseTest $this->assertHasFlashDeleteSuccess($client); $this->assertHasNoEntriesWithFilter($client); - // SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion - // $em->clear(); - // $timesheets = $em->getRepository(Timesheet::class)->findAll(); - // $this->assertEquals(0, count($timesheets)); + $em->clear(); + $timesheets = $em->getRepository(Timesheet::class)->findAll(); + $this->assertEquals(0, \count($timesheets)); $this->request($client, '/admin/activity/1/edit'); $this->assertFalse($client->getResponse()->isSuccessful()); @@ -364,7 +368,9 @@ class ActivityControllerTest extends ControllerBaseTest $this->importFixture($fixture); $fixture = new ActivityFixtures(); $fixture->setAmount(1)->setIsGlobal(true)->setIsVisible(true); - $this->importFixture($fixture); + $activities = $this->importFixture($fixture); + $activity = $activities[0]; + $id = $activity->getId(); $timesheets = $em->getRepository(Timesheet::class)->findAll(); $this->assertEquals(10, \count($timesheets)); @@ -381,7 +387,7 @@ class ActivityControllerTest extends ControllerBaseTest $this->assertStringEndsWith($this->createUrl('/admin/activity/1/delete'), $form->getUri()); $client->submit($form, [ 'form' => [ - 'activity' => 2 + 'activity' => $id ] ]); @@ -395,7 +401,7 @@ class ActivityControllerTest extends ControllerBaseTest /** @var Timesheet $entry */ foreach ($timesheets as $entry) { - $this->assertEquals(2, $entry->getActivity()->getId()); + $this->assertEquals($id, $entry->getActivity()->getId()); } $this->request($client, '/admin/activity/1/edit'); diff --git a/tests/Controller/CustomerControllerTest.php b/tests/Controller/CustomerControllerTest.php index f0f7bcb8..2795b064 100644 --- a/tests/Controller/CustomerControllerTest.php +++ b/tests/Controller/CustomerControllerTest.php @@ -10,6 +10,7 @@ namespace App\Tests\Controller; use App\Entity\Customer; +use App\Entity\CustomerComment; use App\Entity\CustomerMeta; use App\Entity\Timesheet; use App\Entity\User; @@ -176,9 +177,12 @@ class CustomerControllerTest extends ControllerBaseTest $node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-msg'); self::assertStringContainsString('Blah foo bar', $node->html()); $node = $client->getCrawler()->filter('div.box#comments_box .box-body a.confirmation-link'); - self::assertEquals($this->createUrl('/admin/customer/1/comment_delete'), $node->attr('href')); + self::assertStringEndsWith('/comment_delete', $node->attr('href')); - $this->request($client, '/admin/customer/1/comment_delete'); + $comments = $this->getEntityManager()->getRepository(CustomerComment::class)->findAll(); + $id = $comments[0]->getId(); + + $this->request($client, '/admin/customer/' . $id . '/comment_delete'); $this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details')); $client->followRedirect(); $node = $client->getCrawler()->filter('div.box#comments_box .box-body'); @@ -202,12 +206,15 @@ class CustomerControllerTest extends ControllerBaseTest $node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text a.btn.active'); self::assertEquals(0, $node->count()); - $this->request($client, '/admin/customer/1/comment_pin'); + $comments = $this->getEntityManager()->getRepository(CustomerComment::class)->findAll(); + $id = $comments[0]->getId(); + + $this->request($client, '/admin/customer/' . $id . '/comment_pin'); $this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details')); $client->followRedirect(); $node = $client->getCrawler()->filter('div.box#comments_box .box-body a.btn.active'); self::assertEquals(1, $node->count()); - self::assertEquals($this->createUrl('/admin/customer/1/comment_pin'), $node->attr('href')); + self::assertEquals($this->createUrl('/admin/customer/' . $id . '/comment_pin'), $node->attr('href')); } public function testCreateDefaultTeamAction() @@ -278,7 +285,7 @@ class CustomerControllerTest extends ControllerBaseTest 'name' => 'Test Customer', ] ]); - $this->assertIsRedirect($client, $this->createUrl('/admin/customer/2/details')); + $this->assertIsRedirect($client, '/details'); $client->followRedirect(); $this->assertHasFlashSuccess($client); } @@ -352,22 +359,24 @@ class CustomerControllerTest extends ControllerBaseTest $fixture = new CustomerFixtures(); $fixture->setAmount(1); - $this->importFixture($fixture); + $customers = $this->importFixture($fixture); + $customer = $customers[0]; + $id = $customer->getId(); - $this->request($client, '/admin/customer/2/edit'); + $this->request($client, '/admin/customer/' . $id . '/edit'); $this->assertTrue($client->getResponse()->isSuccessful()); - $this->request($client, '/admin/customer/2/delete'); + $this->request($client, '/admin/customer/' . $id . '/delete'); $this->assertTrue($client->getResponse()->isSuccessful()); $form = $client->getCrawler()->filter('form[name=form]')->form(); - $this->assertStringEndsWith($this->createUrl('/admin/customer/2/delete'), $form->getUri()); + $this->assertStringEndsWith($this->createUrl('/admin/customer/' . $id . '/delete'), $form->getUri()); $client->submit($form); $client->followRedirect(); $this->assertHasDataTable($client); $this->assertHasFlashSuccess($client); - $this->request($client, '/admin/customer/2/edit'); + $this->request($client, '/admin/customer/' . $id . '/edit'); $this->assertFalse($client->getResponse()->isSuccessful()); } @@ -401,10 +410,9 @@ class CustomerControllerTest extends ControllerBaseTest $this->assertHasFlashDeleteSuccess($client); $this->assertHasNoEntriesWithFilter($client); - // SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion - // $em->clear(); - // $timesheets = $em->getRepository(Timesheet::class)->findAll(); - // $this->assertEquals(0, count($timesheets)); + $em->clear(); + $timesheets = $em->getRepository(Timesheet::class)->findAll(); + $this->assertEquals(0, \count($timesheets)); $this->request($client, '/admin/customer/1/edit'); $this->assertFalse($client->getResponse()->isSuccessful()); @@ -421,7 +429,9 @@ class CustomerControllerTest extends ControllerBaseTest $this->importFixture($fixture); $fixture = new CustomerFixtures(); $fixture->setAmount(1)->setIsVisible(true); - $this->importFixture($fixture); + $customers = $this->importFixture($fixture); + $customer = $customers[0]; + $id = $customer->getId(); $timesheets = $em->getRepository(Timesheet::class)->findAll(); $this->assertEquals(10, \count($timesheets)); @@ -438,7 +448,7 @@ class CustomerControllerTest extends ControllerBaseTest $this->assertStringEndsWith($this->createUrl('/admin/customer/1/delete'), $form->getUri()); $client->submit($form, [ 'form' => [ - 'customer' => 2 + 'customer' => $id ] ]); @@ -452,7 +462,7 @@ class CustomerControllerTest extends ControllerBaseTest /** @var Timesheet $entry */ foreach ($timesheets as $entry) { - $this->assertEquals(2, $entry->getProject()->getCustomer()->getId()); + $this->assertEquals($id, $entry->getProject()->getCustomer()->getId()); } $this->request($client, '/admin/customer/1/edit'); diff --git a/tests/Controller/InvoiceControllerTest.php b/tests/Controller/InvoiceControllerTest.php index b43e7f79..fe0cf2c8 100644 --- a/tests/Controller/InvoiceControllerTest.php +++ b/tests/Controller/InvoiceControllerTest.php @@ -9,13 +9,13 @@ namespace App\Tests\Controller; +use App\Entity\Invoice; use App\Entity\InvoiceTemplate; use App\Entity\Timesheet; use App\Entity\User; use App\Form\Type\DateRangeType; -use App\Tests\DataFixtures\InvoiceFixtures; +use App\Tests\DataFixtures\InvoiceTemplateFixtures; use App\Tests\DataFixtures\TimesheetFixtures; -use Doctrine\ORM\EntityManager; use Symfony\Component\HttpFoundation\BinaryFileResponse; /** @@ -69,10 +69,11 @@ class InvoiceControllerTest extends ControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); - $fixture = new InvoiceFixtures(); - $this->importFixture($fixture); + $fixture = new InvoiceTemplateFixtures(); + $templates = $this->importFixture($fixture); + $id = $templates[0]->getId(); - $this->request($client, '/invoice/?customer=1&template=1&preview='); + $this->request($client, '/invoice/?customer=1&template=' . $id . '&preview='); $this->assertTrue($client->getResponse()->isSuccessful()); $this->assertHasNoEntriesWithFilter($client); @@ -82,7 +83,7 @@ class InvoiceControllerTest extends ControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $fixture = new InvoiceFixtures(); + $fixture = new InvoiceTemplateFixtures(); $this->importFixture($fixture); $this->request($client, '/invoice/template'); @@ -117,15 +118,13 @@ class InvoiceControllerTest extends ControllerBaseTest public function testCopyTemplateAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $em = $this->getEntityManager(); - - $fixture = new InvoiceFixtures(); - $this->importFixture($fixture); + $fixture = new InvoiceTemplateFixtures(); + $templates = $this->importFixture($fixture); /** @var InvoiceTemplate $template */ - $template = $em->getRepository(InvoiceTemplate::class)->find(1); + $template = $templates[0]; - $this->request($client, '/invoice/template/create/1'); + $this->request($client, '/invoice/template/create/' . $template->getId()); $this->assertTrue($client->getResponse()->isSuccessful()); $form = $client->getCrawler()->filter('form[name=invoice_template_form]')->form(); @@ -144,11 +143,10 @@ class InvoiceControllerTest extends ControllerBaseTest public function testCreateAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); - /** @var EntityManager $em */ - $em = $this->getEntityManager(); - $fixture = new InvoiceFixtures(); - $this->importFixture($fixture); + $fixture = new InvoiceTemplateFixtures(); + $templates = $this->importFixture($fixture); + $template = $templates[0]; $begin = new \DateTime('first day of this month'); $end = new \DateTime('last day of this month'); @@ -158,7 +156,10 @@ class InvoiceControllerTest extends ControllerBaseTest ->setAmount(20) ->setStartDate($begin) ; - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); + foreach ($timesheets as $timesheet) { + $this->assertFalse($timesheet->isExported()); + } $this->request($client, '/invoice/'); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -170,7 +171,7 @@ class InvoiceControllerTest extends ControllerBaseTest $node->setAttribute('action', $this->createUrl('/invoice/?preview=')); $node->setAttribute('method', 'GET'); $client->submit($form, [ - 'template' => 1, + 'template' => $template->getId(), 'daterange' => $dateRange, 'customer' => 1, ]); @@ -188,7 +189,7 @@ class InvoiceControllerTest extends ControllerBaseTest $node->setAttribute('action', $this->createUrl('/invoice/?create=')); $node->setAttribute('method', 'GET'); $client->submit($form, [ - 'template' => 1, + 'template' => $template->getId(), 'daterange' => $dateRange, 'customer' => 1, 'projects' => [1], @@ -200,7 +201,10 @@ class InvoiceControllerTest extends ControllerBaseTest $this->assertEquals(1, $node->count()); $this->assertEquals('invoice_print', $node->getIterator()[0]->getAttribute('class')); + $em = $this->getEntityManager(); + $em->clear(); $timesheets = $em->getRepository(Timesheet::class)->findAll(); + $this->assertCount(20, $timesheets); /** @var Timesheet $timesheet */ foreach ($timesheets as $timesheet) { $this->assertTrue($timesheet->isExported()); @@ -210,11 +214,10 @@ class InvoiceControllerTest extends ControllerBaseTest public function testPrintAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); - /** @var EntityManager $em */ - $em = $this->getEntityManager(); - $fixture = new InvoiceFixtures(); - $this->importFixture($fixture); + $fixture = new InvoiceTemplateFixtures(); + $templates = $this->importFixture($fixture); + $id = $templates[0]->getId(); $begin = new \DateTime('first day of this month'); $end = new \DateTime('last day of this month'); @@ -236,7 +239,7 @@ class InvoiceControllerTest extends ControllerBaseTest $node->setAttribute('action', $this->createUrl('/invoice/?preview=')); $node->setAttribute('method', 'GET'); $client->submit($form, [ - 'template' => 1, + 'template' => $id, 'daterange' => $dateRange, 'customer' => 1, ]); @@ -254,7 +257,7 @@ class InvoiceControllerTest extends ControllerBaseTest $node->setAttribute('action', $this->createUrl('/invoice/?print=')); $node->setAttribute('method', 'GET'); $client->submit($form, [ - 'template' => 1, + 'template' => $id, 'daterange' => $dateRange, 'customer' => 1, 'projects' => [1], @@ -269,11 +272,10 @@ class InvoiceControllerTest extends ControllerBaseTest public function testCreateActionAsAdminWithDownloadAndStatusChangeAndDelete() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - /** @var EntityManager $em */ - $em = $this->getEntityManager(); - $fixture = new InvoiceFixtures(); - $this->importFixture($fixture); + $fixture = new InvoiceTemplateFixtures(); + $templates = $this->importFixture($fixture); + $template = $templates[0]; $begin = new \DateTime('first day of this month'); $end = new \DateTime('last day of this month'); @@ -295,7 +297,7 @@ class InvoiceControllerTest extends ControllerBaseTest $node->setAttribute('action', $this->createUrl('/invoice/?preview=')); $node->setAttribute('method', 'GET'); $client->submit($form, [ - 'template' => 1, + 'template' => $template->getId(), 'daterange' => $dateRange, 'customer' => 1, ]); @@ -313,14 +315,17 @@ class InvoiceControllerTest extends ControllerBaseTest $node->setAttribute('action', $this->createUrl('/invoice/?create=')); $node->setAttribute('method', 'GET'); $client->submit($form, [ - 'template' => 1, + 'template' => $template->getId(), 'daterange' => $dateRange, 'customer' => 1, 'projects' => [1], 'markAsExported' => 1, ]); - $this->assertIsRedirect($client, '/invoice/show?id=1'); + $invoices = $this->getEntityManager()->getRepository(Invoice::class)->findAll(); + $id = $invoices[0]->getId(); + + $this->assertIsRedirect($client, '/invoice/show?id=' . $id); $client->followRedirect(); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -330,28 +335,28 @@ class InvoiceControllerTest extends ControllerBaseTest $this->assertDataTableRowCount($client, 'datatable_invoices', 1); // make sure the invoice is saved - $this->request($client, '/invoice/download/1'); + $this->request($client, '/invoice/download/' . $id); $response = $client->getResponse(); $this->assertTrue($response->isSuccessful()); self::assertInstanceOf(BinaryFileResponse::class, $response); self::assertFileExists($response->getFile()); - $this->request($client, '/invoice/change-status/1/pending'); + $this->request($client, '/invoice/change-status/' . $id . '/pending'); $this->assertIsRedirect($client, '/invoice/show'); $client->followRedirect(); $this->assertTrue($client->getResponse()->isSuccessful()); - $this->request($client, '/invoice/change-status/1/paid'); + $this->request($client, '/invoice/change-status/' . $id . '/paid'); $this->assertIsRedirect($client, '/invoice/show'); $client->followRedirect(); $this->assertTrue($client->getResponse()->isSuccessful()); - $this->request($client, '/invoice/change-status/1/new'); + $this->request($client, '/invoice/change-status/' . $id . '/new'); $this->assertIsRedirect($client, '/invoice/show'); $client->followRedirect(); $this->assertTrue($client->getResponse()->isSuccessful()); - $this->request($client, '/invoice/delete/1'); + $this->request($client, '/invoice/delete/' . $id); $this->assertIsRedirect($client, '/invoice/show'); $client->followRedirect(); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -361,10 +366,11 @@ class InvoiceControllerTest extends ControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $fixture = new InvoiceFixtures(); - $this->importFixture($fixture); + $fixture = new InvoiceTemplateFixtures(); + $template = $this->importFixture($fixture); + $id = $template[0]->getId(); - $this->request($client, '/invoice/template/1/edit?page=1'); + $this->request($client, '/invoice/template/' . $id . '/edit?page=1'); $form = $client->getCrawler()->filter('form[name=invoice_template_form]')->form(); $client->submit($form, [ 'invoice_template_form' => [ @@ -387,26 +393,25 @@ class InvoiceControllerTest extends ControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $em = $this->getEntityManager(); - $fixture = new InvoiceFixtures(); - $this->importFixture($fixture); + $fixture = new InvoiceTemplateFixtures(); + $template = $this->importFixture($fixture); + $id = $template[0]->getId(); - $this->request($client, '/invoice/template/1/delete'); + $this->request($client, '/invoice/template/' . $id . '/delete'); $this->assertIsRedirect($client, '/invoice/template'); $client->followRedirect(); $this->assertTrue($client->getResponse()->isSuccessful()); $this->assertHasFlashSuccess($client); - $this->assertEquals(0, $em->getRepository(InvoiceTemplate::class)->count([])); + $this->assertEquals(0, $this->getEntityManager()->getRepository(InvoiceTemplate::class)->count([])); } public function testUploadDocumentAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); - $em = $this->getEntityManager(); - $fixture = new InvoiceFixtures(); + $fixture = new InvoiceTemplateFixtures(); $this->importFixture($fixture); $this->request($client, '/invoice/document_upload'); diff --git a/tests/Controller/PermissionControllerTest.php b/tests/Controller/PermissionControllerTest.php index 232b68ad..a73a6ccf 100644 --- a/tests/Controller/PermissionControllerTest.php +++ b/tests/Controller/PermissionControllerTest.php @@ -10,6 +10,7 @@ namespace App\Tests\Controller; use App\DataFixtures\UserFixtures; +use App\Entity\Role; use App\Entity\RolePermission; use App\Entity\User; use Symfony\Component\Security\Csrf\CsrfToken; @@ -104,6 +105,15 @@ class PermissionControllerTest extends ControllerBaseTest $this->assertIsRedirect($client, $this->createUrl('/admin/permissions')); $client->followRedirect(); + $roles = $this->getEntityManager()->getRepository(Role::class)->findAll(); + $id = null; + foreach ($roles as $role) { + if ($role->getName() === 'TEST_ROLE') { + $id = $role->getId(); + break; + } + } + $content = $client->getResponse()->getContent(); self::assertStringContainsString('', $content); @@ -126,7 +136,7 @@ class PermissionControllerTest extends ControllerBaseTest /** @var CsrfToken $token */ $token = static::$kernel->getContainer()->get('security.csrf.token_manager')->getToken('user_role_permissions'); - $this->request($client, '/admin/permissions/roles/1/delete/' . $token->getValue()); + $this->request($client, '/admin/permissions/roles/' . $id . '/delete/' . $token->getValue()); $this->assertIsRedirect($client, $this->createUrl('/admin/permissions')); $client->followRedirect(); @@ -166,9 +176,18 @@ class PermissionControllerTest extends ControllerBaseTest $rolePermissions = $em->getRepository(RolePermission::class)->findAll(); $this->assertEquals(0, \count($rolePermissions)); + $roles = $em->getRepository(Role::class)->findAll(); + $id = null; + foreach ($roles as $role) { + if ($role->getName() === 'TEST_ROLE') { + $id = $role->getId(); + break; + } + } + // create the permission $token = static::$kernel->getContainer()->get('security.csrf.token_manager')->getToken('user_role_permissions'); - $this->request($client, '/admin/permissions/roles/1/view_user/1/' . $token->getValue(), 'POST'); + $this->request($client, '/admin/permissions/roles/' . $id . '/view_user/1/' . $token->getValue(), 'POST'); self::assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); @@ -182,14 +201,14 @@ class PermissionControllerTest extends ControllerBaseTest self::assertEquals('view_user', $permission->getPermission()); self::assertTrue($permission->isAllowed()); self::assertEquals('TEST_ROLE', $permission->getRole()->getName()); - self::assertEquals(1, $permission->getRole()->getId()); + self::assertEquals($id, $permission->getRole()->getId()); // flush the cache to prevent wrong results $em->clear(); // update the permission $token = static::$kernel->getContainer()->get('security.csrf.token_manager')->getToken('user_role_permissions'); - $this->request($client, '/admin/permissions/roles/1/view_user/0/' . $token->getValue(), 'POST'); + $this->request($client, '/admin/permissions/roles/' . $id . '/view_user/0/' . $token->getValue(), 'POST'); self::assertTrue($client->getResponse()->isSuccessful()); $result = json_decode($client->getResponse()->getContent(), true); diff --git a/tests/Controller/ProjectControllerTest.php b/tests/Controller/ProjectControllerTest.php index bb5d3c1d..d04d058a 100644 --- a/tests/Controller/ProjectControllerTest.php +++ b/tests/Controller/ProjectControllerTest.php @@ -13,6 +13,7 @@ use App\Entity\Activity; use App\Entity\ActivityMeta; use App\Entity\ActivityRate; use App\Entity\Project; +use App\Entity\ProjectComment; use App\Entity\ProjectMeta; use App\Entity\ProjectRate; use App\Entity\Team; @@ -213,7 +214,7 @@ class ProjectControllerTest extends ControllerBaseTest $em->persist($rate); $this->request($client, '/admin/project/1/duplicate'); - $this->assertIsRedirect($client, $this->createUrl('/admin/project/2/details')); + $this->assertIsRedirect($client, '/details'); $client->followRedirect(); $node = $client->getCrawler()->filter('div.box#project_rates_box'); self::assertEquals(1, $node->count()); @@ -253,9 +254,12 @@ class ProjectControllerTest extends ControllerBaseTest $node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text'); self::assertStringContainsString('Foo bar blub', $node->html()); $node = $client->getCrawler()->filter('div.box#comments_box .box-body a.confirmation-link'); - self::assertEquals($this->createUrl('/admin/project/1/comment_delete'), $node->attr('href')); - $this->request($client, '/admin/project/1/comment_delete'); + $comments = $this->getEntityManager()->getRepository(ProjectComment::class)->findAll(); + $id = $comments[0]->getId(); + + self::assertEquals($this->createUrl('/admin/project/' . $id . '/comment_delete'), $node->attr('href')); + $this->request($client, '/admin/project/' . $id . '/comment_delete'); $this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details')); $client->followRedirect(); $node = $client->getCrawler()->filter('div.box#comments_box .box-body'); @@ -279,12 +283,15 @@ class ProjectControllerTest extends ControllerBaseTest $node = $client->getCrawler()->filter('div.box#comments_box .box-body a.btn.active'); self::assertEquals(0, $node->count()); - $this->request($client, '/admin/project/1/comment_pin'); + $comments = $this->getEntityManager()->getRepository(ProjectComment::class)->findAll(); + $id = $comments[0]->getId(); + + $this->request($client, '/admin/project/' . $id . '/comment_pin'); $this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details')); $client->followRedirect(); $node = $client->getCrawler()->filter('div.box#comments_box .box-body a.btn.active'); self::assertEquals(1, $node->count()); - self::assertEquals($this->createUrl('/admin/project/1/comment_pin'), $node->attr('href')); + self::assertEquals($this->createUrl('/admin/project/' . $id . '/comment_pin'), $node->attr('href')); } public function testCreateDefaultTeamAction() @@ -346,7 +353,7 @@ class ProjectControllerTest extends ControllerBaseTest 'customer' => 1, ] ]); - $this->assertIsRedirect($client, $this->createUrl('/admin/project/2/details')); + $this->assertIsRedirect($client, '/details'); $client->followRedirect(); $this->assertHasFlashSuccess($client); } @@ -418,22 +425,24 @@ class ProjectControllerTest extends ControllerBaseTest $fixture = new ProjectFixtures(); $fixture->setAmount(1); - $this->importFixture($fixture); + /** @var Project[] $projects */ + $projects = $this->importFixture($fixture); + $id = $projects[0]->getId(); - $this->request($client, '/admin/project/2/edit'); + $this->request($client, '/admin/project/' . $id . '/edit'); $this->assertTrue($client->getResponse()->isSuccessful()); - $this->request($client, '/admin/project/2/delete'); + $this->request($client, '/admin/project/' . $id . '/delete'); $this->assertTrue($client->getResponse()->isSuccessful()); $form = $client->getCrawler()->filter('form[name=form]')->form(); - $this->assertStringEndsWith($this->createUrl('/admin/project/2/delete'), $form->getUri()); + $this->assertStringEndsWith($this->createUrl('/admin/project/' . $id . '/delete'), $form->getUri()); $client->submit($form); $client->followRedirect(); $this->assertHasDataTable($client); $this->assertHasFlashSuccess($client); - $this->request($client, '/admin/project/2/edit'); + $this->request($client, '/admin/project/' . $id . '/edit'); $this->assertFalse($client->getResponse()->isSuccessful()); } @@ -467,10 +476,9 @@ class ProjectControllerTest extends ControllerBaseTest $this->assertHasFlashDeleteSuccess($client); $this->assertHasNoEntriesWithFilter($client); - // SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion - // $em->clear(); - // $timesheets = $em->getRepository(Timesheet::class)->findAll(); - // $this->assertEquals(0, count($timesheets)); + $em->clear(); + $timesheets = $em->getRepository(Timesheet::class)->findAll(); + $this->assertEquals(0, \count($timesheets)); $this->request($client, '/admin/project/1/edit'); $this->assertFalse($client->getResponse()->isSuccessful()); @@ -487,7 +495,8 @@ class ProjectControllerTest extends ControllerBaseTest $this->importFixture($fixture); $fixture = new ProjectFixtures(); $fixture->setAmount(1)->setIsVisible(true); - $this->importFixture($fixture); + $projects = $this->importFixture($fixture); + $id = $projects[0]->getId(); $timesheets = $em->getRepository(Timesheet::class)->findAll(); $this->assertEquals(10, \count($timesheets)); @@ -504,7 +513,7 @@ class ProjectControllerTest extends ControllerBaseTest $this->assertStringEndsWith($this->createUrl('/admin/project/1/delete'), $form->getUri()); $client->submit($form, [ 'form' => [ - 'project' => 2 + 'project' => $id ] ]); @@ -518,7 +527,7 @@ class ProjectControllerTest extends ControllerBaseTest /** @var Timesheet $entry */ foreach ($timesheets as $entry) { - $this->assertEquals(2, $entry->getProject()->getId()); + $this->assertEquals($id, $entry->getProject()->getId()); } $this->request($client, '/admin/project/1/edit'); diff --git a/tests/Controller/TagControllerTest.php b/tests/Controller/TagControllerTest.php index 1a6613ab..05bd30f5 100644 --- a/tests/Controller/TagControllerTest.php +++ b/tests/Controller/TagControllerTest.php @@ -12,21 +12,24 @@ namespace App\Tests\Controller; use App\Entity\Tag; use App\Entity\User; use App\Tests\DataFixtures\TagFixtures; -use Symfony\Component\HttpKernel\HttpKernelBrowser; /** * @group integration */ class TagControllerTest extends ControllerBaseTest { - protected function importTags(HttpKernelBrowser $client): void + /** + * @return Tag[] + */ + protected function importTags(): array { $tagList = ['Test', 'Administration', 'Support', '#2018-001', '#2018-002', '#2018-003', 'Development', 'Marketing', 'First Level Support', 'Bug Fixing']; $fixture = new TagFixtures(); $fixture->setTagArray($tagList); - $this->importFixture($fixture); + + return $this->importFixture($fixture); } public function testIsSecure() @@ -37,7 +40,7 @@ class TagControllerTest extends ControllerBaseTest public function testIndexAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); - $this->importTags($client); + $this->importTags(); $this->assertAccessIsGranted($client, '/admin/tags/'); $this->assertHasDataTable($client); @@ -47,7 +50,7 @@ class TagControllerTest extends ControllerBaseTest public function testIndexActionWithSearchTermQuery() { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); - $this->importTags($client); + $this->importTags(); $this->request($client, '/admin/tags/'); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -76,7 +79,9 @@ class TagControllerTest extends ControllerBaseTest $client->followRedirect(); $this->assertHasDataTable($client); - $this->request($client, '/admin/tags/1/edit'); + $id = $this->getEntityManager()->getRepository(Tag::class)->findAll()[0]->getId(); + + $this->request($client, '/admin/tags/' . $id . '/edit'); $editForm = $client->getCrawler()->filter('form[name=tag_edit_form]')->form(); $this->assertEquals('A tAG Name!', $editForm->get('tag_edit_form[name]')->getValue()); } @@ -84,9 +89,10 @@ class TagControllerTest extends ControllerBaseTest public function testEditAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); - $this->importTags($client); + $tags = $this->importTags(); + $id = $tags[0]->getId(); - $this->assertAccessIsGranted($client, '/admin/tags/1/edit'); + $this->assertAccessIsGranted($client, '/admin/tags/' . $id . '/edit'); $form = $client->getCrawler()->filter('form[name=tag_edit_form]')->form(); $client->submit($form, [ 'tag_edit_form' => ['name' => 'Test 2 updated'] @@ -94,7 +100,7 @@ class TagControllerTest extends ControllerBaseTest $this->assertIsRedirect($client, $this->createUrl('/admin/tags/')); $client->followRedirect(); $this->assertHasDataTable($client); - $this->request($client, '/admin/tags/1/edit'); + $this->request($client, '/admin/tags/' . $id . '/edit'); $editForm = $client->getCrawler()->filter('form[name=tag_edit_form]')->form(); $this->assertEquals('Test 2 updated', $editForm->get('tag_edit_form[name]')->getValue()); } @@ -102,7 +108,7 @@ class TagControllerTest extends ControllerBaseTest public function testMultiDeleteAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); - $this->importTags($client); + $this->importTags(); $this->assertAccessIsGranted($client, '/admin/tags/'); diff --git a/tests/Controller/TeamControllerTest.php b/tests/Controller/TeamControllerTest.php index e75d11ad..aeee1b95 100644 --- a/tests/Controller/TeamControllerTest.php +++ b/tests/Controller/TeamControllerTest.php @@ -78,16 +78,15 @@ class TeamControllerTest extends ControllerBaseTest $this->assertAccessIsGranted($client, '/admin/teams/create'); $form = $client->getCrawler()->filter('form[name=team_edit_form]')->form(); - $editForm = $client->getCrawler()->filter('form[name=team_edit_form]')->form(); - $this->assertEquals('', $editForm->get('team_edit_form[name]')->getValue()); - $this->assertEquals('5', $editForm->get('team_edit_form[teamlead]')->getValue()); + $this->assertEquals('', $form->get('team_edit_form[name]')->getValue()); + $this->assertEquals('5', $form->get('team_edit_form[teamlead]')->getValue()); $client->submit($form, [ 'team_edit_form' => [ - 'name' => 'Test Team', + 'name' => 'Test Team' . uniqid(), ] ]); - $this->assertIsRedirect($client, $this->createUrl('/admin/teams/2/edit')); + $this->assertIsRedirect($client, '/edit'); $client->followRedirect(); $this->assertHasFlashSuccess($client); $this->assertHasCustomerAndProjectPermissionBoxes($client); @@ -210,7 +209,7 @@ class TeamControllerTest extends ControllerBaseTest { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $this->request($client, '/admin/teams/1/duplicate'); - $this->assertIsRedirect($client, $this->createUrl('/admin/teams/2/edit')); + $this->assertIsRedirect($client, '/edit'); $client->followRedirect(); $node = $client->getCrawler()->filter('#team_edit_form_name'); self::assertEquals(1, $node->count()); diff --git a/tests/Controller/TimesheetControllerTest.php b/tests/Controller/TimesheetControllerTest.php index 20beacd8..8de64993 100644 --- a/tests/Controller/TimesheetControllerTest.php +++ b/tests/Controller/TimesheetControllerTest.php @@ -181,7 +181,7 @@ class TimesheetControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertNull($timesheet->getEnd()); $this->assertEquals('Testing is fun!', $timesheet->getDescription()); @@ -218,7 +218,7 @@ class TimesheetControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertInstanceOf(\DateTime::class, $timesheet->getEnd()); $this->assertEquals($expectedDuration, $timesheet->getDuration()); @@ -293,7 +293,7 @@ class TimesheetControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertInstanceOf(\DateTime::class, $timesheet->getEnd()); $this->assertEquals(50, $timesheet->getRate()); @@ -327,7 +327,7 @@ class TimesheetControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertInstanceOf(\DateTime::class, $timesheet->getEnd()); $this->assertEquals(50, $timesheet->getRate()); @@ -428,7 +428,7 @@ class TimesheetControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertInstanceOf(\DateTime::class, $timesheet->getEnd()); $this->assertEquals(800, $timesheet->getRate()); @@ -450,9 +450,10 @@ class TimesheetControllerTest extends ControllerBaseTest $fixture->setAmount(10); $fixture->setUser($this->getUserByRole(User::ROLE_USER)); $fixture->setStartDate('2017-05-01'); - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); + $id = $timesheets[0]->getId(); - $this->request($client, '/timesheet/1/edit'); + $this->request($client, '/timesheet/' . $id . '/edit'); $response = $client->getResponse(); $this->assertTrue($response->isSuccessful()); @@ -478,7 +479,7 @@ class TimesheetControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertEquals('foo-bar', $timesheet->getDescription()); } diff --git a/tests/Controller/TimesheetTeamControllerTest.php b/tests/Controller/TimesheetTeamControllerTest.php index fbaa37ca..fa90d41e 100644 --- a/tests/Controller/TimesheetTeamControllerTest.php +++ b/tests/Controller/TimesheetTeamControllerTest.php @@ -192,7 +192,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->findAll()[0]; $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertNull($timesheet->getEnd()); $this->assertEquals('Testing is fun!', $timesheet->getDescription()); @@ -271,9 +271,10 @@ class TimesheetTeamControllerTest extends ControllerBaseTest $fixture->setAmount(10); $fixture->setUser($user); $fixture->setStartDate('2017-05-01'); - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); + $id = $timesheets[0]->getId(); - $this->request($client, '/team/timesheet/1/edit'); + $this->request($client, '/team/timesheet/' . $id . '/edit'); $response = $client->getResponse(); $this->assertTrue($response->isSuccessful()); @@ -300,7 +301,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ - $timesheet = $em->getRepository(Timesheet::class)->find(1); + $timesheet = $em->getRepository(Timesheet::class)->find($id); $this->assertEquals('foo-bar', $timesheet->getDescription()); $this->assertEquals($teamlead->getId(), $timesheet->getUser()->getId()); } diff --git a/tests/Controller/UserControllerTest.php b/tests/Controller/UserControllerTest.php index d1d78037..a5050ecf 100644 --- a/tests/Controller/UserControllerTest.php +++ b/tests/Controller/UserControllerTest.php @@ -98,7 +98,7 @@ class UserControllerTest extends ControllerBaseTest public function testCreateAction() { - $username = '亚历山德拉'; + $username = '亚历山德拉' . uniqid(); $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/admin/user/create'); $form = $client->getCrawler()->filter('form[name=user_create]')->form(); @@ -188,10 +188,9 @@ class UserControllerTest extends ControllerBaseTest $client->followRedirect(); $this->assertHasFlashDeleteSuccess($client); - // SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion - // $em->clear(); - // $timesheets = $em->getRepository(Timesheet::class)->count([]); - // $this->assertEquals(0, $timesheets); + $em->clear(); + $timesheets = $em->getRepository(Timesheet::class)->count([]); + $this->assertEquals(0, $timesheets); $this->request($client, '/admin/user/' . $user->getId() . '/edit'); $this->assertFalse($client->getResponse()->isSuccessful()); diff --git a/tests/DataFixtures/ActivityFixtures.php b/tests/DataFixtures/ActivityFixtures.php index 8d3dc466..a47149c8 100644 --- a/tests/DataFixtures/ActivityFixtures.php +++ b/tests/DataFixtures/ActivityFixtures.php @@ -11,14 +11,13 @@ namespace App\Tests\DataFixtures; use App\Entity\Activity; use App\Entity\Project; -use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** * Defines the sample data to load in during controller tests. */ -final class ActivityFixtures extends Fixture +final class ActivityFixtures implements TestFixture { /** * @var int @@ -95,10 +94,13 @@ final class ActivityFixtures extends Fixture } /** - * {@inheritdoc} + * @param ObjectManager $manager + * @return Activity[] */ - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): array { + $created = []; + $projects = $this->projects; if (empty($projects)) { $projects = $this->getAllProjects($manager); @@ -126,16 +128,20 @@ final class ActivityFixtures extends Fixture \call_user_func($this->callback, $activity); } $manager->persist($activity); + + $created[] = $activity; } $manager->flush(); + + return $created; } /** * @param ObjectManager $manager * @return array */ - protected function getAllProjects(ObjectManager $manager): array + private function getAllProjects(ObjectManager $manager): array { $all = []; /** @var Project[] $entries */ diff --git a/tests/DataFixtures/CustomerFixtures.php b/tests/DataFixtures/CustomerFixtures.php index 467b60b9..2e4d7291 100644 --- a/tests/DataFixtures/CustomerFixtures.php +++ b/tests/DataFixtures/CustomerFixtures.php @@ -10,14 +10,13 @@ namespace App\Tests\DataFixtures; use App\Entity\Customer; -use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** * Defines the sample data to load in during controller tests. */ -final class CustomerFixtures extends Fixture +final class CustomerFixtures implements TestFixture { /** * @var int @@ -65,10 +64,13 @@ final class CustomerFixtures extends Fixture } /** - * {@inheritdoc} + * @param ObjectManager $manager + * @return Customer[] */ - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): array { + $created = []; + $faker = Factory::create(); for ($i = 0; $i < $this->amount; $i++) { @@ -92,8 +94,11 @@ final class CustomerFixtures extends Fixture \call_user_func($this->callback, $customer); } $manager->persist($customer); + $created[] = $customer; } $manager->flush(); + + return $created; } } diff --git a/tests/DataFixtures/InvoiceFixtures.php b/tests/DataFixtures/InvoiceTemplateFixtures.php similarity index 83% rename from tests/DataFixtures/InvoiceFixtures.php rename to tests/DataFixtures/InvoiceTemplateFixtures.php index 9abfe0f3..471a759b 100644 --- a/tests/DataFixtures/InvoiceFixtures.php +++ b/tests/DataFixtures/InvoiceTemplateFixtures.php @@ -10,28 +10,22 @@ namespace App\Tests\DataFixtures; use App\Entity\InvoiceTemplate; -use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** * Defines the sample data to load in during controller tests. */ -class InvoiceFixtures extends Fixture +class InvoiceTemplateFixtures implements TestFixture { - /** - * {@inheritdoc} - */ - public function load(ObjectManager $manager) - { - $this->loadInvoiceTemplates($manager); - } - /** * @param ObjectManager $manager + * @return InvoiceTemplate[] */ - private function loadInvoiceTemplates(ObjectManager $manager) + public function load(ObjectManager $manager): array { + $created = []; + $faker = Factory::create(); $template = new InvoiceTemplate(); @@ -58,5 +52,9 @@ class InvoiceFixtures extends Fixture $manager->persist($template); $manager->flush(); + + $created[] = $template; + + return $created; } } diff --git a/tests/DataFixtures/ProjectFixtures.php b/tests/DataFixtures/ProjectFixtures.php index 0acde063..abb0bdf6 100644 --- a/tests/DataFixtures/ProjectFixtures.php +++ b/tests/DataFixtures/ProjectFixtures.php @@ -11,14 +11,13 @@ namespace App\Tests\DataFixtures; use App\Entity\Customer; use App\Entity\Project; -use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** * Defines the sample data to load in during controller tests. */ -final class ProjectFixtures extends Fixture +final class ProjectFixtures implements TestFixture { /** * @var int @@ -81,10 +80,12 @@ final class ProjectFixtures extends Fixture } /** - * {@inheritdoc} + * @param ObjectManager $manager + * @return Project[] */ - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): array { + $created = []; $customers = $this->customers; if (empty($customers)) { $customers = $this->getAllCustomers($manager); @@ -109,16 +110,19 @@ final class ProjectFixtures extends Fixture \call_user_func($this->callback, $project); } $manager->persist($project); + $created[] = $project; } $manager->flush(); + + return $created; } /** * @param ObjectManager $manager * @return array */ - protected function getAllCustomers(ObjectManager $manager): array + private function getAllCustomers(ObjectManager $manager): array { $all = []; /** @var Customer[] $entries */ diff --git a/tests/DataFixtures/TagFixtures.php b/tests/DataFixtures/TagFixtures.php index 4389dbf6..83e519ab 100644 --- a/tests/DataFixtures/TagFixtures.php +++ b/tests/DataFixtures/TagFixtures.php @@ -10,13 +10,12 @@ namespace App\Tests\DataFixtures; use App\Entity\Tag; -use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; /** * Defines the sample data to load in during controller tests. */ -final class TagFixtures extends Fixture +final class TagFixtures implements TestFixture { /** * @var string[] @@ -60,10 +59,13 @@ final class TagFixtures extends Fixture } /** - * {@inheritdoc} + * @param ObjectManager $manager + * @return Tag[] */ - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): array { + $created = []; + foreach ($this->getTagArray() as $tagName) { $tag = $this->createTagEntry($tagName); @@ -71,8 +73,11 @@ final class TagFixtures extends Fixture \call_user_func($this->callback, $tag); } $manager->persist($tag); + $created[] = $tag; } $manager->flush(); + + return $created; } private function createTagEntry(string $tagName): Tag diff --git a/tests/DataFixtures/TeamFixtures.php b/tests/DataFixtures/TeamFixtures.php index baa9aa4a..c7c665cd 100644 --- a/tests/DataFixtures/TeamFixtures.php +++ b/tests/DataFixtures/TeamFixtures.php @@ -12,14 +12,13 @@ namespace App\Tests\DataFixtures; use App\Entity\Customer; use App\Entity\Team; use App\Entity\User; -use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** * Defines the sample data to load in during controller tests. */ -final class TeamFixtures extends Fixture +final class TeamFixtures implements TestFixture { /** * @var int @@ -89,10 +88,13 @@ final class TeamFixtures extends Fixture } /** - * {@inheritdoc} + * @param ObjectManager $manager + * @return Team[] */ - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): array { + $created = []; + $faker = Factory::create(); $user = $this->getAllUsers($manager); $customer = $this->getAllCustomers($manager); @@ -131,9 +133,12 @@ final class TeamFixtures extends Fixture \call_user_func($this->callback, $team); } $manager->persist($team); + $created[] = $team; } $manager->flush(); + + return $created; } /** diff --git a/tests/DataFixtures/TestFixture.php b/tests/DataFixtures/TestFixture.php new file mode 100644 index 00000000..f61dd197 --- /dev/null +++ b/tests/DataFixtures/TestFixture.php @@ -0,0 +1,23 @@ +activities; if (empty($activities)) { $activities = $this->getAllActivities($manager); @@ -222,6 +224,12 @@ final class TimesheetFixtures extends Fixture $faker = Factory::create(); $user = $this->user; + $tags = $this->getTagObjectList(); + foreach ($tags as $tag) { + $manager->persist($tag); + } + $manager->flush(); + for ($i = 0; $i < $this->amount; $i++) { $description = $faker->text; if ($this->allowEmptyDescriptions) { @@ -239,8 +247,6 @@ final class TimesheetFixtures extends Fixture $project = $projects[array_rand($projects)]; } - $tags = $this->getTagObjectList($i); - $timesheet = $this->createTimesheetEntry( $user, $activity, @@ -254,6 +260,7 @@ final class TimesheetFixtures extends Fixture \call_user_func($this->callback, $timesheet); } $manager->persist($timesheet); + $created[] = $timesheet; } for ($i = 0; $i < $this->running; $i++) { @@ -264,8 +271,6 @@ final class TimesheetFixtures extends Fixture $project = $projects[array_rand($projects)]; } - $tags = $this->getTagObjectList($i); - $timesheet = $this->createTimesheetEntry( $user, $activity, @@ -280,24 +285,32 @@ final class TimesheetFixtures extends Fixture \call_user_func($this->callback, $timesheet); } $manager->persist($timesheet); + $created[] = $timesheet; } $manager->flush(); + + return $created; } - protected function getTagObjectList(int $cnt): array + private function getTagObjectList(): array { if (true === $this->useTags) { - $tagObject = new Tag(); - $tagObject->setName($this->tags[($cnt % \count($this->tags))]); + $all = []; + foreach ($this->tags as $tagName) { + $tagObject = new Tag(); + $tagObject->setName($tagName); - return [$tagObject]; + $all[] = $tagObject; + } + + return $all; } return []; } - protected function getDateTime(int $i): \DateTime + private function getDateTime(int $i): \DateTime { $start = \DateTime::createFromFormat('Y-m-d', $this->startDate); $start->modify("+ $i days"); @@ -310,7 +323,7 @@ final class TimesheetFixtures extends Fixture * @param ObjectManager $manager * @return array */ - protected function getAllActivities(ObjectManager $manager): array + private function getAllActivities(ObjectManager $manager): array { $all = []; /** @var Activity[] $entries */ @@ -326,7 +339,7 @@ final class TimesheetFixtures extends Fixture * @param ObjectManager $manager * @return array */ - protected function getAllProjects(ObjectManager $manager): array + private function getAllProjects(ObjectManager $manager): array { $all = []; /** @var Project[] $entries */ diff --git a/tests/Doctrine/SqliteSessionInitSubscriberTest.php b/tests/Doctrine/SqliteSessionInitSubscriberTest.php deleted file mode 100644 index 6bbc01b5..00000000 --- a/tests/Doctrine/SqliteSessionInitSubscriberTest.php +++ /dev/null @@ -1,71 +0,0 @@ -getSubscribedEvents(); - $this->assertTrue(\in_array(Events::postConnect, $events)); - } - - public function testPostConnectWithSqlite() - { - $sut = new SqliteSessionInitSubscriber(); - - $platformMock = $this->getMockBuilder(SqlitePlatform::class) - ->onlyMethods(['getName']) - ->disableOriginalConstructor() - ->getMock(); - - $platformMock->expects($this->once())->method('getName')->willReturn('sqlite'); - - $connectionMock = $this->createMock(Connection::class); - - $connectionMock->expects($this->once())->method('getDatabasePlatform')->willReturn($platformMock); - $connectionMock->expects($this->once())->method('exec')->with('PRAGMA foreign_keys = ON;'); - - $args = new ConnectionEventArgs($connectionMock); - $sut->postConnect($args); - } - - public function testPostConnectWithMysql() - { - $sut = new SqliteSessionInitSubscriber(); - - $platformMock = $this->getMockBuilder(MySqlPlatform::class) - ->onlyMethods(['getName']) - ->disableOriginalConstructor() - ->getMock(); - - $platformMock->expects($this->once())->method('getName')->willReturn('mysql'); - - $connectionMock = $this->createMock(Connection::class); - - $connectionMock->expects($this->once())->method('getDatabasePlatform')->willReturn($platformMock); - $connectionMock->expects($this->never())->method('exec')->with('PRAGMA foreign_keys = ON;'); - - $args = new ConnectionEventArgs($connectionMock); - $sut->postConnect($args); - } -} diff --git a/tests/Doctrine/UTCDateTimeTypeTest.php b/tests/Doctrine/UTCDateTimeTypeTest.php index 9d96d234..a65210d7 100644 --- a/tests/Doctrine/UTCDateTimeTypeTest.php +++ b/tests/Doctrine/UTCDateTimeTypeTest.php @@ -12,9 +12,9 @@ namespace App\Tests\Doctrine; use App\Doctrine\UTCDateTimeType; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MySqlPlatform; -use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; use PHPUnit\Framework\TestCase; /** @@ -24,9 +24,9 @@ class UTCDateTimeTypeTest extends TestCase { public function testGetUtc() { - Type::overrideType(Type::DATETIME, UTCDateTimeType::class); + Type::overrideType(Types::DATETIME_MUTABLE, UTCDateTimeType::class); /** @var UTCDateTimeType $type */ - $type = Type::getType(Type::DATETIME); + $type = Type::getType(Types::DATETIME_MUTABLE); $this->assertInstanceOf(UTCDateTimeType::class, $type); $utc = $type::getUtc(); @@ -39,9 +39,9 @@ class UTCDateTimeTypeTest extends TestCase */ public function testConvertToDatabaseValue(AbstractPlatform $platform) { - Type::overrideType(Type::DATETIME, UTCDateTimeType::class); + Type::overrideType(Types::DATETIME_MUTABLE, UTCDateTimeType::class); /** @var UTCDateTimeType $type */ - $type = Type::getType(Type::DATETIME); + $type = Type::getType(Types::DATETIME_MUTABLE); $result = $type->convertToDatabaseValue(null, $platform); $this->assertNull($result); @@ -67,9 +67,9 @@ class UTCDateTimeTypeTest extends TestCase */ public function testConvertToPHPValue(AbstractPlatform $platform) { - Type::overrideType(Type::DATETIME, UTCDateTimeType::class); + Type::overrideType(Types::DATETIME_MUTABLE, UTCDateTimeType::class); /** @var UTCDateTimeType $type */ - $type = Type::getType(Type::DATETIME); + $type = Type::getType(Types::DATETIME_MUTABLE); $result = $type->convertToPHPValue(null, $platform); $this->assertNull($result); @@ -89,9 +89,9 @@ class UTCDateTimeTypeTest extends TestCase { $this->expectException(ConversionException::class); - Type::overrideType(Type::DATETIME, UTCDateTimeType::class); + Type::overrideType(Types::DATETIME_MUTABLE, UTCDateTimeType::class); /** @var UTCDateTimeType $type */ - $type = Type::getType(Type::DATETIME); + $type = Type::getType(Types::DATETIME_MUTABLE); $type->convertToPHPValue('201xx01-17 13:30:00', $platform); } @@ -101,9 +101,9 @@ class UTCDateTimeTypeTest extends TestCase */ public function testRequiresSQLCommentHint(AbstractPlatform $platform) { - Type::overrideType(Type::DATETIME, UTCDateTimeType::class); + Type::overrideType(Types::DATETIME_MUTABLE, UTCDateTimeType::class); /** @var UTCDateTimeType $type */ - $type = Type::getType(Type::DATETIME); + $type = Type::getType(Types::DATETIME_MUTABLE); self::assertTrue($type->requiresSQLCommentHint($platform)); } @@ -111,7 +111,6 @@ class UTCDateTimeTypeTest extends TestCase { return [ [new MySqlPlatform()], - [new SqlitePlatform()], ]; } } diff --git a/tests/KernelTest.php b/tests/KernelTest.php new file mode 100644 index 00000000..33f0f6b0 --- /dev/null +++ b/tests/KernelTest.php @@ -0,0 +1,26 @@ +assertEquals($sut->getCacheDir(), realpath(__DIR__ . '/../var/cache/test')); + $this->assertEquals($sut->getLogDir(), realpath(__DIR__ . '/../var/log')); + } +} diff --git a/tests/KernelTestTrait.php b/tests/KernelTestTrait.php index 74ad05e2..064d164b 100644 --- a/tests/KernelTestTrait.php +++ b/tests/KernelTestTrait.php @@ -11,9 +11,7 @@ namespace App\Tests; use App\DataFixtures\UserFixtures; use App\Entity\User; -use Doctrine\Bundle\FixturesBundle\Fixture; -use Doctrine\Common\DataFixtures\Executor\ORMExecutor; -use Doctrine\Common\DataFixtures\Loader; +use App\Tests\DataFixtures\TestFixture; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -31,15 +29,9 @@ trait KernelTestTrait return $this::$container->get('doctrine.orm.entity_manager'); } - protected function importFixture(Fixture $fixture) + protected function importFixture(TestFixture $fixture): array { - $em = $this::$container->get('doctrine.orm.entity_manager'); - - $loader = new Loader(); - $loader->addFixture($fixture); - - $executor = new ORMExecutor($em, null); - $executor->execute($loader->getFixtures(), true); + return $fixture->load($this->getEntityManager()); } protected function getUserByName(string $username): ?User diff --git a/tests/Repository/TagRepositoryTest.php b/tests/Repository/TagRepositoryTest.php index 20a1cfd9..f8b67da4 100644 --- a/tests/Repository/TagRepositoryTest.php +++ b/tests/Repository/TagRepositoryTest.php @@ -38,13 +38,6 @@ class TagRepositoryTest extends AbstractRepositoryTest $this->assertIsArray($result); $this->assertNotEmpty($result); $this->assertEquals(7, \count($result)); - $this->assertEquals(1, $result[0]); - $this->assertEquals(3, $result[1]); - $this->assertEquals(4, $result[2]); - $this->assertEquals(5, $result[3]); - $this->assertEquals(6, $result[4]); - $this->assertEquals(7, $result[5]); - $this->assertEquals(11, $result[6]); } public function testFindNoIds() diff --git a/tests/Repository/TimesheetRepositoryTest.php b/tests/Repository/TimesheetRepositoryTest.php index 5e96590f..685d238c 100644 --- a/tests/Repository/TimesheetRepositoryTest.php +++ b/tests/Repository/TimesheetRepositoryTest.php @@ -88,9 +88,9 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest $fixture = new TimesheetFixtures(); $fixture->setUser($user); $fixture->setAmountRunning(1); - $this->importFixture($fixture); + $timesheets = $this->importFixture($fixture); - $timesheet = $repository->find(1); + $timesheet = $timesheets[0]; $this->assertInstanceOf(Timesheet::class, $timesheet); $this->assertNull($timesheet->getEnd()); @@ -123,7 +123,7 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest $this->assertNull($timesheet->getId()); $repository->save($timesheet); - $this->assertEquals(1, $timesheet->getId()); + $this->assertNotNull($timesheet->getId()); } public function testSaveWithTags() @@ -156,11 +156,11 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest $this->assertNull($timesheet->getId()); $repository->save($timesheet); - $this->assertEquals(1, $timesheet->getId()); + $this->assertNotNull($timesheet->getId()); $this->assertEquals(2, $timesheet->getTags()->count()); $this->assertEquals('Travel', $timesheet->getTags()->get(0)->getName()); - $this->assertEquals(1, $timesheet->getTags()->get(0)->getId()); + $this->assertNotNull($timesheet->getTags()->get(0)->getId()); $this->assertEquals('Picture', $timesheet->getTags()->get(1)->getName()); - $this->assertEquals(2, $timesheet->getTags()->get(1)->getId()); + $this->assertNotNull($timesheet->getTags()->get(1)->getId()); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..00f851d5 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,24 @@ +rlI`v$xlH$6$${&It8&*PlBTSrgIzA+AYF#56R1g4@B3(IcZ1 z^vLAUP!BB$(vsucKy-3Qy`*dT0y&B$zEF}8({W%skr?K5RSUUv-L=~K)8ztR67#tQ zDo+c=R6(4TbHs*5`{~-!P*#9840B_NSU2P8p;@<1_U^I7aCB^n9^|IFq2qe!*yzak z#8?yn&;x^2V^KR{V04U2>==>q)7>(BdW;+3#<-CfH?Fdj(u=fIpDYRV;l{>f*ol$E zL?SvgG({(dhq-tHU@Rz7u}`{2q>i<_iDc!nC@A8=QsdkZ2j(1$j>n>Lf_-s?+~qc~ z-Yxnu6ACfR0LR3bKp@Txgky?DRi(8zm~VX%o%vRa(cQC!{9z$gk_OGmlEc=yWsC8) zPQ4XZGdBorYi7sQ{89&KCQ~)!szmK#t-<8@tU&LLj>UFH$GTa+UvDm{d^RiON^}!D zn-hy73_iMlbaaS|j-0uTHIS<5@T$7a`dhjBYI?h1hu+>~taMa;l_xmBu%1BJ8}`S% z16~EZD$CK(*S@*}y1rHm(cP(Xv6RmW1*xk~F|D@FO`D9by-?rPYTi0|_GaCz=3StZ zb;ACDUd|WNpl7a}od%!Rq^tiRE0`{)XN1xp?uqId*I0?Jz0J%-z5HtIkm@-TnW6&Vp;ff zx*!yb&26QeD;0DCYTQY=P!Mvd>Q%4)>g=I>uFezHYIcx6zL-|o{Im!TNhKz`vf2t@ z<&##nS^+B$b^HC8y;;Y^Ms_9R1bPiP?EWun0V z7YDE7=U8u$^QdbxX+BNP7KDR>L`x{z6x))@PO2-lMJv*Ir25LpG!9;=hnCFGeUkF*9>-Vj#QBWK7qtvh`C2oS zmv_w;Sp{K6ESAVdA(@ue{Mt^VwuX{e(mBP(X@sBSVYi~G(8OyN@)=lP%j*J_3EkV< z9gndS^Rq(aa1nN%`zt{>Zm(>H{-8l>sEu;V5nFOtcMBpde~l1fM&fBHWcv( z$IG*@){myMVve|xd|}IUIhTf!z6Cr&?nIfFu9Xf6Sz(Kqoskm3O>Cac&2+KzT^?W8 zk@6vq+kI?HtbceaKxTr0j)4aB)w-tm^A^)0#+vXJm^#*ot zIX-`AcVJ*TcW_{8djEWIuV=HE9~VM94-aQ{?HQQLWfCV&uoHpk%t0}fpAH-z)cLZy zfqEEPArI8XfAlt}UDYSP8mg`?!?lJD-c^-epgQPwN23zeScWg~^iF<0Cls4-jSY;R z8PStD{i?P|w6MNFf zh9<_Df!#-Ti~DlyV1Jg6MT5r&hCCxtU(F2URs3sga(rSGZkU6QO-Znl>V2EV&h(vD zn@@k`)pX~{jA^yO?cG+{&@;E0@)ih=~8W|(A^o~V2M_4`nb=m zpBL?i`jIm{RNB*jRe0CXiG5{e%ERo*jT|nP^F!It(O7u@*pb=E`C|tI@x=6OIy@R3 zVzOK+CN4a3Qc~08q0{U?J=p4Xed?>ieayVtu+HnPY?4OL#*N#z6WkMdxL-ERi$b9Z z>YW0grt=4(H)QW5-7Hrr3%OeNB+cCL-rJ0@#q0NanNVQM^g)&hc&C|kn)M#^GM*ss zVMF{u4-@tWSf3Eye0Ww+ms^FS$3>AlesFT{iNw^{-XZq5Z`ZDJcF(?%-H9E6-TaCD z#|q=SvRn4<8Q_HCm?t!O*sll8sK!3QMTg;reOy{lK+BRPfzM{36$_u}0oUZxUwAd- zf%B(;{6=Yk#i@24&EQa;k4U+OQxY>XD#*^zO zS6a?%XaiTD}R0SUQORRpmSTPTMX1ssqauAqYt(L4 zw{)1?_S)N>F4H<=^=_O=I<4L>v5@^gZJ@qKeTI60x}Tb(uApdY|I)M$Ye51?00|%g zB!C2v01`j~NB{{S0VMFJLEvK3R{j25|F(N;?KMV!&9=t0y>{QOziO6B>h|#ZYt{=* z!Rmfqf2C!ODO$I;*I&NK6xQwY^;b;NrRttvf63y6Zm?3P4AhUPufn_k@1!1v6MP{7 zB!C2v01`j~NB{{S0VIF~kN^@u0!vL`mDOS}uJ5oO%9jh7`K0s%VZOjBi=1fDC5D() zW;xNUOZ56zndC%MZDNa@*rH1eu`7)hgMEF6F3sSCbY0?ia7B5(B3aXj3N3kWd z@Zf^Z^GtsAiu%+a`N9ROszuZvC@soYE?j8}t3|5MgOV?~R+!eSmrDFh@w~OBEvh2b ztSi@f*dJP*5CU`CnGa)_M z3LY9FJH=FC>5jkN^@u0!RP}AOR$R1dsp{KmthMoFibd*x^O? z|F0ORSI)TxVuO$X5dVyUs83NJqdrW%k9rsN9Q8EyR_aaEebimlZSbssd8$ZdsTqo= z4p5WSD3zdM)HW&z&kxu{ZJ^dt7gDPzCuO6Iu9scEbp5yM2d;l}{j=+-wbY zqpp`+?{U4;^>){jt~a|Lblu~+({;*qqwARKsOyMJa3x(+u5s6}Ylkc9+UoMVwz%A` z^{zFpPFIJk&1G@^-uWBn&z(PZ{)hA1&Tl%u;{3ex)6P#g|I+yZ=ZnsFIG=Go?tH}g zfb;dv+np8X_0F<0?-ZRW=QYlK&OOdSC+FPm3_HEfUgt*VI_JgC)lSN3cbeLN#|CKD z8}SAsY>@RZpIFH(-4bAIwQ`MA$3m3(83IS z$AT~RUGt0YUp#X1@frBwh0*Bswg@&ePby@4JwDpwjWD4I%jl9APsA69F#bco9p=jg zs5Q;ciqqvH4gbUOXc>~=pOiQ&mgu>1X0}{{-*oId^$+k%4_@!lE)dk>^_t2ylW|y6 zosnAOVIn@Cmc-JYP{a?dVGVcPJ6Xu_bWSX0=wd#T5mRERoECGmn5O6Wf(T;sg_H=g z`K**yDi>zOV&9FQ{7p~t{=2SQue5ES&En9i57SII;s=QyHOcFtK?Bww@rFlsjFfXJ zdQO}Z3I(1PGSC!0MQ8YYK@dt>^ZIW2)~&B!{n)DM%ZLQil+AJ-sSfC|G|NQ%&@4p) zBzb91Sn4!r(=+Xp)rz2_ie;f7(1**#Ql94Z;O_hMm!HT+9+_Q7z_pZZEk;h&nf242 zK*ZyVc)hwL=pwH#;$ye(`GdI4K6-WcFF$^dKr7sMn{`a99k{U(uP4G(1wxW90-eSB zI!&>1fuDxnDbRdIl>0*fgUII7bUL5O7wB9*2Yr>}3qs#S+Xps3yXL*cwWMLDd1%;6 zsvM~^z{;S16x(BZA&Ch@d;rvMc;J;G=nClU*a3FpjX{{;!;= zR~j9A<)JN~_RU_T5i}bI)%Jkv1A=TQ;$`(o4EQ8SYBT>q1b06(39l>nU#Jj_*{sHE zw06NLgK_JR_`0IyNv;V3%|;dtLI-{?L0wch;I z7brCk+N_2*YSs9pzVU@4Otog8#FSfjo8ePhtzOA{Gk{J%_emo+4E~3C{w(~Q-}CfW9ovoys|Yy|V?Ij6z?>bT z0TcXr6>mV2eBcVe3v~`e6(?HEr}+%1o==N;X@=7p2~8@Nz|jhQ{DY6c>(N6iHEtHe z`&BuT@AQ!F&?kWzNK@4C`m^G8%M%amd-Jz`cc4R&G-zuvd{C{Hm1hGVOr(lMLyN+a zw*mRAR}7raps#;Oc4hllkjhOk5LD-|@Mhbo~2Ol+q7_HdCKgv7eUa8nD=^>xU$n4;%E~aXPIJ zLJ!YA{ndRg1xd_iG5lObBEg3q1{bd(k&=R7NZ>3zlZ$ze(m$@}X6}4=#;GVR*sS}s zdcm=Sr9eH(V32)1m@NyAojLHpS^4$iPp=u;dU@ZzcC7_1#)5_i+!W{vZc0BIq?I8E z_x7|d)`FSNJAwPV_O)phn~Zj?Vp!#YUBKK_^-z!mv#_+HVBfb`e+p0C1M8Ge9CRoZ z^RRySg|1@B;9$HeD@tNK@;b#6@b5Z{fpz3(#~=O3zr2UHD{}V1qD;ds}hc{slA(F7dteDpFn-u}o9aD43} zd*OKNBU|8j>XBABo_u&3j+KYE!ts`e&x7O5Z;}Yy^rjtfyzx!z;CRDBC*XMfLlbbE ze@LQt{2__nu?Me%WBI`_97_+bgk$lI88{Z+$ieaG8!v_9>;n?L`~wob+yfH5>;n?L zBlk=6#QS}4JaoSkjx+ZigyX^cBm%;H7r-%n?@>6W?j3~V^t}=R{vL_Ib@xaFlJ`gi zuDwShaLpSe0ten85tw>|^i}x%ug}48-|MA*-1~Z|A1CjghvUTE<8U0myBCgo?zX~l z_g&Y)ar7>!6~lK)HZb%$iNN6Nq_VqSCzaiK=W#ghxN{7S19xtMBX_3-j`2ILg=6fF zARPPeSOLfA?IIkv-yVbGmA6X-w!Ky=yY;nu;23$WR5tutsch&rsci5zschi34mkR6 zJq$)mvwMZVhIW(DV5+eGtBk3$dhk?x2_!aw&o4;ahXZ#7u^s$?)mS zJUtC%4oP22Rx*QhDS%q3Sv|Z|W+Ofg@qP?&6zpQPRljaYY3#J!LJuTN8M+`4myB6#E+pb$<>)f`@_`2~D zKb;YdNL|Gy70F5It794XzIt(?bSgH+MJG6VBHBO1(F^3NtR1&??g$$zgJLc%9G@IX z>`6w0F;CRzjfa?+C*TPO{2c2Ia-M)v1)nO3@Xb+CC??@M%zTd`Mk+eV(TR~bx1V0L z9(r^{Ri~~lx?4$@Pm1Xt*+AVVFCVjYhQr2F&|Osubp@NNwy7C`x{`D^d`U4eGQsWO z#^~K+iQ(wj6g|jI(b36?(ZmSUFwBii&?BP|CWnT4z#8P{OB!nBs#m2kMemJ{#db!= zy8RxvR*;U34h>1&(VrLr5k2s802%lSu9}tg``vV$8;DL05kZal#i9u9r~5}ohq&m7 zmU*LRqb9^%**swDT)*CUa=ji_iJwueMEaMm!E%|xPP%fLquXKEwJVn|frV!>4!eD} z0OnVir;iBpG+!>|#as%$)(&4K?s3>_8qIj1)mp(G7`4*3>D6`yYg_5Qsn6CK^cyQ{ zs^-VU{P6%-U!B>>qkjgzv|keQxr-Jwy86;OW2>ow?v}K6S7O_3oqoUZq+1WHCa1!u zChfu+gw1lCwOutrtXkD+Y{ebxvUcucj0PEE@hJQVgpeei1YYH35$F|@ zOk>*W%Fgw+PKGg_+$y7J%+Z)~UJZuE>vXpwQR{yOW{PUNgyTZ0ToTer_#KHj433G! zFsB)|G~DRk-tKseotU2$B581TB{3@`#cURSsRPD>+tH&RvZ^q*yJEc*%xaslvO#B7 z0X7gD;KFrg)!W|fIJ4Y* zI$aQo;9-VF`)fP0-u03L(Sq~O$c)io_DW0s$noE^-%h3uI$uVyp{blOXfAB=r;Qks-^k8!cYcmn1_)%APi>(bn! z0+%Kc^(vWxz!Q~V7HWjtL#t}0VPY0DO%&~+(?SY7c_x`wTvH`NLAE?$lP}F6u7I{B8l}sKlPMm4Pe+82?Y`l>1sfh_2V$%@8q$k4omRA3s!_BG zTPOXtxmC8#4I7MiZPCy7vNh7$(FA{VnQ`;QN3R6tc7yQ7PaWb)7MetGbOEx zhQ+t!_KKxsCO-pv!lnxoXNRI9Ox zhOoXnRJ~qjaJB0qG3|DrJY=?YZry6Ub*A3%le5~^Rox{>eW6dbEHoVSnQE3Hs!5Q3 z*YA~Txzx;)`u);rH%JXlgc}p1BjXceQCLyX1B1y>+!GoIc$sK0z{S1N&5hA9F0o@o z-raS}O$0FX7&pL;aU(HqT-}sPm*hp2TXHFJZis_zTP!*ri^e&5@d{1g#>V7rbz&qj zk%$frP0@+rVJ@D4MXX!e|F1F@4d%nHe{=<$?{B}i?dNTCjzRnLcDJ$E`rcN~_IX>( z`uEl=Ef1T2Y^D~w2Y}bkh`?&~wh2kOj9X!RgYv3no|hq z_F4@yg|_@t*Wb0{GA$HS1#wndGBmC;8!8iyOTXXPbQ*zGc3nGEK^qs^;JG(0)I^vt z0Z9GYSq0NW9~6(nX0mxMRV-OUAA%c-nfWuyD#FjD!OWF8qs-HAQ&O5ywFy-fyOG%_ z?QBZIOdjr{s0#%~=eIY)YX)J!Pyy(rZ@+?DmSw$903V`Yy<%Qq14!~*?{-^0cS{Gp(a@dp?m{7h*)?2QL} z;d;jrY~Xgf8gv|W4yHQ3duVlpkg3vLaprBE9BAo3t(NuL1 z3#*`%Xf9eeVYstLh*V3X)CFwQwXjsBM02T9gWbi$3-vM+2*jCzaIC(O&IQG(C7O$? z&U@~guK}S!z4was2RwmTe>B=3kNX1sT)j~F9fz(4$@RsWNtTP!-P#}o_u;NwdNsWJ z0UGGu{iv_2?tWply!7VBIz^SnH$N7t<_uL+-~8x40K4U2-J2h^Iy7apR#B{G16y0} z>>aPVoH_%~t6$1DEEi65JG(LenY{IJ{Z(L0@J_;o+JNzU{N4aJz^eABfKm*QynA(d zgDusU(sfu(NxEBS$(4(yz>q*XCrg*#{;IF2&X8&<2`1W(tKH`kXJ5lnI$QOdWqh}xto5z@Uz^gz? zNxyuZg)O4&Q8qRhP_(X?uBU2?(%tI0?2FdyCB7(8HKF>d>P$#2PBeMVf;O~JHD{)o zYDF6-i7U$Lt*BOori3;$v!mK_XKhGo@nz|W)X)E)(ase!ApsJ8NO)FF7LUq5w;>sPKXxSn_2>blky za;?5|k02?z7!)&<2VR9R!bm<~|I1_rP!>%TQe2)h{qv`8tB}pO1<7L1_cs$+?o08-S zgPDM`es70WO9H*Zo}f3_VW}ob!;bX_I?QTP2y%tsA-BN}lbQr(;_-o(33s$;NuZY` z(bHknlE8QaplU#R|9_+N5(D*3>c6PJrQSz9McqT)K#9~|Dn@yzHI&2kYu9&OUv&MI z>m9C#UAMUkuB2wkV>OBF8yU}Qv?T2Y&;~6@yW1mes!3hit)2A^ zNukTCN$O3Tby|{khlXxQ@~+h-DfeD3)snOuEo-Vt>RpvfNRs{*%EelecE98zU6OKh z3C22P_R#%hMyBFtGlhj)ks~VD+m0FT^6QZM<1U)3* zaaf@xX}1@sYLa>{!KEc>Hx8WDB+}cSP`j3--4bZ4CaL>>hnA%6|LsbWZvWq^CRO+U zwrUbcl=uHuU6Qi@w`fV){@<)6Y5RXuHA&t7x4=ysJKUuRd4l@=e=GHM1N92^YwBn4 zDS+=&|3>`_B;X4PAOR$R1dsp{Kmter2_OL^fCP{L5?KBO%qD}eRXW(DgH<|Mq=Q*H zn509C$z-rg@Bg<_w-~6OQs1GzKz*EgH+3&{3-!eEZy;)j1dsp{Kmter2_OL^fCP{L z5SBix9hK(rINZmy#AW?LQ}B1pVwb$S!0UYYxnm0%NLo#x_!R> zib=Xu-Sg`&S)||pGpu;^oUjHYfCP{L5nf+E&|1dsp{Kmter z2_OL^fCP{L50@<=sE19TGqSNB{{S0VIF~kN^@u0!RP} zAc3VIfS>?GPS;@Bs*SKzJ*J6A&^G9)j?02v0+J0m4Th{5ga-LijL*mmqu)!uuh-55jvP zya&RI5dH$fyCA$1!aE>558*ioe+J=M2+u%x8-%AIJPF|m2#-T}48ofsJPP4W5FUi^ z0EGJ?+y~)a2=_pE1BAOF+y&uv5blIPb-LIK|FaOBgadWw5{IWDNK7O)5+kW^BwqD(?P`)H)jt4%gAj)hgV3i7+u;8zAZ&#Y zfp95=H4rX=a502c5L^(XkBwL%Nd3$}Xon!xy%xgd5Zn;D>;CV8^K}rUJW^2Gtgrtf zNWTz*#Ns>%svJpHORtB8KbmDCes9F%VLePpPV&;8aKsyoFs$L3_Q?`mEEiMatXQIF z^To1I5a`3@VkuAaC3>!$nJt(2lF)bGr@#C}HuA{qI=jKx;`O$aZ7oKQ)X4O*e%cd= zczhABSC_)!!B1}~vB>5tqP=xh$nquVwKMhg} zG@lXWdWEuB%;wW{I-kiG=v+PrGIM-E=$mN!z~*PyytlZPG|V&)4SPwIyLN>hEtzB!ydbPX7b@-}{H!<)CISD$@hEg#)jq4{c$fLZ@cMe{V!Oc(=4d)#YjJB0 zVQD4=#$9LfV4YwbVZ;4j87XHnJQ#bS#M7DlG{hhzBhHi|bxz38!i-QX@x{K;u~#12 z@@e1fMH)e~aZqg!8=!$88;W>YeG(I4{E*aU{(}haer6J0SMI-1AsDk+jn`=Hf>8$J z)*tbObxACYG8lVd!=rz_TgYUDTuCS%EerIVSmNO*nHwz}@4Mw2y(hoco4@)3rRG7K z)$m5G8lTiRzHo%8*6gEMuhbU5&v5@Y#|3Su@=DJDBrtR`y83Q@`GDmwuKLzYr$Q=a zv$m|%*Y1_NnF;EWKyN>g@}+sMkcVy;Vel*9p)aO|LY@}NRR=_*_LcwXis5VXQ#0p- zAaDw%l+B#dRqBC}3WHPW5g1F*Fc_)Owt(TW(%A@l{ePYRvCR1&JhWPA5!iBzsaBE~ z_=NOH(8y54!>oM(MyQYx#DhXEO=pQ&11w_EZv4`X8{Z+EM@$*~ zoZ(YitzOA{Gk{JRG;))qksAj8!#sZ$e$MZC`m2s@M}<{{9EdR=C1SvTha^n!=T*D` zN%Da!058-z5LKLLF`wo$pn5(n<|R+9H4>UsEP6}MZScwpa~zxBHV9g3tuTZ`d?YPGC98~7L)@VX@MHXxt%ih4!m^sZXoePfK$RSZvkxLz2vg4f^jmoz@4Thi9Mu>OPl( zBxbW1ey+A(f)70mE?z?-B?ZBdz*%}G7xNyae_YSa-1+c~Q&C#5S@&u6f@23ufqIm| zAp3eSTNWHUbKrrq^6SN)UNf}y^1glTS_@i?1q~03X3!PflzucwT>-+qJ*|tiV5aj< z;Qp?CZCb@9qg|_*m3=wPO;ryCNiYjbD+=~~i}k1Q)IG3H`NTnoQZWzfhhOL_mJANY zt52UKuTwk$|E{wbSVw+#{Lzp6%X@gcB4-~=uJ`LI1*ZUTSiKyWEj*Ifz2Ui$axMik z!kkbj@bU_T=2LWrhs6u{C3X4Ncl+HfU+X>jo#|E(AvxD6Tgw_v0!%(Ibc0~_x+Hlt zffqE~J9UPul9NCB*$?@z?>J;rnjyfb{k>MB?CxMX^Xlgzk5^j1GT#0DXXPJ`bv?W0 z!S*j5uqu)UZ5Cs7PJ-sZL<&Y)Gt!`iFrhP7zU6cy?ZofCzWZXAn5zR)Zm$} zYi{}H&%f=rqwktADMDZpH9oErBF*|RmsMv`zqA$rSPYZB?`$T~Ti@OF+tFv%rCPMs zSd1e&L10e6+h5(8$o`Lo;o;r-#WQWq{HGuN$hVBU1f$XwFaRk{1}xH~T?q^geUj`% z{6WXh&SLxx{PH(F$@_olw7+1u!1x(MOUU>cYKl6~^_Z*MnRU|bZ*5=OcBt(V$6Fk0 z?Q2^j|k2f0fh60L^l(h593WZ`)nzmpu zSsYcQkm{Qim1Phe8;jH3se%9-$aHdgJ}IW%6>DV;(Pcw}E@5xjAM*})7u02BL0#0M zr_)7go}@|U80fLVSZUMr@b+`@cq}*&8VJTYe>|w@Q4;uUG14HqKDSP^T!QYFqKv?& zC26@iTW1iYIb~^5mnGE-{2c2Iavrq{Io(ygwKTb&6^ccEMxbLuqy73ym11zuA`W9>A~DQqSLxo~?s$xy zn4c9QX)wi-2z(qp^sJc6!M>({baaS|j?i&#AUZiTL2vYI)QY-qu5^Nx1dWxonw12- zEXT#7;b?z6?hEvDs+B;k#X~TkCQJNG@zMr!sV}DQx0<40H`+NE&U{50yP(0U1`Yb7 zaW>#(n5G)6Sx^JD*kT&+Gm_Ds8mgQJW*aoVHmEnEs(w;an@Yd9neo(?t8HhKu7=8+ z+lbD}YFPEqq=>tRj*X6tPmD#uF6n{6WCQzw(J?NuV??&onkwA%7&pL;aU(HqT(x;k zZOPQ#616xt#DU|DMaN^&I59M^9&T(*9x92E#6%)GG&DsghKIR$0_<@ig|x1yRfy1U zrV8->!?kc%43$-2Ofb=0tQnJJLQ!vjC?0@K2OEtk-PthP3^mwOeMy}?HI$~i<%tRS zRqU0O|2HSQ1tv>P0T}n%%<8TbD;>7ZZQG0|=k*5Op#DNob~Q8ehSF#4A{ACm#za+j zdT(?rwlg}`?f1Aff7JtbWHR|oypV$yPx0m1YfnGZRYF8dev@q?EP3 zv*~Vm+JWAsY@HimqMK|0s;_vV?k`omZ@z$%g0T%Z9{BfU` zVZ(t~JQP*ui`hc{Fx(z4Cgu5lN5klIbBzJvdrG*Huj&8LUCJ=}-17US^ z*VZtwpm4R=BEprbB>9Po@eDxL%GN;ECUX^U#UM{TCoOtIs?DWzO>JVlnQ7H#p}Upy z=JSixBh9bqXP#;mFz=8OZubp0Ll1U!3bU}SNT^H1q ztr|eZa1#+xE4^(&scM;pq)L@YeIA>) zcJ5H>@_B?FKv9|NqKB{py@*AT|aGAOR$R1dsp{Kmter z2_OL^fCP{L5-`a7fBS?`ERp^HuMO0%5epJP0!RP}AOR$R1dsp{Kmter2_OL^aE=jZ zHQQI~_W!RKs8`Oh7GiUd01`j~NB{{S0VIF~kN^@u0!RP}AOV?x$z;dR|D!D+0VIF~ zkN^@u0!RP}AOR$R1dsp{IA;mK=l}8j|8ur?v0+F62_OL^fCP{L55jkN^@u0!RP}AOR$R z1dsp{KmthM93kK`nhnOawh5tFqDumwmEQk1Qa?AqAHI+P5F^;EVCUazHcI<{>GS_B z(&ztwi{Ah^XF3=gf&`EN5=~LTm~W zKmter2_OL^fCP{L5rP1riD_gWp6Y18KnABD`z>*XjL{>;`nxy4x7Bj(b=@kD%oG8AH% z0gj0Uqx~K(x}h4ndrmBh(;1kI~sr{+sMWKjw$`HIN` zUlNK<6Za{~O+@>LI8|tEIl5a+gYpyH4sMLzJ(d`bj!n^n+!P(1oES}vKn26x$OJty z3Sn|+sD~~Wg#zfNm5Fl$(a9n8YE}6KGDynO@zF_1q1|I#EHRz{9@Xj(ipPaC-9I`y z#6|V#Vy+|8kYFI(KX}R>ox)H93;llk`P{0}Rv8MS}q@?hX4x zOw1GTgd2Sm@eE0&o20Uns-s%8qNW~-R7Q6zK6pyxWxm8xo< zM!jnl^{NiEzTl$j=(V`1vWX~hVS^GXs|F(`&AMhmCDek8DxolOpY&BOw{>paYCLsw z^D#uEEuh5Ji;pL@19ZOk)%$B5z6Jv){{D~Qdn#)yZZPwpu`;YPbH?xW$Nhe_2h^#! z=}dQEVIyCl8eNCh&xo3dS3Q#Z#!5HX7HD{J)wYzH7gg-4M$4)zwumA+`6ntHiTu?K z@>M`h!AvbEUoEzXeATX9KQx@Mb#ky$a%_<-i>WLZR7qZ=?N#(?^u!C*(^NmXE}bK;(b7=q0$!lyT8)=l`Qpk=Ho&Sc zx0o_|9d4+sCn|I{sGxFc=rXxY6AP-KmRn2()mXt~)`9$O#>z%bJ{yj*-hemM@9{=M z-jF&~Y6D-jn6U;8>PzZ0XebQ^QxIT%n@&!XSv9kAaLb2;6XquG+p4I9_jK~I7k zsyaZ`m(x=5Tvaz^oRTWpoW9zK6RxbU2t%+*i((nHnRxGbgJ!*pl zkN^@u0!RP}AOR$R1dsp{Kmter2`nuE>GS`%|6f|BSQ`>R0!RP}AOR$R1dsp{Kmter z2_S*xMgaH!%dJOHA0&VTkN^@u0!RP}AOR$R1dsp{Kmtoo0Qdh(&lGD!0!RP}AOR$R z1dsp{Kmter2_OL^u-pi!-~XpxUT&J9K1cuwAOR$R1dsp{Kmter2_OL^fCP}h5)d#M z;rIX5_y2MKzXU|F3M7C8kN^@u0!RP}AOR$R1dsp{KmyB;fZ1%i7Sbs5-0$2_OL^ zfCP{L5Kmter2_OL^fCP{L z53<===e;IZPs)PiP01`j~NB{{S0VIF~ zkN^@u0!Uzq3E=ntml!BkhXjxS5{7##$OtbQa8J1sV&YQ8eTRSj9U$#w{>pXWIQ=s;-@ph5i!e) zY_cd6i()=ktUDEAV_bBCqbH*MLmXXqk?t1L^xo)LY-e<=n_)w4dSrBh9+?~(>Y>wo ziKqLAM*DSXB{3_|iIE9z2RBAfj*KUEjBs(?^^AB>I9;CIV~OGD*c3g;O@VZ`+p^g( z*STkt(IDp1!tvtKj93zqe7TgD&y#f;Cz-m0gHS%u>EC3mbjSq~@%_nQAm$Bm-eA}p z_Q$*fUZsebo3mCptK9w0kDFOrXAp{Ql8a4_B=%_KSTF1C_j=>ia(OW&6jKUHDeoi) z9OB%5pwOfsJvySI2BC6cKu!Kj9uzY|j?W72T&3S8Yi@7QJQ@seanQa_gK0$rB3j>6 zwLnwJ<)TnX0_#F0N(7z>Dp_lOL4hjUrUFytVkw^`LT??d^vULW$E4m|RW5ZcysnwK zs(EE|Rn1RlM|5;@Vl*)VMTfZ&uwTuFb$V!KTfM4aX>vvFs$4lcEflm7;`dYs2pt<8 z8j=Q9e_{m8xra^_1imDslYEJeL;6HwnA0?&dwaX%F?M2pR*1m3lE;YK(L+mPkH`YSS>_%>+Hu}Syu_#Iw!$njB3NRzLH?H-@}FcVL!)ugPbQ&rv-ajE$S;c zomM^coLCfL66m32mr_|<2@oX*Ppf4AfHxcn`aN|@wwFZr;T0Fre^w5^DePJ$QRP!vXn6&lV-j+HxQj1A~Ry+fX`>MLas!gKBv-0bpPn+ z5Es>t*i=4~ulkpudvS@=<@AiuWIU;AZuD%_djoJTNu`MDG}W9QT|UWh&S>2A(1eiW z!Kyw^tx{SwEad0FnG|$x@l1+bCgjqWot>1X-n2$mtZ z=TirwVvedKC^8MBqrQZ0G-##hZp8%Lw@+5Qw$9#O;~TEgTfdgGUS4Z6``2=x^#Eux zp)CRkCbcE8@$4+0tIj&C-+vlAPvvtZK2_3;^7JM6bh-fR-!mACseCzCD(JLXWDY#5 z33K+4ej-1;$_HVxR+rwV&z;Rr!?N)#3J9<%$ecy~L$F?(Rr<*cu z165T|SH(k^`A@5-;ux!q-h9UTow;7_a%Bt52mo$XgF0@ zB?gMsAps3<==-|I4sbP$eXQ1dsp{ zKmter2_OL^fCP{L5xOoB z+k4w$j*mL7wBKys()t`+z!wrg0!RP}AOR%szm~vFm7TUu|5jt=LNS*Xjwj;#li^Sx z91g{!Y&Z~$hoamOG0Tf=l24VyIk6-P#bim~v&F-)F)lj6(TR~bx1VlSkRBbO7br}3 zOLZj0w7b$$*+Dc|*Pua&iFpE^aA1K-#051_3ofdG(x7=si{Z+EwUgg!G{`y>k7mS@ zkmSpyynLQqK!GH)z=cDVtrgDJxpk}Yq_f0NXM_dvEs#8;G;AW;Kg2DJP}czHg$Zs4 zH%5<)PS7KhLqk2Z0=UVQ-D8R2=-3oJ$W3+Y>d=a~-8WU@w$9)tV`Zb<{>hQVo@6i( z^M*KYkc~$JA=VRAbQg1Tc`+pv(~8bg-bvLenikY`idvZNR{l%o%Gqh5;I0@dF`~hy zCK`BIZ@pEL>#}4FkBlMTrLU)$p{OTC=uAvATSKVV%`C7oj`s;fog%K0#oH;DW4@mZyl}l**Z6EGTt$% zw^X%f>I7b=2&}P8H81pHS~EADktlRH#Yn9Wn(V-#9J@qF>z}9={R6*cNLORKp=s2WLB!;zZunyG^Yo8}Lz_6ZxI!cu$%@*>9g;c5N zZ5Y<|1$4t&Ekt+ghLaoK|2OWke8@=cr*>02sh6l1spqICsYj^$s5_|&HBbG7`aboq z)Yq(6**oo&-PZd1)?c^&x7E@5AFbbN{c7vyTR+wM(bf;NzN_`^t&g|9sr8=L+pW{B zH@6;Z&9~09CR_Klj<)V--QF5(-O{?Tb*<$~)E8SjTd7u??e~`7+WyD(Q`--0-?n|- z_C?#LZ6C9}WP8!_L)&w*4lS+JDjhqxSE%|5N)vwEtcEU$=jx{k`okw7;$WE$we?zq|d`_8Zzu?V0v;`_=6e z?L+PH_AAwp-eM*Y?Y{AGdwa@{P8CZu?5x=h{Bm_Lpt%YkOzg zGi`5ed(awbdwtt&Z8x@+ssFKjrY+khv|ZCS**4t9wQXzjxAnGdXj{{^y3N^Uas1Bl zODpU6vEzG=e|CJu@j1ueIR47~FU} zZhw>g9{cTZm+6>2Z@tVuV^7-m*+=a=?Az@@YSzBRzR|wc61FfFw`HB>LQ99mZZVpF zWB#f62j*{^zi$4b`P1f)nO`!$XnxN8r1=r^edasO74y9Ls97}g<|*?YbHW@ohs})H zZC+=-(A;5mm|IN0HT}%=L(_Lm-!Og2^cmB~O&>D7+w{EYDbu5-`%SMioiv>=6-(c`<2U8_2KzA*w|0`)av_P$q`vE^gYxO`AyE;o^AaOgX z_n7^!$a(8t5?hpfrvD)4#v7FQaygz`F{s2FN!WC zyOMO|&W>Nm=T~$P;8%8hQ$8Q-_=+4ST=OKhawKjc;C51T^7+oTUy>MR25EflG}Cgt z!}WVP9_Sz)GvIh1Id5%O;uaEH9wD)bB5}*xmH0*y8=Z2@tsn-(QRk6!$7&MWOe8kH zOk&gDk+@}}JWjZE@@c$-^kdxhJ96$iti)atx08O4JIJt)w~}EOZzcU0H-C!cGkso( z3C6gQbZ5+wlG9_YSrS`F_w{#>`ubf7IlbRbx~1RxZE|iV`TNaoa&CM@iCJ0wevU-M zIHDayTGa7?oGWS}Fr$W3@_C>2?@4U_35gAVE63a0|AxeE%DIC$r|q_XCF!=CmH2!T zTmKJ<&4)VsN<2$q=j}>-6^YxXNNgwiUD--H`O4O3 zNxDrrx2z)PE&JtoTSt+^u09ewf2G7fCvhtorQ1wC`8-WckhqQTN!!REOF0k7=_xzu z#gzRHa&EhZ#OALm@s%VtJtfD}6zR2Tis&`%dXl8K{wIlH*&`YKv^7U!i&D-)9ZQ%T60fIex{z%jvw!Oyc&(N$lWBY$pB0n=et$N&oSt|B=tHTk%sR z{w#?dgv)gf(tp=kZkEmHI-*9B`VTo<(m}c>X(eMPX(59oY1&M#8;SZ!1L?MF%{_AZ zwWg1exa9+Ke2tx8xyI%s=hlanxJ2R>Qtvf}hh;Ucxlm5N+Cju#?MRbzJIQ`^D`~~m zEhpsk1FqkYxSh1}K-*4oZY5L>nC>R$M$)VUM&e4Ya;}uquWIv?*iO{Gs`XZKZX;v( zD(n5^+)P~LRi;PCdCPNhJk>Ew;x;lir|e{GPg%=y`u-K97xu3p*0rCynxs<}61&!s z*!c?*+g?XvOOnLq5fU4|E64j>gvUOoa^7Yp>Gm5)Y$DdP&vb!|bKjJlym!U7No-|E zZ23D9n{Ohq=`lH;r2dPS7YN2S{xH ziV~A&dsFT#ydFqe0&8NWaBF)m2)qNT|p8%N%xIANi)aWh`!@i(rx30&&c`4 zC?APkaS}V56SWFcH(<>Tb?B6CgL-88{QzFkFFrrI7+pVbLR|+9hZ^V8YQurjPa2bUy{#9 z9Hd)DtSmXVt|PIfgT$uyDlxIH5#wiMbw^s{O0D$+=@xi7zFw{Szd%5tlG* zB}O!CA-y9k#ryU4kNbk43;(wc@8kz z%RZao^{j{S_j*FT9+qa&ilx5j!xxWK-uu$t2S9P75k$mnEvuovK(EKw%LZtVC*o5?Xh~4NKf;E4 zr*;b&xC>koibu-=h$``LlqCtr`)2-j;B}v0#TYq=UK4)!ukFT>KHrH_gD zf?AT7_An7Y6Y&Hu-8}?aW@&M@SkBVvd?sI@i<16X&^!e-779g+7ZRK#B$Gf$LPCPO6qk%t zs8V;=*H_n9*H`zxx>0%c_kVlubBDnCS2C0T&the-yMc4=IcK|^TbH?@bkU;f#zCu} z+%)*FdoL_*St%QZq3!dm7tAim%nA+231Pcip)hiGAtuwd4{dAYm0!7$*-0fMdwa&Z0NW6Z#=kc4i3AMc58)^WwIK-txq2lP6DZY-m}oUK*$C=aEBs zOg05HawxkMDyxtVJd~wfx~+r9yV1Ah$bP5xp3<^RvBMCj{xoJN056=B70S$_qa6@J z+T{SMh&C^jJ#l|qr#tx8uQqnOXJWWzsSFG`tNn#h@-Zsy6UYYp(u!=dGPCj+m8)HF z(?KH>Z-j zR@t&xjb7?B^%WQj0wd?7tQ^`Q97`7MP%9L;IWOBiY_N=y*;qHXw5Fmlvu-OA zzqEs=Up4W*^P10IQ{2*|7B|FcYCp&fxg-t272xC?VH8NO5V$f2;si2sN>f|Jyl&qo z-QPZUW_imZHS%<98_6o{QNc;sEH<(a$XP)Z49KPV`tKXtR8v!$Szp(HIC)K78DMo| zW=(at30O!&Iw7PX8V4;p@`C%{*GElmX#|mpq%Ff7xx|Y&c(6wZTSUAlDC{E1M{L;w z-$5jHh@c%lr257Di?XM;G$^hZPU8lWIpmz~mMxD;IJRdsb7J>>->p z>7fxXP5rv8rA`s!Wk`q=&=c5@G-xXnW`!!x&eJZwCIvoRJM6OEt82q0Eeq64&>h;3GAv8@EJ%0~ zUae51Jg|>B+S(i2Vv6P0IbOMRK~q&rjXIACXuRGSM>fBZ^io~{ltpV)9?h1OeX4&< zX%mD6l1KH`Wsp)Ls}?o^Wmz7X%}q7+O~}tx44T*H8HkgkXSB>$t019xhP*P&1?(53 zdM;#CD^ym&0I1xYJZH_DB{noyCWO0`cZS^7gwjrE@R4I#_A_x`y)VOj7lP z!wbRs9n^f!&yVbyaeQ%0xtbC(K)(y)g8;!jtn|S^OdAOlqz{axP^P^t5ios1I)45v zZz+?J?Nh-xHjE7V%7)!b=R=}V=>wo(KfP7z$GdVdl$-^lx8BB+=moC3I|6$%rRX1}&+r!2YPlj$up)pAB-Dzd>rSzuXaBob{z5*CV<2S&>6yuLkITDBdYP}6hamKkbv z7#p?|FceNA`=o3ZoUk@{bPGk|2n3g#^I0nEm|r;h@K0|2N&Nq<>emT*iuS6VMiH7#saWKVSfj0>E?ym!Ec^f z*BI%mC}yeCw2j52Ai*p$;BjVTJ1cp}C~_8o4QUrn?BJ}s-}HCBxKH|&NFU597O;H? zPYT%{V&jX75wZoGYuzC;J_a2P6pl zZ&)Cs$O-~c=2ZW;t${cFd(N>{H~ecxL?VX4k_D_bJ}Cmx}TRmU>i2KGSV~Q6X;^g;v5k( z0+~QSPd1X93%KjsAa6ntY|u*6vQ|gH0pwZ^_TzKQx)l< z_NM}Bd#3C){5#7F4=Dx2D00+b`DB+LpOP)IarOr!LyIGsiK8&}7cmuV0qhtkkyl{F z02qZ$%tMSPr~A`w9mV|ce|~j$-BlUsu0|%?gY+9qFp4g~O2Zoy4Ou8KJei*bOID>| zdu;iQEcRonBHa?ZWz!#!-C~P|#0UuLfZGAnmmT%otn@ol+O3Iu;C9SBsVs7gOl5~w z)4O6SiG4w~_1DROb_6L$vYg=R2;b%u>YY+|yE6QspS-oYF{F zH4(fLo3i2|TiJV~Xq=b?WG4aznE)#xs+{!w$nui^{g)eSS(8^)U8zP<6R7;(P zO_OmEWr8)0C=;2wtxyoIIH*uz&rk=oaP_N~jQr{1o5~}HsY#J+(H_JMB77j}us$3U zi3tk@8YI%lE4wDy7)XbmbWy{1DG27J&KrKnTY7GB z#G5!1hmCw{;fq|#$&mS|!bKo8F@hm93kpte>rATBVA5an)%1u5Ba?MK#%T_g6%=t8 z0P#BfA-2XT2@3WetiDi}#qFA^<+EUj4P00majVhcnhoXGMv4SN5fq8wJKKD)HA0-q zAVmhnHNS5vHs6Y_Y|EENTxx@+BN$k2y)-2~!D1A7PoP+qz(>!`8aydk(puO9cKr_{ z!x5*G{=HvX%-*jKp5qY$wYcbk_X3U=K=NV6Pa3L!xsk)HeQ}!d|W2{pR?mPQNi6 zv8&ZkAVY*M#Wb-R$ZnE*GLxX_;Gum-?L3G%+A+k|WWfDjRVDs^Ir;xtftLdd0uKKk ze_!8&zRBMG-YwpfQT-iu|KyIjL$23dD^Q($hjW_aXU8^2rg@inmhp+P+USic>PfZ_ zZOyiH{YL!^?K!RdApbuc8J}2;p)bUn%3_wk!ttMm z$T&6AQfNW)Fszv<#spR;sHEy7?Z-A zSMmlHCQwlDkk>^$`==DPEB*T8orNF2QyLkgrh+l6KZmJU(h!j&=m^RXnMv|UCJOff;hqk&w`aXy-4sB_d~kp2Jsu_`iJ zCPALrrk^dRf|xfXJ*yN>0^*<;MXn&YHD5db{#0mULth)}FFbXTQJ9LfL6g%odCMrW zfRG4YoDw*#QJ4vWsIcei4n;Z_Jb=vFM=diVBh}(Yg9^`;@j(-aLNXO{wF<-pqi}GP z9?8>orMDM!ShW!a2a}Il7#X3gq@_;%a~_={3ZU9*Oil-pb2}?q7IN>kRRXcktFpZ!f z5h$mhe_<+%TD(OX;1UDEAB{=>L%<#2nS{)J>A~IWR8Pg%1xH|8*6KA(< zn-M9H71^n>p#gJ(K&_MqVl7-8ASmo4mV=moffnuBo=#jp@5QH1zuQ$A$yXdv?$ke& z{X#SmYJ}9eSVywwuzVt+DXhU96t_6`89&V z-a(yY&1sWl*ByTgqKFeJBe`m1IAR)!3)prX#lsQ6D#=y0odouR}Y5JqfhXfL zsSu58Yof^!-||}4T{9vlDy@Pt!1FN&h#94yRw&sj$z&8If3T}r+C@jUD?Go1_9*a5aZ>!;urzB;+5F{=y;(IcN+` zMGjath3?b#9pwN2KR$#b zRxK|jrT)gkK{Uw6BkBT*L3CTi!2|_rMesBK)B&jo_v+gaso%D8N(9Bc++~2WYKqij zBC!zQp@HU=ED;o39Gq-^zIM>2A7V5VcsL1F5>XYqRN1ayh>=;WG+Ee|rL*R7PNY>uRLL%T(p4A-LWl(*={<}?yJMjsr=a3Br#F=9U^R%tsw<|5y70l?1sQHG%^j5GLpq}huJ12p5vuH^-jAv9*eKb-yGNP|CqhLZtx+%@!9rO*x+E!} zX1t7gjd8^#5v#HnHR!f+vNG%wVtOzjAyU=KfMQ=BDk`T)+jQqa)Nly1>~OQUJ|#k> zyrhf3EaKWys01U!I)#C)4qs3d?|^+0yrwNy8ATcAZuithP{_;d-Q?8iTv;(tavKo~ z3O6KBoL7O(m6N^T@(x+O!^Qg@S}3b(=H# zjg;uP@ndu^eLf`|v1)t4oVGp~R}?n34W2TlB8?#^J`gxPdHKV;ry?aAuHw8zaYWVm z;(Q#oei)XHf&vY&c_?EbaeP7H+(BQVyjh{P4rtN(z`r&P{&HbN75D0BrqF{JSxjKq zAsB-K8;77s@#S`mTTcH9?M&A{_s*OMs(h`^vWGA};xot*K)E2AhCeiA0>#XdRnX0s zoI!FtC5-B?sEVMvm!^O?)Az{|I4}c_$EtV=ER<2$FA_Y3+U~_EQNh@k?)dzuv6Jc| zDDPDyZu9X}6lv!q2X~PxSOs^GLH}eIOd8Yf@CzU30B&gnb-sK^ zQ=KYFK`tZ**h$6_r-I}dg+oJ8M<}njXOi;yjQB;^qRPsMRp48SB}8RJnAMPMbS4sf zV!9?2_!g{`>&ZyLq#sNn&$wP3vC4W$5%_r`h-wugEV+wds}*7;ptG! zXLb!oP|VBQ1mP)d7uXZ{spOtY(=g4>);4mWJ6&xpV(Y7_^1v$Og)*~IpAE}kY&uSs$wInzWqP;19G2 zUKApbxS%x(-e8gTad@C*&bw^guyLlD+J`B-u$u z%%QB@_~J;hS_%bvDFy*kp2?C#E_8<2kc5Kj1*M{J?$BcGLrkH@y_@kfwmdQ=u}L=l zHS7JUqk)Ahr_9Bq*t#vme3la+odb#gAwI#9emUN<*ENW|2T zuUJuzw(Ic}Vkl#;{|A1)s*IeiI2uv9Mmb+NwTPFnHOyt1IC4heG|0WnozN*26J_vd zHPIxsZ`618t;~er9||V1BqjYYlb}FZ;FyBG!&6vM={88&f{e<@M68Hx)e2|2b?}G< zV~s=?{mcE&epK!NcZ}|T+jGv5cM#%n(z1r@vU1B>!|3crS_t5V8e{}iV{qI%L0~Nk@ z&VOO?;5yjyZ0&NUO;BvhgV*!&rv06ga%ilEehV38QB~nf?v(Z)Y&|pq95!SrIB;nd z69WZ8h9!XfQK-hBq`F>w&W|gM?N82$QiU(k5fb~QLa#g^7cD?Lw?>KGmN)CZWk7^Cp(Uv3 z9=uH{Vr|!ZDIN5>chIc*@}3z{RmBUd&(I#hLfG$z|H|nV3BV^%aG$W2qCNxLwfYuZ z2_>;JBOIl|U0O-G)9}i9(h-n3L(voEl>-HH1|kYRbV%F2_#hT;Cr-0<^Wd5;51%@UgH%p>}T9wg_qWh#wz&?X&wUqg0Pe?_KV6kgdU+4MHM=59BCnlw=&! z7K!rw2{Ths4-4;sEM1jW5k*NZTb59G=VKzV$q-9|>j2A=%=-iiO9D1$&Z8-4hoM(+ zY_&S7igH1`ruJuIR6YsVm3d^s%0wVCY#+>?eC<5Sq9z~5+Fkn7=SPh@Y2f8aj@N|%hbaOw!f5zI$HFKwm2pgz7Spa8pMuY_ zYhKTuJ$ntWj8f6ABD7&VDwS!78-b{)ir_Pf>|C5o-pcD!A++VQGXH+(zRyadR^2XA zZQB1Z6SfUrqo^q`In5*}_QEKCT(G=ra@)3b^K-N6)?8T`MZqo+Gqz3tHL)0224bNf zSWF^Tnxk!=l)|o!>CBPlDN!oerCkH(i&sz11+vDxMO>07ZY#3J@cocF%pPQGi|?18 zM?U0B;V9~Lm7}L!jTJzFNCX>)4oX!DM6!A$2f+7*15;v>g1;#Q*nN7GYIi9#ie@l< ziEKW`1(P6(2$Nc=6G6dKg@(@`R@@$w)L!me|3_od!l)|PrL?-<%tt{+2^@PKiZ!7a zB{&KYFxd~X)Jkbk^gE(NmSMFk{_;7 zFit!X2uDa62;zh%LZxkR*N`48&@Ml|EgIYT((Tk*P!(0ByA+Alub0&na#1+Nz!DY~ zqd?G9*_3sh@bJczsFn~ihKEOmkf8u3+$XA-YmGwU z9X3GW;Hhm9z>=HaIBDG*lj|a@)xaR<;TQ#?3N{KtJm5{NKnXNPQ5c?rZcTcJsCm^* zoXD?=tddQG!DrRGlU#%FhigbS1>OR}pHW0|bZ>~Z`I%I9ZO4kOlwh0^S*ZvCjWhRQ zSXfTzl3{^Qm06%|w@@U5uH) z6e4Ay?BTa56Gl)_koc27cu-phQ8S;z>{B935+|%*Dg$%Wid+TKF0E0ZUGo1k7aX*N zLyn=}^*pfn|CeaN9l@h;1O9OTH~w?{y?p=j&G!D{-R|w>`KM<#ZgJo4KGyZLtHu>@ zUgsR_c*9ZW=xqMOoMe24Ti5&AU$i&dgSHE81^OHM0*H!B+I8TQ{RiPFN_SafgG%)M zSS3gT&J0K!)fQBF(+UMcjbf6age^;F`!i-8|*45 zqI1{=M;)DtAip)>+XV^+G2eR~DsOT`~sIJ4xQG zs2&yqSc|$)kIW7zZ>T+-On&v0WCN4UOHhunKT;n>nJyoG6XNo)Gf8N3WgVs<5*G!h zP_RF6N;$ncc1y{;2hYdP1#_dSXxBa+9DE0F8(Hjd(-9^iBe4~VYAHa11=>YdB)hV; zCveQ9IEo5hUKz|bohlr#e=rER2oEBOgZBv(XqxEH``$rheF#6F-HdBin$MgZMd7Zx zT&C|G8>+1~q;-SDAwBS6s`)lpTp zOC=@RGdv6Vd>~lt0?|z?6m%P?i=4|dQ}Qy_9r-qO(N{!Ky{p0tHpLrIH`prZ8u&^? zLVUKA4qy!wDx9z*1&3^K1s8f0N2!dLycsBGTL7a%?ISn_hZ#x?5oi@Kk!&2)J~_-4 zH@9`zO}9#lKdP#AF|wgYcx#B4K}HZtU^L~o3JS6iBNy~~tu6ALpMk!GJO5c3MWL>8 zz3t?BgQIzG#O78P!VwC4gZLpCl9Of}G;llOeBo1^I;o6Wg}O+9Y8S{%EbJs=&gnxI zC(@Tg#1*rf}R# zFGMQqy{@HE)a|mPkE(xaWWu13Oqd6zG0boxD+@(yq^;A||CyYEI;$_Y<4liI;V#5F zm1$EmC;tDK5jF;w>_n?F=J19dfHFZqA1p7*)+!48G}P3LFOTI z1yv4{n1b9Eid;L;S@r^dyYbcGL%7zaB&rH@;XFFD@2$-cBbcJuie_=4DBKo=j$tK* zw8)ojvBR39C_u5aBC1Mq^-?&G6yj#P6s7_8fUHH@`qn6Dr~F{@Cw%$~i}BO^zp5yz zaG9a0*QSZL1Lh+cJ0zhjoh>Ljb)0f2^ioPPs$xj5Gs9a)R76pUt7H@xu|goBnweVw zu<@4u1`3`wOib{2!3pOizobFg$8peTdKAUD%G z`Eovy@tB-=57K4X2dPNMW%(!%dEw8>XkRrGb@@{Z2!@4Gz&i)k!L@J}isA+o<(=|t z3evIRd%o+nD%wXTf)i}#vNUWSa!joG==KOQ;VcwM6fkC?w)4JJ*0idD$~a1xchUMFX$x_GRlkbI@1O=A~IzeKjA7WTqs@ZYMcGNh zt0NRuAHz~yG9o#_yXhY4D}fbsteOLq%rykekd)N|MPUIXy(|<+8PTv%-uQvZQu3;C z-1k--?WJZx!xeXRVI@e+L9b&9Ohi_Yg`$Kt#9LO)e-1i~L(I+GtMl4$w5N=W zD72}M!!SapNDv@IgTZNJ3q=kAWK5A34W&R)yM91t{GCV>^uSESjfmv+#i%esM6Yuw zP1R-*iqo-JSg2^s-jrOZMN|2%(Yk1+nhEZQgmYXe_tQ%uFeM$WbXT79+awDupuwbLXi^(`YSB{vO`lgJj|w^`2V>7@5e&5%;V8;;S^H51>ls3cFwv#%nsN-SQB<4@8>dHIDsr}TGp7N{ zqE=Zh+CSOJ0}UbF&O(WcfrYFD1^telF39=sKpb*|7$DlerB|058kDMECS(FNLi6cJ-f}k?QAGrYFlh8j?5VH-{ z%73y-Y{}*wf{Ae6h6Z$WVq7!4nzQ?L+gTlltwa8woBQrg!><^U8B95b%l z!QNY~PK<)l3xSSq)C=ECVWu6IE~3WA`e>;-c*t{|s%%)HlAb_#3RFy!Cr}(j%?|lo z9g?mS`y*{$`0A7>mEjU=!bL~ejZOA%$)FgXW(b(D&6rIEm)&$b4mx+-`! zFyH9Vd3>NKK?9o3*Vd+X4d{wYu7;|MqCS_^SCdnFLB@sS21}6)UI|cQDnUWq6UmJo z-638XbUU|6)J0LNt7g>i<{1$lfYgT?!;z@SE1^Jy6z@P(G~r73Gp9OGI3;cPhChC{QEW9Gw@WLKi(pkTBg_5><7)h)vkIWrnwoN?Win46DQ* z3=S>~#|Gzyv<(B=5(I0fP+8xKX;D?YOW#qU5ILK}6yktPOXRgd+dz>$07+fAsNIT& z5z|k?8$v(Q9d}u?|D&D0_ zoDxxgQV4i?%pjp=OiKHQs0iGxoH^4wIO(FDR9`!^IEq4EWfT~;f!0c?(hM>J-lBL$ zG%2`^L&iA;V6e%Y)Eup|Da96Y3bdUyZ4JGu47FE1KARpLt6m!P${hs|`4R;LPs33p8J0jH zqYNm$$W9%};ZfrYt%dBP+|EqBNq>VNi0+ zovZFF6nXFzPnq^*vg&d1>-^b-^5`geeO!iP>bJ-S!QzHD1mOgck+AfDBG(-XAYa=` z{b|X#Lp$Dh_LLKj_$eG6sWu7K5Lydn!4!}iU;|iMFzUs-Qz#h6V1v9k~iu>GFcc=m@nQk{?Sj6WJ}~sbEYpvS>YmLUaPeOAM#&`K^OQu=-fG{ij8TLzm#F z5II-*JRCV!E@2L<6bC3A9wwyVt#;8%?UD8P*XZ`|fhEylYDvV8StM>VJzipqb5V!xAYky;~j{G#l zRI#iLtjpinfUOKJ;@Ap=qUG5zijNW6xc8?WJhl4AB#j+YRkM5{#b4M(o|MwP*j1EJ zDK8zNQNF~Bl8?FC-XwjtHCs5RTotp5XDe`e=g1@wosbV819C81qnJiRd9E8$3Iz|hi!`Pbc~}z|a9;D-eW%AzOUt^NKI%ahAqy!AL6Pnd^^rg!n+wA?xAv^| zxcHnW&$I0^Qju|cHyi4H6j_K@U8{+tg}aCDc3ahuDp`%7ae zsAaW*D@s%>mfaAFZcvR(Vjc((a;b1O^}Cal=S!$s=EA4LF{_jol%{XSh~OOj;ZQP6 zL#vrfP-JyttB!m-6?s0RA2+Vl#i*>7lpx%8%5w7oK)XWFk&{E_U;@SVQ(k_TYdYi? zly)PpeRyL`71i3Qr}=A)&#@0ka~ugNS#e8oc9bwf;10{n)gm3ew_$Y?bz&VgEk*^m zv`!G;2woHg*dU5m-)p@jNI3)uNfXPGn43v-#&2%_?@hhwO+Wwu57 zPkRW9YgIL%|y*kI^8YJi=qN@ z*xa!1Xum8Jn-CN~xNvYvQgy?MNb%nH|0<4Ap{*2;*z|ibuy_ivSY%Zqj7X*fMb*#{ z5(AGuXjO+CO}&G^j8q(@N?SFu&BG&8%9?951=%zBH^jsw!)Vf>rY$Vyg(V zK{&&3r4zwRFbZsh(k)b^U)rYQKH+fGjrey@kE+sIVt1WNwfF!i*MpEY0)dGG0E*ZV zN;PL#v^{=bwE$VNyIplrs;*T+^Dqn!vcY?Du?U&Lijo8c%0l=iuSc{)J5@dFVd^?7 zj#^c=6yqR^fISm3yE)c*pxFr=Nj5*oEPKxBZJDS`Pv!dEs;E^}OZL+`Sqx+swnc(< z%69}M^R?W+XfD_TW3N6C!7h)4XLOCQ&4~f79#!>76wz2 zDhn#d%&J@cb6s?C;(*j;=tM=ZpWsV_+et^`%ov54fX&NGOY6`9jepaLyvqH>(Po(u zq-;#W@Q}PxmL^`R=utvp7h!!+1A*3ZTU&g&?M@1BT~!us!pJ1IC_#RNyt!N$k3&`} z8jAsn-Un6DMzu1OFK8Fb3@|pB&ZuBt$$mx=-y)U=wRuM>2KAb~6eoJKGTNZl zN0}XUlVKuw3^k*ok&Lnkzt7nzWOf$Zg(86O=GI3Ss)?F#f6rN169@q!lk=>UR8{2+ zjDqb!sJ!})eVK8|8sx#&p< zYnuZ}o=awxM(fn_=#IfgSst85m^B!c0+g*$6u1I&jc!Vg7VD4TkV$p4w$*yR;Q>_; z3-J-q07)d!1RGdT3v|a|NL#lzrS)vSehvB2GolOBdXVYSKE%LGi8c0|J}{|%yGtd#%`mV{T|%^|AlQ2^8dH#leI6k^(h4a=EkUAmULCA zlT;PR5riP3TqVjADC{kTAG3~0!7IxBi5pI;Vph2_QQ3Z)^m z+$ofj8W|7&>$ej}Op8(dteEn)1w1T8pg1-IJ5H6^C=@sV6p~%AwmsFp;HXz0z3s9y z8e>#KOVXACC0Ai|Hl}DDP<%=d3W_HLL4>lk4bcut!tD{hHMTN#pq|#ysdAUqhXk9c z4pdl}+;GeaiA)-#p+1FU+ISHg!INXCr&R^)L74_y2VKuf(E5llP(mOGgyyZ9-If(G z>gR3yOCI=dIEM0BOSSjLz+fY2SH9?vE+AHH1QbLN#Dff5wND`YFU zCTCeihpd!QM-%T%pAl2Fwz!4Crk%@Eigp5NBhO%k0im6+E}R>sJ)3W9H-);o`$y=D zIdNu8mDnP0Zr7J$c#r`CWL%I6(Q@FrDk5W;K;#{3Ti$DT{FQQBNL37VwR|k(!o4DA z0G{AL3x#FL@NUI9*tDGNo=>JkW@Q7pxx6ff(pru(m;8P%4}&W zlpI)JIiwIGCmlds6y#<%hERO+SvkEblU-2lMtlJKTCFm6JZ2%$jADS(WJGoxV2QxR zvaBqi;8jx81yPEU4^pw`8mIARVW!8d5?i{E;RDPBnT(Ui0b!fKJEU$b6nUx$WoA#x zZchm`z4$_>;tTuFh^e|-W)pHu;XNXq0$&|NQ&_nb3eS{AD=gp9E*UR7l8n){WihLk z7H83fp}-|DJTW@-Bso2;QD904`#fJkvPUsFjqVovZhDOBYUwC&;Yu||l*489fmgOJ z`UD9u3f!5OV{8AO=HtUu!0`K&*s&Ph4K)dOhc^R!PEkX8eT>u!1z8U{oKv9fn$wmd z*>)Izt9EWo715&T+{Rzb!sf7cCp}Kyn`{nH;1Z6UWEE-WCw(i|hF<&;(}tL;sD-~{ zx4C3{K$g%M*a!$(#SVhP2~)?Cb8rW>d?9rd{rqTU%qp%$tcx^=oIA%LDJDTQYoTDb zL&@hBtvL4}7qOjC*EyRnq_TyQ7}eI25rfmw?!~Yy6lrzXL@;;4oB)cV9~9`<&VRG5 z^VtzBN0;S>vKWeJS!YdmauWr}ik(_IWB8*)y9pG+Q8=;uP|{!I%Y`xV;BjA8#k$G+ zLoOORbOgP>cp1zN9ICTW!rfT~{pbrK$(s}P>p1>6J$8(miasZ#E{{eX0=5dQ4uhB^ zJ##-$*eduRp`0mu+Y=-WOF1xHA3Iu&j*?^Rn>DC}Vpa#YbB-A-$S4vl5F~l0UeX~I zU;Qp;Gb>|9sVPw_ZaW2UkMY4R2&uqS1#b$KDHM5Am@+go1ygd-o16)*j2(%IMAjQN zIU}4r!ULEGj7gM$GZGXc`>@6GPyap@YF^TZ>LqTgigi^h0yU`HkZH&G<`1x|x>}e} zm>kju8lZ6Uu9T=@&GDyDm|%MB2sIU{l^$~R*gUQ*pfDbBEvP|IAO$K5sJy*BYFPGd zXIx$}I~+S)Mn^HGLnqG#+z#`IBSNt5EZN2=3KYZ8%+mI}oWj|xewdpoDq~&LI;dgb zEuIPcLvA03QB*y#pvX!F-4*1mOJSnL>3josd91UV2&zm!9upxh389G~Bv(wZMV3Hu zI65!8?~lpqvV~XkJ(d{x!PP}E;Q$v<>m}?vN_fqZVobw%xw3+ z+>g5N5B?V1AN(r#N$}m^>%kX-PX!+i-WR+*cmui-E)Jd>+#TE!To+s(TohaotPIXJ zk2Q}myO=@KW$MQ7#*fC=#;3-6#=nghji-%AjDHw+7&jVM8J8I68GD?kI}4pz&J&!y zoJTu5I|EK9zFP2`W544o$0x>CW4*D$XmY&kc-`@W<0;3(j{6+9J8p1X>A2W&u4A`j zi*t-~m}8w|xnq%Ifuqtf+fm|};27l?;yBe=@2qx~Ij1|%cH}wwJNh^>9bFw6#+<;j zfyV<61nv&p61X<7FR(Wd4eSVP3aqwYZokm%7aS5iHJBIdAM6v%40a7>1iiRvu@PTe z31%a$Uzrb;U?t!j>P62d)|d z{f6%^-!H!JaI@owzPEg@_@4DW?t8#@x9=9zdhGM<^+kO^-@QM2zxICWeb4)E?~C52y^o+%wm6aT|c@8LGzNh`8KmDW z;7$Q|2)JFq3;`nr3>7d~fQ*t}Bu}!w^zQO>w1Cb6ItfS@5MYodv#0$mPrM={?PGa* zN5HEB{w3fk0S_|hG+myK5g-$#9VSmQQSfhm3d#(@{qppsfd2^iK*0L~WZK|s^7O2L zhXu&A!F%NCP64+FxLLqO0xl46zJNUfwg^}upkBay0rLcu379QF)|c@vKV|S~8X4*s zpAh8Z0`3*CPQWq&O9d;Fj_#4fW88RS~HH6Cp&}mZv=cM zKsGY{F?sr@fcpi=d!}D6PuB^!Lcrw$E)yUdnSQQ3Z56Ouz#;(+0u~4e3s7sAz)uLkGJ{;5JHJIjNLGsR<<}7*Qoj1)Ad73C-kbqnPd=94BO`eVsaFl>_0e%5q0S*DW z02XAX!E9j~e=yScMZkUm9|?F*fCvwR+0ZnY+)RVTg=t(P|AY_DG-N{z-el7_PyQ|< zV5flX0yYU)FF-cLV4^jRrGjMoG>sa0VkR+-dGa(@0H3sJ6wA{D0es-5F-D$-2^b(C zB%rr|;{;?1=q`YThiS;x8Kxj@4D7OJ_TS{`zXD|I>_5;G+TI16D4>`8lWzrlEr6NC zw0|s5vdMPYWc$Ab`GSCZ1;}38S=yNP+XZ>GfPDgF%k6P_Vh%U$4f3>505gecm;JO? z2~u{@K24s^79cxlA1Y5n1e_wEKtMkMJq7d-aD;#~0YL#miguwoo9vnGNB%q8R{}m4 z@TmYHKAY@|jfJskdr|)GIRV0(wkPH3AptC3Oxv~cBrIx^eX$8s+BV4FEf%my083%h zRwqxgOSVdR5^l5=%hO~5!j!f#@-#|-?2>J;JPi~eduEe8v*ij>h|eZ_W)rHj$=2Bp zmw(b(fQNxD^=rvV*!1nEsml%U1+ECg2eP4+^+R zfNZ3GxjYG->6gjVr2>T9^zHJrO@K%~eXTsL60lN$Y`HEpsLvOq@U$*luFEFtLW;WV zwLVh*%fE~H z_jvw2mVb}n-$VI#KL0+Mf1k*|`||Hz{JSUr?#90}_;-MRJNP&6kI`v7y@k>0(i5HjGy@_<2wQ0z;Z4$9h>OMbO<%TljEl*0YVgxx$-nez$^kr zr`-aWri@O@Z7IH5PkUmeFQ-`;7PUR zh^|pQycG&zc!a953YR>cTv5leG5DU!+V87ks-+eoUAykLLO4`ihzTgv+8QOXuk0hn zr&J$sHr;Di^6!$E>aIof+om5Y?=CSZgdq|Arm3((Mj@n2_w(myS59s#T(t4V;V74O zm&VXi%hB(tPVG}3hT_r)J5o+i@{h^8!Yqh5Bm0jKJ6CpFi7rM_t^>0a&Fh`yB+FmJ^;gb7 zX$;-7ibL%B$<`!PCL}fSs^SxoPDBooO`&>#CC?_uAxj_U%be!Ms9%=i>7`C}^9t^n z;G_dKOMO=mk!M0-vkIql3w{ zhRb#!x?Wltv$|;!^5&jutV(JFKnFMymG@v2(E%l^<_$=p!f(^3T(wvnvpQ($cF8o^ zE=qZECLK(4An^ed!tK=Pbe5+r>9S@nKFJlnwK8V4$x@LbrBo<+Lgfuq{eiQg>_Y;@ zZ3H>FffXsxz~qnVy6&9HnAJ5)*Z1ASqhha!XFyg;#6aW#1-d}87(A2Pv?Ya!mS#Wz zJbiI}%xa>gYtGkTA{H%D4M_w?du5@BUs0V{*tDiY`L9(kaO$HvW_8!nRVqizj$t9l ze<7VhwXq2lw=+-;0zTO~F(pm2`=3on=^kGhv)XNOVA?-1Fz1HoJ7!dNnNXZuc}bcE zXO_S4=@j;H}Dug!W+d&wNqNz!Lj_lao8q4E+ znN>KZ+HNUVrM)ZTBQYiV8sd#?0!+y$>>-svm}xHBusGzFRnRobg=DjkkyM#|Mqy+$)uW5<$~K2ydl_A$ zFtH?N_1i+)a49?6W>lnu9fw#@8A)lK$I(*@v9`0#o7Z+;HsrR;c3)i;LsKnpJWK(N z>d$y+xh5BHY2E&e!WR_nLFHh!!oxpH$r3pGWqbqen^!7gXt8Cx37^rpQjQo#2XrOO zF}!i>dIU;G!fwi%eP;@^G;|)91y#k)Of1Q!Zd4~FfJ7l`fIy`pK@|$=JSuMQd~!;> zKc%qy+viUFzBD#eO$9y5ADRT&LjNOGj2t24m|7E~Almp2kJawv6zR6I&iLrXrf_Ts z<{-Am2}#2Q^_g(5H8nzsT}~(#Wl(8(Mdi&Y9Z=J?{BhXI*coay^hHZmlLRvswL#Db zpkS)G#ypIoLPSWi?A57!wP*QYTuA)M;*!{4nFy@n(EAA~pyUm#0!s?2l0+GHl2Jt8 z*vTSo-A8S4;fghMIdj#N*dRq@=q8$gVYqgZ$^;;Zu>_G=R3^aJ-5}?kTX@vCw)AqJ zF(|NK^mJA1bU6n4(omu}mu`WP@`(iXQsExm3=HE!+dC;Gs@na}J!pvtmBda&y; zbFfAs0!9_EvY9Ouj0;q#V6Mw|x3!8rm(mwMpPmvMD64?@b5MycD-2l$u_2wXps)&T z7kDsK*`uv}Zyxw8ex56ioht84*D&c$35vZg#%W%Xj|Hjtx*2cd@fdR*%9C zC~qvCiq;b-=vmqz@N~m%$uaPV#b~Y?JzV_%&06rP;3lI`_)0|g2M?1cCEO8uZ-fB+3{eMf0PN@GMi~9eawqx~w=rgtb z+UC?<|8U&uxutrLRlI9tEpSZ()o3Xa1`47c0vVK+r$tlI=DWWA;mM;L$5zFu&z7_~ zmFTL{K=3n&fy<99+h0&%K}?=|+=D5}tnz308?Dphs_T|6=g}3B;l53-0i_ktqCkp_ z0v%HOVE%xN_8jHfMROtG&B{2sYn55z(2Fq??UyPs0(o#regXwHBw0|F%Ef6-o0tm2 zajV%DT~K;YAv)3}0YYQ>3%*XaU}|_HJ9zamG}bclq6v)UY2Yy|46KKs3&a!(58N(`Y z#Gom;DHt+^j>ke#Q~_L{f7)#+SgOmNd>_H|nALVmb*CF;yD$-mnVc&rpTrI_3I-Qk z@Vuht*V+?aOQ)&}38>SSwu!o!DB8+Jpx`~wFUsQ&6oHb@&(S2@(sR+i4u)WM|3wbP zxM?wT-7;y9c4`zxX5mYM45A6hHP~`!`c;e!!%++YL~~#} zig&G0s4hhCD1Y2{DXFZ5elE@p$E-G6+|#80h^atJaG^Q2i|ByWCC3$@bX4#gvPscV(LXI%+N z$*}_#LWClx7^K~={X3Y;s(7nr#;RonP?CneL5An>E?JZm9&CkzaRz59D^I(mq+KPs zvnyX;TNbNQ#G+HS~nlTx{Eu_h=tWLP^;o^G9olPyV$tzh~0Ti!YJAx z@MU4mYwfOK#TLGcqCQrshHpZ;V7m-Y@jy!*l7_+XjKT(y-B+-BdxtEfZL9dRRHd;B zH6^84Z^!tskzsy=SGfDu>Nye=oG?h9yv`ZP?b^g?w5;XTvAODK5USu_2{{MO8F5%m zmc^${9AtHXAjLBaiD(_`gIhT()xM?H*8% zJLt;4bE;xxYPZOdd_%_Km?4-OB2s-s1tu(cu`mI+f$E-=wDP?3DNlKJajaBLg~*6T zSD?tI!RVuWfrRlBC=RcKTLWzw`{kFwAX?)U|9^)TygHZ@cp*?7FyQ}Zp*nny_Z#m9 zZ+Fk*`2OE7`2Js(>nYbP=TFXaodu|upK5-B`~O15KaIKe-|dvBP&mf3pf_vqua zAG95*`~UnuO5>_Km(%>`W0~SCfurD!K{AL@g{2XbK?>1T5urwG*BF5f^`+joq0jKG|~89zN@A(PEEOF1LBjcr($$4 z5DYbu_msYFjUo{N;vRc`Di*<@$M`mb$~ZOV(lX$a%dKcCq6J}3Ccmr<)5FVv=>&5# z-*)f?t?g*FOp?@__fCnUEtgk>Z;Nils5lZxYJ@PrLrM`OP&g$>h3wNGX^Zks$tCvb zGAC|z;zEnjzThArSPk!Fgw9Ef}YUH(QvIBxadl9@~+^FdRc1tkj-IXqA*P;kGo zS$Vx0Q#t6`-Rw_JiCe9>@G8b6CW1(YFG~R;<(k83$MjCf_IH1JO^M=1z~-mm{Ft$@FMw@U$u1}>+AWhh2l8%-irHZ zaNUcL6sNqgO;%bMB*j)31(@>|*R?hBrd?bU)DTzAwrC|bte!#WK?pe5RUpp90T7Bg zBPVOb`VP9<%jeQP9@kdH(T2+c3i+DD`GhHCgd{f@gd%sXQJ`b8ygTrT|Cw>slnVpa)aUUOMAHx> zIB>XnB)x>D<&t+I1MQ+Q?dp-;KOMQqf7MOX;?$N);vHYAr)nN1Y#3r#0mThy_ymdw z9Y%kkOM5QSh^_dJz+iVcZZ+V-LDn{7D9Awgsc^WUkFZ6gW-S!x0}Pp+H8-Via;>;iRjCd@w{$_Yb{QWD9WCdeAe1@3$B|3Q%CXrGZ)I0ba%Re^?eJ2{3o7FoirHaIjFQ7( z7mLI*$$M1EFP47L^Y@V2uuYTCe@rcZ~Bu!acBuB-0idd3OHnzYoA$P*Vt6iP?0&Wsc}(V zX6d5L=BAqZrbVTTDh936aBb(+j>>pI?ICV)wt_EGHUn!027oeU8HELL{=3VkDF~Vp z`49k>YrsMEsOiqOj9w_cQlin{8d@b7*ws<}gOjnwlVP1Db{kJEy`2EPw}9{j)<8+;@9B0BLO3f>dEC3sEn z(%^aM!`~cS6I>Fk4^{=|1WSzm_V?^}1t$bY1_uX=f?2`igFVpIo*r}u^}ug|9|B*X zhxtu>P2g!G9(Xu#Z{Sw+#9tOTKd>jTC9oEq?+X!UC=E;vOhDKB;6M?2-j5ITK*xJ} zz#Y)h@BV}T3;&1yH~la9pY}g&JmJ6Bf2;pm|7HI3{d>^uzSh6gztCUpFEzUPr=r_^ zq<^r#$e-mu-robg?&*HFU-$jy`@#1GI^Ey&z3ls!?-Ad9zT14)q0jvSUyE<6ag%SI zZ<((FeePwxX@hHn7++)wcJ^d0HT@OgYTbh+=xHxNGZzU6({`!Dpk-{-x} zc)j_xL}4``iY;=lGNBYu6{} zcYoFOtm`q?{jNJ)H{kw+VfMe#^M0W#>e}wwU|j54>1uK#TIn za!zrMbq;f$=FGR>v&zxzsKreTvy4rSDUPv@VUE)r`M7_fx1*b*izDE0n1ACgif_&TnD3ddo6j4~ zM!xw3Zlk!%yve-Eyx5GJJ8>7qDzrG&nw91(`xE9A+(j|WJk88E`U^?vI z8h@jw*ZiC`d92e;<5NVBju+?i&TVjuz;+P@u7e;pR^D48xre!Q$x# zFx=HC#BfumehiyCWiSkPaxz?z@fXA08NbNiXME3aUB;IT8#A6|xF_R2hKn=geQGnV z=jUY^*D(xdoX2oS#%_jdGgdQPm(k3yA_E>D-fKw)632imGKMo;l`)Xvnv8sgJ2Ltz z{ILwTWn?m3oDpPLonbIspP?~am;M#QhIDz~rRksWb94GT3>T!o!LY>`B~bsG;hvzp z=N|Vh^6V}WXloIu-O6xx`e_2wWSre;u=()*yS+~OM|I!vC;`xTV;*k+u>&s)>)5E%3eG==`R z+ULl#em%o2=|csk$v$ogE|=%v;R5|P3H047(EASsZ&$ETpiBL|D@UH4F@X-D_szaD z_<6Ipvx0Kon>~t89bv&6a?a-jSr02Hq9uk@wi>>?F^IoWn-DDbKd! zSgdVa#*Z7)y9xBmbQ?Th$g}%sfzFUXyRg#+yKKV-o3Q%^?R*|?!%Y5X>(k_I*88uO zXQ%Audi!E|*6wDwE?uZ_U0Rwv2g?QeZxiTSA<(l_pxYzRCDLS_vyVI*=PM}VuR~!d zam6})EW@>F!gXuYWc6!(FA3h)OQ822f$r}V+$GS}Akg?9!!_x-0@LI@)&%9f*Z757 z*7()m`-MB!_{!wp`$QV8aor%#w#yi<4sI9d5o%xUmibn@gj-hIzvsVS6%eXlN0$#JZ5%$I*>prsVbYn5#C%HT>yuk`Ozuv(x`j%lS^m~f?It>A6) z8Y}g;`FVv~ws(c=aCtVn3N&QAVFG=!>}AgT z<=N3+pdnPa%-&C)ZDvo>XKzHHPqkR9rE|u9)?T&q7#?+ zgh!V6o))}!lR(c11>FM8zZ8`5mY70COSHX=UmQ4Fpik*q_XG0m5@uR#s()t}*}GU5 zs$Q(0!2hl}ZM1?y&CR}d<=H38ZuYq3*)8jAb{-|qrf@;CO=MlOO{k+OO_;SQP4=nD z^9cX@CeI}b%Jw$dE|+IrsHaI2=3eCghVhG>4+^x8XV{oFK%nn2f!-?wdg=taUlr(( z^*8ut@N=$UtzBkyS@@zYUkGnxc<-)W^MlbX~s-Q5z zLidaEjElTzxP`Xa0`=Dz)(1sa)dx%DIZz?c_pv}zn6oZTn6fUdi1Bqn;fgxj9rCQ( z8P*0xNYn;}nQH@Q3Euy^fmzMwq2Qj_*9<7?865a@VPpjj=@$QEc9GO5x3AqJpyC`TkmY_6m8=$F*tn{_~BPK>Y~??-i(t)R>okJU`D1e5s)9*Sx?IdG?!f`bJ5guJS} zA$j(S6svOoB+ss;0-dssDt$JSUzJd6I9;|c9K4_ZSvWXfVBj->?vE7QDbOk061K~B zhV_R9ua9C_>3cz-SN>n6Yo0v2U$A6>$4BvacCfxtO!`sVquV(-61%WvN7zVo;LtK=uoHJVZDSBtd{eloyey z+^l{b474<5S0EsAS2%9<&QflKZd*kFiN6ky06fZ5tRgN!NxGzP%J-=xcGt|uXQjRv zQ5iqbOKaEP#7q*QMhcFKYb7~ip%96s*7w}r^HK;T&MD`VWm(+nrJag>L~oG3P>7V$ zpYrv|2Kt)@P%`|}xpAv?7I!ae|HUj+{lw`I1l}m4&bmNgtKR zt*%*`<{KUpL?B-wpoFOkw*iF;OvHE*w=OKgbrUr;r3fK6EGo^csVf7lZp^Hy4mSbY zTv3x*5w2)lRN6SGb}a6jxTA4e+-jLcj!&wS=$->U7RrODNf?D=p_9p4dR==tzGbVp zey=#Lx?`!AN`G9=1MiENE=MV?FF-Mh5`2jE7nVBPiWoNaD1CGMQWM}Dmi7)t#AXmZae+HVR7JInqL@3*Bum@A1xrc(66bX_ z+}RtBk5JL?Qn1H#%q7upWT?O!*nY`UOA47X`=F>CZT+YexP65UCCNQLtB$LlS^kLJ zXNT+)b#Nq5oW!EcLAJK#h?Ej8Ykyn%fNj{^x;Qn>f)wmc&N5yT)w^=_1x`e*iBK%l zu%>Ku9bVd@bkcbF!0EI&+Gn|x1RYf*|2g)KbRy9O)j+mFVH@e%@SLTC+vAy$@1cHw z_)QIQt6>(mLg-&&C}MDyE+qKDeyvcH9z$v*tJfVJR5-J{;=?LGzY&g82dz|$ncCIz z{?cv(h9^pBg~DM`&roRI?o<@cre9HzG41`bI67!)?~sAfrTv9uCuez((&5fy5vNGb zajOp!Fz5D2l5@v*L&epAZl!V6J&O`PyRK3pw2fSALMlv_BPi5?fKG~zO|mw5)<;C* zg;jC1(ejp|sz7@U19Enh%6@RM)yg(=nctUq^KYQ3dRB! zPC7n2(=)V4|EBcl(oIwxIJv$-71A;)=Ok$w&ze9bKx&Z&CH+rbF$237M$zps zOx@S$3TqDV5`Yq%`88hu6c)q)3zt3M-h>F5n)B!xhOa{nRC!Cs5Gv29iW9B#6BQT; zj<}u@2{wx2v>(wG)EkaH>_-d=I!I|eg9Q*vW`XC2W+~LZsU(G? z>KpP^mxK*lEu*7_V#qg5T4NTn&VHid4Yeqblk(V55`a!Y zIz&kb3u>JY$a9KK9EMe&0BgRnduco!%K3KfWOwU6IVA zQf7escCx-$V_-Py^b;1=ichKj3*qWkVL`N;cFiX&XNoKqF4i^KKPs1Pgu- ze3YCEmYNzxFTVY3-PndqnK0;7n?`qSz(T+=i<)`XL0P1OgvGj$Y6}58opQ{E`VTC3 z*>qgpaO>6y4H>Fwsw=5LJ7Q+>%iw@TD_j$qStw|O`bH5zfwp2|LZ!1h!;5=VE~{ur zSMr2gLI|LR8G-)7DZ@g_SVMy)S}rT6*39b&15@b^iuXee-PA(kgzF;x0CdeTa(KRm z@h&Wk0V0*r*WBwOi_#edEdKvtEx0w9A9yJ+9sd6g|5?6=edE2~c^kZEAyYlh{fB!U z@c)mwrntP$tDQOeYx+Dr=(yI=!~ULqhTUV^XUnxdZ7sF@Zn?_RNjv<1$Ng`*mFfmn z21{lxdk9ckuwLZAhhauGfWad05B$RdZB5janS05)Yp?iW?5?T?qY@TTK+E6UFmw|Y zc5?ZpaugH`Wu#DIxiddBwKwK|%LVYE1}ck{O=zz}PcQ&b2?NClg8-u-#TaJ+!br8A zDf>+IG_D)M51)Tm(O{Iq0>{(}%|dQ@{%EFP+t4Ny79#+$Q_^$ioM{39IA|Kb0bbQ$ zRK=39MrFkK{Q!Jop)gGd7JNH;M6RO6@bpxtq*#^bdwVooA z2XG^_8;2%%>tgblMXC+lY;CpAl$0;~_0!+nt$U7dK;f+N7cD>Vn8*Ntb%SaK!e%vQ zaRE?9?uo}GIx#yzE8O4d`N{^A$nt`qg2LJdO<}{p22CgP0%0NJ%oG+}H&`|~tv)m{ zW94fZL>X!@DrC#?x=c&?&6pm5S>f}l%?f3Q2p)nLm@x`f=09jgu&WNET6X)*p@yT? zvbwbsdV*Pt6-To;h4DuPUYI3*N@iAvv1WRj@Fl-sSKMG!%hKH_E*TS@)+`08R0?Ba z7C{6tth^a{iI!$@E<+lN8;lZJSV~rMu2`30#wbvLKgz2nF(X7UHfT=%fZ9Z@&;N_> z6RK*U;#k^=I1se2__vcQ4NVD|qKRtEf?-C-gu~o>PnmRU$s=2;8dRk$-&W&ByZFhN zeFjiNfw4tnu&`6eip?Lo-jw>B*@CZ)ENQ5U>lfWZyPembLWUB3j;}qj$lSz{6e>!n zUuG|3As*j=a#^-7hdG029LE5_xrpdd(UsIJ3k$OdM-)3Kx8I!!8$Wj>o@2XmM5w_i zmLs8@zZhEtQY|FJA!s53L&d(F7Dy}0gfK}myABMTGLN|kpkS#(5WmXzBb zXTg2Orq3PlTEc-W=!kpz?zlJ9V3fy_7T#<$Pj(H?N_d+`&t(aQQt^kj=@}DBWyyU6 zV_jX+fZ|wnoFJi!o-qB8Bve9TVX-BYtsyL&Y50?OM&c6hijC}gg&I_KEXvD(p_1u` z&cZyQd776-EDD0+kjd4q{?~+sv7*g=RAUfoFp6WT7EfGc>_)6TL<_%~l|1|lW|3S` z)nbdA&FHL2zrNq8>c%H48z#jSzC(LM27qIq1F6{e#@#Ez;^QPeJM^N7A+0QRXT^$KEOpuYWEF8%od4@qL=b%;WbG~no)IN?VwKokG;%Vs_mS36`o@q_R@*A$MY8bDU z3)ZuBqznWL&%QD{uyGdLa@wKTNL3e_GL=>?tfqWqX~Q_x6-938X|(BXf7re>w=R~6suRc37l^TdEszF7a7{FA2FO{ zDl{%Er6fVDtf5HRe7L`A8-6rvW4T5M;b!ByZ8$ryoGFA4CEqc-R>LW{S5j4PM#y4 zV)qX~mFKvgbkzdwz0KJN{{IZ!1OLB|{bPH*J%Bgv(ygyr>#WCH_FD#N?`lh%_T3x* zZE@JBdIhpY+b$!*=9S|VgCymLK?(~VFM>FkLsuj+)M`ia?aozUqwKDGyVJGY~*|drB`xY{gdsb$GLn7)VMIJ zTiH|_;_ShFP%0nc4w!1RD;u#$5un?gHYX)olFrE8bm;JIS=gv-#Rw*%B`hwVmzaC9 zmFQ$MSmfQ1tv+Z?vID3orKq;AC~Q=?()meAYVjm;xcZOG{Wwcv_UI_DB&DgW&9ZWO z8T*aeu&QW9%2U4(t*~K%E1xj=@Q>65=&&`wf_}yb@UHZR{mnW9mVD0_!BmBf(pCbY zwL+gPSyal1O;6zxg9Y_NMkv-J%X6J6A?|ZFXFyBCs<;)|JFC{f!=pMQzA=S;q&N#( z2up(n%bvbCIgPXM_q-zmU+7;GrYctoap5kj(P$5f7~(3{2F2hTvyg?SW1*$LSrb?~ zg-XQ#DGnRet^itS&!T+{34dT9;?TyTiGih9^bMPO%nOMGy8adp*-Z*li7Sl<*DOQ6 zNFq3wL2D0{BD;lH7$PNa(f)$Blbf>3Z+qp_-&{YIg{i)k3>*cy;0;2~4F}8(I0|EH zpjU8>Xim1acV3dG&9dLZI3x18H7lQ9tN8QM0S8NtG+Sc$O6*oAd7pr;9Q zZF+?yWx^ewsJZ3gmhkEIMPT z9#FgT(WaEmKx>aNFpIPf_GNx)t{Jaj=DjvNI9yf~HmX|b zlEmRWBywZmLLp*_g`w5w9Yp7i)B|S>%WKNBnc0&<0B2Q&jVe~k2;6~YXp2+woVFaI zakLK15;chsKzdI1wvemj(L6O4%EMVp!m5@PkyzcLf*JVHoKu2(VE_=of)uc8lUZ;^ z;+wz9@+Saw(5pDCidZSl$B+2%grF_*(Xb=pb41P*#DJ>vYaEI6)$-ri&aVukd{qgR z-GEiXrQlErg>{s?3X4<;ME10wOn6HdPp_o^m4&Hhm82JC#g-$OeO4$G9))8>khwSu zd!KwIj4Xe0k{f;2FtL%tPNf=gt?w8#2q}g8U|9A%Rl;JwlTbXk{o~K%)bE=9U4S}V zQyJFdGp1YSiWni30hy)zg}5z{EMY;#W72Z+J~1(4RbzSKi^2}o5jF?kD8>s<@`{6& zz?$eg&LYze$4URLO>K_)N=}WGhV80ly8W1rKxh)AT;Q*e`xCz&Ls z=KV@r+0;@%TUE;}iN8-E31lc5v5=92w$IAYmR)Q@TP@g%kmzqel!vWqFo0)Ts3-Qc z$Y+2Ji6xQ!LGeGx0XoOIlE1iLvNq(>wH$+)6t>84=v=2ta;$JZn{{%oQzwmBq)syJ z?VM0_nwl}#!s_A&Y&$`gsCWXpke!Hif+EKAQ{H3;vUoXNv{g{mutZH4 zVh9}9H8unV5U}q^s)U6Y0(1$5|4uXao>pxg-Rb&$NyB2*6;f2%0iGc;?l>z;nv0@y z7!I>=5>vc1dus25!O>NxT+;vY=RCCyi&XQtzrso&W8QV-P~q^vNkzw$!NNI+t&HMCN_}?8HP>~v7lLG7HI~`i(tQM%Ql&^_!rV6A)|67|36L(?h1Cp3-%-Y zU;3}~ckmtd4flSGH{4rzwtLQWKja>PD)HI4{r^Vi`TE*i2cQxu37;3WXcPPw6fw3`HP^5Hvan8nIyc(JgR!i+?!g$VrehzhVMQ zT3HzIQ^OoEfB?7D68TOBpl!$Y$!WA&rycG&7_$g-{&PI!T z&Pi60=1{ybQmL>x;3$(;)pqtUaaOPB2*F=>`|4-NSp6D@bD|2`9fiZJGH_fH)H`@UXn97YLv?=EYf`BYtA!ckXQYH=)xpd zS=fM~(!DHu@S`De$Yo-B;NMeRyDQ06;e)Zn^ zumL)q=d?_fmKe8!Xd8yX4jCF}^+^XS<}6=Zaa%K$!O#^zG~D-VRoK9vQeK;u3Ht;B z4AU8w0w#-gPn-p36V`P4L~~E0%*n4W*Gv#v#cAU1$=!g(4yflXENu~+;3^3%dT00IWYsf8g^J}H0VFPJOMtlXjfdt6e z26LKR^2RI(33h9C=g*Uyv+1WjFzti~&MyxW#FV6(!q~r{K|Z(1Y2$S{db<(rRA^~R zY_#8)>_{f`BbCZj_c6OE!5Y>#dft}g+S#iNi95oBE)cd zX&7<pdDb+#ZNq9QFH}=e8FZ7ZO{;&&HC`!s>^xM5-ZUq zr=|t?1dofu?aU&C64gv%31?1k<`T|*6;|bQe^rLhQk^25Y@zofSV$;*3zdh%N*y8K zLX^lHhsQsn)Wn3=EhGE2p(5N>$!DokyF&Uw;RR!qD4k4aJUS5;S(P|>(>^oNPrLR5 z=w*_s@R`yN{0YlVXq0UtIAydCD8SQ*Mf(qGFjHIeGB#WCk+BPUb2&j}xJ#qC`+ysU ztjE)NN5!95*n_b9a)v!>VobH)vQ-`mpCKJl&fh|4MLs_$@WWR5(aYDt!k}P8DtiP8fgY+E;C~xue5Bfd+elz z#o^AXCs-6#9Id3^(K5fiLYX{TC}CmpD9i}Asp6$X7R9W?KhsmN<>AxRj3FGy;1yO1 z0$QWUtFplm<3#`hdkGp5FYa8Qq|ROQ7=v-@!>6h-z+blN@`DitViYOOmKp4xTy0BABg`yO$&yDd4UH53r${UWm@8)>}drh0xIKSWc z|E~%YxRlaZsFW$fKp@MU`XvJvnp-x~(QGih&rB_Rs;RKXl~>+GnUlJ(LYE><>oA^m zX7m~90{n?5G)S$(wuF!rXjep=by%%!IlM*DmRBc-73S1d45Q`%#s@u%EEWT3$dy)l z*2r~H^etE0Jvdq0X?=7Qa$C{zumYbV8R)Q3*|K<<5JH?0=tfx>?0AxShTsbFO+Ay` z$7{Q{92IzST_|kePNDU*r_c-A{Zc4MJ|Jm}#w;?QwQXj$l%K9Wf3a04Ot4ehQV35D zL#wRe$Ps`~i#6>jZ*rYZ~sDjP85-u#Sqv6DFUOle1w z!8nTpp)`3py>^=FYU)M-I)#BpWz!QFB+}&23dlvVF^lunD6?c1X$h=c{=#RHm4v}N z;OjP6#)cIX6@^p|E~CRd!5@GD!A%E0{&jI!vH~^C?!7yv0yIieF7D4412!!kf7FNKY6BvUapR zuvZNhDLf9?WMrLob5qIF^rg6{ch5(qVE|B#0AwXvL96(^*zPzwf^VB$lcX3hQBOi%MikyK473lsK~mLA!a5%~S&gmDcjb{=#I z4as0pA`dB-^b;o}E4t&>@T1{XVFQLrRY9+FlUN-xM%X9fvw%-hu}`o%S-HLHObOV6 z$2|mRDh&gNs^oxcC(sRM2}^`%5UdY52^0%~2XHfke$aLmDCxDm}+hPGjt31f744hvj$m=LJc5#@5;lb)~|Xcpm8WK&3mK?a$H zZz$00JZ=3WrY7#HsUQB;aP~E&VFPYXpo5$75j18HhGvQD_YasDYm3!VOCT zctY#6()zFgI)%B&2nQ${KxHHf2>V)Xe`Zlehw3!auWzPBRlNei$k45!Fi@y`4$zZu z4jvdpoNPmE92FQ~5vPCz#XDhYAJ_UAwV5F1J?8k$cpkmHGz=&z_e2q!9q0+egMn={ z%hs^4a5MnSgIUciemS`*TsD>?X_a9@qLN!qK>J&z592;+%)Mfaz`i?xAsCu;{Wrb0raE?9SXwYZOrF)=T^;JtBWs}ODn>oRj1^&yudw> zqQGH*(+tZc){bJq`h^3Q*X4#Jw{Pt!Cp<8%WlBwW6gnZ5hsef2^a0I-pdh?^=mQ8H z38ApqF2KIZ$m*jv%T}x#!9cs(@JKZ*^vK|NULlHgBGa!Tt#KCQ54yL&J-1n1_I-zt zH&2&^FO)8kEVJ@c2GA3*7BEU+CjE6rJNQEQNq5YK0H`;L`TRzZj};P2$4`U(Ad3k7IbVzp0&AY)6Sic%pC2W z5FR9*^7}vQ&?%&m<(#xQ1VUjdMD#hPhVawUX$g@ISKQ1mbyS82#%IrA>52APG$=NR z)qxeJ77T_NTl-WUSui1DLNbi?~{R_9; z55#Tt(>-qYHg~4$bwmJKIqz^5>HpK0>#ZEOIL@`dVP9ZB*>;1ir}Z`KG^=j8(b89Y zPn*@)>;FIe|Dg!sS?S24img-;1RU`g3cM8ZR~xb5u)}%I&MSMz%nn;IeDT|NMTVC~ z0BSXizcjRpa2gzA&ND#n`Mx+|;rwNFl$|-cG|AJWkH#pT$_VhRN?q7nqYsDzpB_-6 zxMai#7YhsP1gj=Db(9&)eEeCDomBVOokbA?)C#1j<$IYUym16$2?rs5JsE${Rm_57 zf>31UcmAmC&WJK7?K@Rmfz47hnFa+gTxhlTG})Qi(CPy_1sxAo2A3&Z9=)@_k)Tfz`61) z4s)st2UHpf5i=4l^ayzqPlwklJ-zG433Xgn=U(t>(fU!Ct5nR8T}PfKEJu2Zk!%HF zVLG7#Fn!q-`x6B=XLlk`>59^b0dpnq<#+j|a8*Qul9APzg~8Cl-s!vKteu)S(8Z(E zYa<52l`>)n(IDm@5lwhwbf!w|%3wj7={BwWnn#+oB#SQKhvzCIgm)Ed-)>pUGlYYO zPCFbtFcf9d6pM2N8QE#0OdPAlw{T3oDxyHIxGBtG`34P4K)>bews4_yZSFyKr+Cyw_shkm%kFps7*}y2_O_Ryexxq|FrRTCu5{eio zSd2-F@p{4WCy9e>VR1;!aa=_}2N-60PA|8ao)&D$E}R?txHv*sSXw56aK9vDg7$>< z14|y(k5bjbf|r5alO0Tm4k{WzA=Bxl5d#1V@C08;1{DKa7$IQT)7bjT`Vba09yE4A z>o&>Bc42?M$gey?@K+iE!a&v(8G%IWu>@3D-IxVILpCA9{f}oQ3zPN zrj$Uo*e%%d(9;t9B_T6dFa#iH8QHVTk1+)P6TqDJc>33S%U>)C1N6!f8FEDAr($Jc zS~Eo0fPR1lnPSjhR=T$5#im-m@F{x#DXS_Bj4QW{Z0ftxGL{j+1K1PLBP2r*U^yyr zv|vx>XiK-4azuvI5P)TEby(qDDcYk^1P1GjKiF{a252WVVv*^DKY7|^d8Uq+H6^>L zT)Qj`pewI8u3WW@L&Mnbm>x(h>oRryi?E=eVUwj73^zL_iIJrd8$*oC~_6cz|q5utlGvbEFE@fW=dUC@;3BZ_S=iXbK@=BtsP~> z5krC~lgfqOYRtlBM`syRn~abzFXS7jYr=ETvV=Vxh5Y-l7*Mhl(N{i&FB&<4SQVDE$BiiU}j->WIpF<2b;`TT>A3&u#Uf} z3eQqaQ-GX;IFMiXvpDZz+(Erk5QkZW1!f>?dX^Ub>6jHr5Y=1y5aM9%ey$A9RAvz# z`KAStLGbmXMsh@0>`X&xWvBZFnCa-ap9r}>yf8dNEfA*bIQjjg4f*%Ogi%MZun4sQ znQ}OqviCCUo0`LOr9$ETkK7v_RtK_FiO7JfeEmD;77QeKljVB5GjT&sG*Ya}CNIN0$h_k|>5 ztyKUIESP4H1)v)WQWqyQREJUOrMJ5>T%%?S&V&)HLlFQ(iy~Tr0D&T7mZ-*z{7H2t zY_55OIWS!to~}B={Sp>J1Yv1mvvM??d^~j=G7Ej+a=e|pQXL#@Tj`4iqS?B4D_vJ3cTXC1*t(YV5|9{Xg)4MsIa7@Bm zX;<1aY){}#G>3JA^%Tni%LUqZTKNCq^P6K8MhIL>stL#UMaD!>O*^OW8FRyPLo5oH zVofJ~(Uj!Rt43b)-2=rD;A)j&X5*L}bOcO59CK&`P6tW8!J-H@L?x^8twc`8tnb+! zEQ=6;mVP#Xpr?x*l3>tpwA3KsMer(5y5=9x`N+qvISXsEs_LTFmd9}GAnu|Q`KS9DMw z27SdfFBT8xPL{T9zA0OA`L*cj`Oy^-Kx%mmxSBMIb|J)w4&n!6iZBeCHG_rl4JpRV z6PzY=REMYN!jO4mBLLOX1|rabWe-}#>LZ8)lO57bnTj|Ir#c6%QJl2p*yHa%Hv77J z`uG0(&VHeYft|$>ZndHv%nd@u(6jJ2u<@1C#4HSeDk3wpKQ-f?g*`$SH0hxsj zPx}-zgDV@j8j}1cIxpfZxQ1jTp7@!m9dv3h)_9c>0?pD6LLo;Dnw6q+U=nHjhy^b! zC<(&C!HMra(#(*mhciyBJYqm)34KJ^eFz#0F<}y6;*tQwS=g(Hv1Fb2b+WprVjGPkpue+OLgP{npu&kFU+(1i$ z-zuRLEc{WphOjvGmr-z5&M_?~u-;}Y>UrqwJ=4k}K*MrR6;_*NHhPj&FPpuHLaI7d zN(h6+$h7pVtn*bVFeNbBvGWnEQ|oT9AP1EA%^76ohU*0f2mQ2T zaNp7h;b3V&;87{dM-ZF{H(}M4h7lH@IGI_;Z^vMgLAgvCBbO;gfPiJyf-oT62gAA$ z3K`oHF=j?R3Jb;}g$1FLYnCRot_Bd?q+2W$A=E4BNUXe979L&*r;sTRMI(SLFbh^9 zZO<%ir_0P1Szqk@<&#%VEsGEWmgWkz4TLnq0>Jo0G61z!-&v$Ph##zt3z_r^~`UU+Ts8=55RD59sXjkJXreq3!KTROb-q8wpVNqrX(X{l?%wyERe-uprRF3^s zN7~B>7&&t{T87nRWLD|tl5K6U$gZbzUc16?YPQz=-hSPZ>7h`hotiE3Mae(M)Zje9 z!r(AbJ0{K|=ruxgZB0GhUR=d_j!>kn8Wn2pt>hwNzPL)AlnVLmjaX#PLeJ!B(SV6_ zf9WWo2e!2@ikzaxM9C)wWx;%LJ{K;ck?CL-R)+gu5^ic`E^EbD-LlBZs%5G`d|#FU zdR{Ve1o%0YMcKd%*F{O{u-;d#-uKLR9cm&cp%dEs00f_a1~L6KRUA-;*{`Al!h%If z0nHOKn`Km%cRUrJV)9#fn(JSi~5(1^zghjPS*fl*qYRamt{)7C{5o02) zu}5eueVs$lF7^jzoqcy|S0y%hA;=)Z@t@OqP*V$${vh3!_Q2#wEBVC$Fv}ZWXc;F5 zBSfI8jI=CRFzwJ2gE#gJOjbiKy9d3Ly;l=CF}@UfvHX7C;*>O@Xz8&m#DKE%^ghj6 zikxQ|ZBrC!8D9#!mF6AC72ljqL%{rqgdJs($){b_)RcLxcqr`k0VR^iW8kCiZ3yzbVtglA({Drdg-=Zhl8tCdEe-}u=uOUQVHGADbOM?z>uf@Xp> zb!>8B{{EjbdQANP^;$3%>>hYGFh7vuzs*0|_qA`MuN&UIpXT*@uJQDCzvG_a_TyFf zbDYmPXF0w4t$KgQSB|-k6#HKLdA3(>u;5iz9&T@=1ft ziDjuVLb~M~hZ=dajag7zC6R+(Zew#P6d}MaZ5>>#fMue+169M`D^|=16bXyt zhUxje?>2QfPumF(WATyVhymC|eYoXA<3|%Z7b}Or&C$gJ3+Y?zr%dmLMB^Cs<1^o- zW#>(Z5J;E2uwgileP|t;i$RjK-Bs%b3loPwnW%HGPAGUQ&bWp$zo7`>b1_da%4{cb z^Wsh7$CDi+mIbjOc{pic^<`-ri_92hOQzHNDQ&AFfai)iuF9+Mv*F&e5d%|_12Smxwva#wU~e?3M8-3tgl2PAP=HhMEiO0M9&=Q9Vqz9qcV6#)7Ecjr-C?G34Q(LvbgtfNpA=IK2d>M);_%1rK8R$%Q&*A3K?L^cQ z7EhdbbOXDpK-g>5jzMuo!cAB#_u09gW3z?{`bnc3#O5H%WuSI(3nDvJ zSP)6RQ2s^?w>V1x9yzVQY}P3_c?`pc#zqu=mm)XX_bdYv`h*QnI6%rKVg@A|icl1! zGhf?#>_yrCF|Ie9O*P05RYeSxE}S0AA83bntQ>>19+KCg0Yce9@F}t0=;=hg&h5rm=x`g>6XP+W3yuo9Mkr6YKqPPGwXOY{8pDWvbn0YRMHK#)o(nMUi#BS|D7ZvEE{hd{UF1LET*w@IsVQwbC+Ai? zQglNoLU>$S2tv_2W!9xu87?7Bd}9`l4Rn=lKYYv}66}?%r@MD)<;tmy0FTQ)A@Xe$ zf53{PBOKZSJD`&^&O*kJgrMieW;*H~q*%n-@(A#_JS4jOpcR_MR3Q!|5G!N?lrCi! z9pf-q(-()5wSO0^N&`4#SSSK8E)S=|snKgf(EZpL46Gp+SGhaFLJxGdo@}lP?TmrA z_jGl?(uje<#r;S|K{0kXawepRMLmn!#P)&J07t%HT4izuVCXi3CC-I6s=qrv0|wu3c*algN?aJe(O?G*L!2qt zp}AxI$w}0VuK#=L`c(%CBmL9>ko&uk2LSn@0kBKORvM?*#StRxW=88lP5I5IO=mZ- zBGMN>o=h>^8Bm6XvGD+r;s^%KHR;AU3w>eZWseV=u!?5fMTqoAiX(m0B#{eq8MjJ1 z07oA*BN;4>SkUie2Ig0HOtb`-zQsU?;z(~bJ;R)gVI$L#aXK*WAdqSl!ovDe=CNO5 z;Q6^3^zz*3P^6cdBb?;qazfVe(;=d)Sb3`%EG#*8Urzqe3R4Gp?NELgtTfV7jRRqy z7TgoX<2Z{6`+(y~*nYyoX22w6o_R}>dm(c--ybqQa^BIsK)xiKH-GI+2uMdY-*gJ zyw534Oa{BKkki380#51mgA>Z`+>iKjy!xmCw56JyT}E59@nD=`(ZonckF@b9<_D!% zliAEMEjpcm1M@4Rz}zY`QC0UV)di7&?@9J3N6Vn`>9mEEWLKw~30d^#*AeQq!_6boh|7BRQ&Z%L}IV#1a`5)4HR{4F9dmNqhlP*iXdcsLCE z!wN}pKMXg7N%Lo|F=O@4Yr#sgC`uSy@u;kpM`f0PSEj#>g?(jKG7yK|?j3gYQjNZ!YXXv80C!BLvMQTLMN(Av)oc1^HdtJS16- zS!75+1iV9=nzI470rGp%gH;g&hf9~gQvMLah2V{7HfVn3-!cnZ2cfr&>;Y{}d2rMJ z;1jqw0yHkWp+lV75%h$mfa1aKAS)fsHe!+SNOPxc-kIol94^;|)kPEx7gw-Y8O?#i z3Sz}+9XK_{E#}OEVT9OFX1ca*WwUhY^6LmrKCe2WP`F6)*|<+k9`-z+?$Fi7jTp>= zE(K%(LyAo`byO^!OU6ZAC}QApu|chL2|Vvc5^ksfLU%M~!89f$*(p{t8+6mmkD7bgTgb&hocW&|!9xp2~v!NQ=}nK-eqnY(E8!*ruTNpVCWby2|Ju%w_<2sms? z4&P9%hr|#S3x+<-!~Aw_63NC?Ro)m;U|p(N*X}bWiom9rI1EW_Cl#HdU=iHl1;r*j zx}~_E6PI^YMgY}iWdM`kN^>Wk13X|phzucwf59xIQ1GI9M-w)G@9Xh4Ys!bAh=S@O zU~VV70Skgdn_vII<~J_Yr*oW~Z_4;)Xj^K`oIYzxS^&2FYDvVv?o#M~J`YKv*klc< zl|ero*oLVHD?L4Zen!Hhd#Pg(VjyY75d!VfX+R5q6^}%=Er~UbQOT1N3nPK0i=CWP zlknxt%N%q;c(g1+KwWBm989_IjCn%-ikt~Ti^W+;F~TQ9p9N)feMK^Om04-9ZQ6fU z8X>$cg}+hhP7@>=6xtqrkWU(CK^O61mYt*Ryf!%}bwvhWMP3pypt^L^`9b;BP@Xs; zDIsN~cSxUtg?=GenWvwZa24vtQISbuP2^%_%gl0W?-;EkQ-NusD-OuQU~Nq$Q5-bP z$~wMzS1q>xji0fvjZ9N3LpKuqCGEqwz^oxd0OLZ;CpgT+f*e4oGy1&MENxx4ir!GW zy)-g4zD{-u9AHF{T5-joPt+ipMamjRVup70x5=&3+DaE zK233S^4*69i@bA)V0wk;7$yCmXz}@LIhar%saEqv$)u0u*K?AEPEV+N<%bIk-!L=s zv)*c^R<3MdKdL-ZC7mLIWaYOz@Y|u4*@z~buL{2~i*^(&kz8&214-_m<5tYPF#KUE&7P^(4G^gX;<wgzOyzMB^P}#V{avkJ+a*3HccR8D1W}xl3WBJTAtT?q~(;j`RRx zVpOalKBBM?c0*7UMrhXPu}ztqz5gMH_a@2z@6du*1iJ=a3RL_5_3woL|DJD!?=0`b z-tnG4J-a*u-S4|+xvj3KE5rG;v)-xe+w?5Q%$+y2Ng`!_+7VZ^tTkh`ij zYT#$d^S@oD82%Q(FE~KSAa2YeBi#z41hW;l#Ws^iL?)cPfeqcKY*xej!_LUbEiP|L zj%F-oU0x9ds+OH^++|CVHrQFbZ^=Z!fknGJ&cbX$SEZl#g$aZ1oY!cXUI|4FU@ckQ zN)w@Bq-Z$3Y!vq~3y}sWr_9{bk4>E=5Qx&VS;1FD30X_4LuD3=c&@NIz=}W`2z8+eRp(p`rsbxw5 z5+)Fn!4>>;lp^*_XJDKK8v@p8diF(TcIVVN(BC_LD~TGYT8dXuNS(9*N4+6Qu!-V| z0PZXKK-is`C+$yYZjJqG{jLKKzgrqLkhHigz&c8rhv~+NGPK3U4|)_9#k{dqdMEij z(!!~%%Zj6b(DDueidrrP$I(sqPe7c|o|0u@k-r0z!S`pv5uCu6DO^$&1#VVJs~kP- zuc3J;4o1mhP=*UfhBXF}RblRK)!DjK(MQ|ISKVQ4A75ewEIHPtE|MZa;P*DESUh*2 zW@gowa6;Zw86_Mo8S+T{QSk%}MC8lZsiImL(pJKPJ%?F@&}C|?e>b63E)Vjp#nn** zMoTx7luEM{KuJm|1C?P{Yy)AG&%GT358v*fdppH3Me z*tRrnaTb9XkOgQnv{|QcXm!rK`>dO*q6%nBC`9$n8!T+9;KRyhYy_5)?E>m0S01Q-{oo6(e!k#rnc1L2Sijaln?KRfk=MRfkyt zi-_hb&Vu6&$1x|x-_&*~zKN0)StU_`YkA>urdUtmiNL~3kee2QHa%1(v#?+I(u=t{ z2{rQkv+$bJyl^OboN68sAPE75Nqhf<58scNdoZ&H9L41uo!fk>)r0cccUVgKQ5%x?2`Q^GU0 z-$S?UTsNsGni6l>uDvTW0i6i3gs7kyQnr{cW}33O%kk8c_YfY&eWx%hiH(V*)fdJUu!C<17d* zRAFY@jtPad%X)Q|`{P2Y^L71l^1z7V)Phkzf@S7g2cVp?GX|OR^=saS<1yv@P}HZ! z0bidECr-tY#R*@PRC;3;#sO0?v&UFdQd)373NOZ;Tp0CYOrkLCN=$h_P}x8Ie)J?K z6i!A)*QMu8U)e;tVG28`X*e9ij(WV zEPZ1~eblX*r-;%R`IRhEw3A3N$5}9UU>K058*#Xq5v96KoCETBUa{=9ZM8UhJI--r zIl4Mdb)4d8>HgXMjW^wUhPQ*ajrTaO->ZB6_59}f-t(pBBS(tE?XcSawEtxPpZ!z& zd-m7uFW8^7KV-kheuw>L!~?FjN9`NzSJ;=^7ujdqYwT6_Qu}!ODEmVs z8TJnLHumG}e!Fh_*Y=z3d)t?YA-s(`f@f`y*^b!mwB2UgXS+s!UVlP=P`}%GvhxII z(CKno^gr|;^{@3$oSU3$oGYD6oO7LZ&MD4vXNhx+^8)7pXD?@gGt+lJ-Y>irZy4YoeUd&-AE^(~`|0QExq3JKbiKXaT5qBIbcf>~$FGj> z9A7v-biCzw+3}3yQOEs`!;V`WH#l}XwmLRD);d->mOAD+>K#)ZlN}Qrg^uBlfwrx| z--AB{zY2aFd?)y7@VVgQ!3TnO1rG#o3|@=d7{bAI!PUV_f(wE(gBJ%YgBJzI1}_W_ z4)zW92<8OO4t5T<3$_ZT1-(Ig;P1dMfo}t!2R;bAi5Sk)c*pU+z@fk`f$Iai0$T!` z0&4;*14{yP19gEZf$~5}U`*hGz<@xnKtUigaAu%W;N-vw{%-!${q6m&{Vn`Hzr**B z?^oY-^mLf%8r0ONd21;=Ip!$a#zNdgm_Z7N6T^_5SJo$@@R=r{4FxuX|tcKIwhPdyn@H z@6F!pyjOdp-VNR>yvx0dytBPE-YRdYcf5C$cc}M#?|I%l&$FJ#JV!itdT#UVL(R)J zPlM+w&t;xvp81{`o@t&6&qPm=XM|^vr;n$*C);zD=QK}S&xxK?kH=$k|K%NB+BUm&o5ku#9mB+PgU;0D6e3^FfB4J! zyO6Jid?n-yA)gBQSjZbfUKR4PkY|KEDdaIBM}*ub3<24(dxg-*H1z|5h5efpOdd=g~-74hvZ8} zq~9xFHw(E|$VMUSg=3d>h|Hi{hUvakm~({85+Z|k*UDF=kP0C(X!iv9 zDi$(Y$VeeF1uhXAR|+#-K_MRbCwW575~h5)vgIpNNQRIzg>(_pUPwD3ZJD@x36Uvx z$=bSPQC%`V*EIPLWc^$+B3Fqp#|arLM26xTAYc83^cB)u$hk}cTZOC@vRKF>Aq#}e z6(YR{s^zOx$Rr`-g-Gv#BKaC6WP}juJ&-G3BKm>O@^z|^PC_~gky#4Jlm|rI0zUaW zmk?cu#>6jD=l?^#ei0%v=>J;2J{KZ;+W)qEy)5K8Ax{Z;Ldb(c?iF%S$ZbMoYx^Ve zB|F@|PQF$MStdlL)i1lzFVpIm`Sh2|Kaq*_izxbKBKu5$**>OD_{Q;@`VuD z+O9|B>pmeOJ+52iOMa(or+i&4M1HO-#$Ud>gxoIV8X@u@__oTI`j-vzCDP-YEnjs) zrVEkf_l=RSkwOLv8GwNP`;L@5_$%drkeh{ECuEP1T|(q{rbzE8(tFAp`Mb-Rq)OAN zdGd9Z5Rr-0PUx@Jk+M+!gE>Ov=cb63N|`E5`PC`K@+H4IMT9R!WH+U|{GEtmO1gZB z)TPL}r^vedL_U3I^FQ&)Px5t@FZmCAaw7QJ3-cr)qI7&B6L76qk-3HFLPRvYfAg33 zS0P^u5rOuKKzl`?y`RY6y({D`A#Vy1>G6tqd!G>I<3dEFyrQ1GM}&Duh%B6UvwSTR zA~NA!EMFoTUQsOGQeny@dnd}*cp;;OTqvZkkX}N%36VMTo-ALjg~*oiy5!5r#Pf%c z--P@m42u7QUFRmlH0 zApifiZ?UhF_b%^6owCpe?bGypteFu6AX; zskH3ntA-;lyT2-`3T6>kbud^B!7&625wk+MT3HiC^(-eEfLPls6N9)U;OYod5qRrA zEF{&kwPwM`dcGzPW0poyMa%JfdR%Flj0czl2FW1}pelh8p+lhSP1+QJOe_ZUG`@)V{)tCBk*nKu~Ai7 zi}GN*MLk`|CFcmrQqG=&)`(z=fH3kPJyTnGEXd(MDZ|G4iQYC;(?by^D0XZ z7KdIbAe%jKLQ|pF1%*;&8b!@4i!LrgIZ=K$Kq`nyBh*gCBaK+JKalOr7?+#eQqORq zY~YTL#ZeT?vH&BxOJQRu7F7N~fRVpOdrGn3aSh}krS3F;P~y6}p13IC9A`#V6lJn1 zKX0>+M)S}m*dG`HO}nhA!NLd#*PRpWZAxtoCdA`qhw7t7xh!PXGMBr;rltfl666TA z(_|q`%`6Bp#q_fm|J+QOUG*oUk|#t}=`25pb3(iatTS*QK#+>Aq)q{l2Me9zk?Da6 zgX3#w(oLG*OpXqa`NQ2rIzK+n+fFrx`O8O_!ork7xj^OQYT+l7Rgp_q@xA>;Q54Ve zGU1K{_3jRSGvP&%FH!(0o;zX@e2=P)LrqEWyt6KUu0_%Q2~kx!%Qv|CD5j0?jVCKh>^jCnAUVje=0+^a@n_k_H&bJsMlhE7ACyIn`dXACSnf7{JK;XC z_SoulSn&wK!f^0AvhsSKjzvnQ6k!TomV3pbvC-~odFVt`Fcugjq84#>$x%)=3e+Mn zS){qNZNrpv=U1_#}A8Euw z-?)Ag*SvKlLSJ5SWA67;)WtnmE*%op$gxP^~G7j`;ojkE%Y{9I>gDjm*jA20` z4!(1GS4A__WKngY+{Z^oIPi$rZYU&DdRFr{ujGXJMVm?0`^d+uN9O$49(F?#6i~QB=nAfdsvz zJ&lI32N@1F=AXYh08a)^W{l`~WVbhVPr*;RkcyVxcbZ?tbB^DHLtx#o@ieSk`ppsFl zR>sdpTX4qU6szMp&LYDQ@TmMPCU*J7`*GNP{ajU4RlcH^u+Usq%d2QtRIRuc=!#A#g9Xumfq>h4uRha+ z(Z2lOAAl_yP#L8(nic%dc zEfebPEF1_a7Cc^E}x!ah9WrO*f>+ao@n(tc;i2kd3X_V?-X$ZJ=p+C0*HBbH% z$*iEG*m@@x+rF6vUG7TKTr1lO3-|UJp{Oc;x_z_4G59QKMS6+^Nyi2jZb)V6ac&UCY48k4H2I zjtK&Fc)#0>boV$DF7}0AkBzD#Sy(Yv%lpz3N3*bL$Xq�L18^%V7+sYge^7rmO#$ z!k0YM8*dn$UlCQsuyi@qbhM0>=Xrywq;RteKM4!_2#CX+wtY6vx7P=7MSLQc>U%T9e?gV&P=Kd6(a<)RZMMIgbm6 zDx;{DRkng|sl~*JDT1rHuuf2})Kw!Elxupfb-#(t)ZqX<;Ql~Sv`m?#eVrOP#%L57 z6;?cS-v^0=!IB^of|8ei+SF+`Y$rYPG-^z=6fM)T^mWchD+uKwgah@(hqE#a6pM@> zD7AvzrrOP~`uV}DO3o;ZPEyN_VnETwm~pC}g?kNcMHYuR7O<;1Fqx4t@7$&up7#z_ zSnsWlUL7&`$2z@cp9GkCvHxQj60 z?H^5Y15TJqnSuLiqa~^nx_DtFh69N)6f%~;2qg#$3YyYP$FFaeUMbuTV{pXhGor<6 zt>}6#I$1C-l)Le>=wu0~WF1)SpCc5LnXhe_)l3C?X%-j0g`(q?WdQ%}dR{i#?0ge8 zNgstw#DGJmm`s>k1-(qYyVI-tR0_L&IVn0$^@IyRI?0TR6$ew1O5@`!$q~ca&FP-x zz4-+kpcz>l9jjJt2((#0X%LnZR6imYXpqDm3n?sg3wI>D4p*8_3BEI2<{ zADj{_3yu$t1m3()@SI>~uuHH*ur<);o}e}GN8pFRmw^v~HGeViWZ=QTU4a9E8w1w_ zwgPE>Wng(=VPIxpT3|A8=A#2c1N{O$0@;DCK$)KuI4^w0E9^H27d_(%JP`uo{>1dsT8__O_8{hjguNl|V1d_D%Oy`X&PHe}S*R?>t|w z?`+>`!1|xy14hyNkM|evH$aKM>wVSxtoKp8ba=?S9~ki+xbyHT?<(&S?;Ie+L*9$L zMc(1w0p6b8Ja0E|XK!0?OK;HYL^Z^(o^L&$dEWE9=6TNZSnxN`{hmXf{hsSQJ3L|F z##eckc;?{d#E|DApvH%L26%dU@;u!1ect6WQ5b6hp95YY5RuHmi$uAZ(uS2tkk+qzo1f-a}? zU+1qt(tqZB&-ohOba>2pKXCN>o!2>cIK$4XoU2eNFnLkLZW>TYw9>8kH04@IK^HeXd@sSD|iVtUdyO zx!!t#o}r(Cs)<&5s_xb;j^7>Mqh{g*#~Y3p@RsBQjyq8?alK=wBZ`|PS38!WUZT!X z?I?AOcZ_rlcJx7gN~WWWql2ThBhBGK1QC;rAF%JU@3wET zZ$$OUrS=8(8TP66a(gjqPlnk0+PmAc>}T3LqVlAL-HTf*|Fr#R`wDd@Z`oe5J!N~y zcDL;Ss!n#>w%9h>uCQHdTVR`En`$e!728JHhS>TAzYBgId_VYl@cH25xLeF}e;R^- zR?F5FOLd#oVmVUCpZBNr6dGJB)SD{QHCxdPq0U!?>XQ`hC)D<*qIp8CmkYJ1Z|!gS z)_!4nUyBojrUE#EpL1Wzzd{4_AP~O2PYLxL7V6%vC=%)P_pTa6FBEF;#Pr@4orMN} zP;|Y}K($b>`g_N-@@@Y}s6~F)J!ua9zQ;dSsE=Z%R?9tJ`FHo|FUYrJs-iBT_Ma7P zDOCGa(LGG>PLuh#JFP^%efKEZUZ@VF7?$I18P;8?@}J-3l5X#ERq;Q&%ULN@pC;6C zuTa}LLbcDC-kG*lXmEqjfK11ozI^%iEEVd!S*T5x^-c@WX!sd-wq^F=7V>Kkr#>#< zsX0OebAg!c}8r20OK>FsGERJZ$olW(8Q=IuU@e0z5Y^@yT2;cjaP|qhq?HhSm2eSC{wiZ^Q zDbFdoT4>;9q5f}#y88%q{itY)Q0GLUc3IuqtPAAZ@)Ofr({59=rO?#tgnDK5Z}rIZ z-D>}U`M0FVUcDu_OujvXh3cb(I!+a8J58une&7BUGx>Xei&KQAtr40k?e9-TMikSx zKjlWD0h#Xo{@L>F`A1QUP<^3LoBXc*)(_>|vQg2?nBJV4FEr&_p#k}QH@jui-RzRz zf3rn?=S?kA`QP2--6s@>5&f>4?6pFzOPJo6COYlL)Ys%Y_@+?*J3?I|HaF@b4mUd9 z6~5&=MW152FLkfbV1rQalZwjt_j&B{?YvS^*%kYA8OJ`0h}}NRTK@YRQjZAr->az1 z`wcEx&l~i^!q?X+S}fH5tfCWyT4g*pSY&;!56bVq-Y5L)?K}D3U2i{5s7kayvO>Ud~3Hby*6csQ2+TteP;{xJj(Q%luv{PUli(nL(!{* zIy(xrKf-i(s_dKHDI(9iQ;rvYP~>@cK=$o!=XBxQA7;8MP1a*q+8Fsxtq~fO`P&tg zaqkL>yzTNWm%n#^DpZ$s*rg9q-<^dzWIyk6OpsAITL+r>hy zZbe;8ceFS|XzBo=DeZ*@q@5jp(Y8B$vh8;G_Q>D+P7&%=e3$xmh&tS1>mq+|6%$~G zwFM7jhmG0W(|#A4D%xOsiu~u>gWZJh8!6PS{v9C6BxT!So-);C*KN1=l5eZ5_I684 z?q>TSW^YSZ9?5Y3w55SsLc9SU42_#7QXfe(=DmLYfUz1=M?k^Dd2|JxJn5qKw1A8`4%`?Gyd`O3Y2crWvw;JMi|#{DJgywgyreLAYG z$DsUnyWY$3wxiPFw@2*xsM!x$|Ft$)J6iTz#%O>4|9bsT)2d_YA}hoZb-qXj7ez3j zjS!d!B~rM`U`aGIvp`$E$W$D0;hS_D?e}A1xXDTdS?K{I#A^_fX0#V#>FQNT#X^!9 zvA3)f-)SnBb7~&$sGEIHQH<`bk|d~&!#7Cg02RWwY9PMGQC49gO~e;%Waf3xXsYGv z-V9o+i_t|@6c$G2gJuG8PW)uVg^=P<83AFTVI)3~6i6>|H8XCvtSlcfqF-xc#)VZ# zRcdrUJc3#Xr^!44CPd*hVIc%dqeF1F*OSc(P)^Hl`&M{VZeSMUKinkCw9k4|j0~}w< z5(wDUh=pY#&5%3ur)Em}>Kvro?tY^%MweJg(!~zUCk=oTt`d$mQW(Mlju%najKPPR z+O`V|9-!pzj2PWvMX7fL+!1kSRf9wkfF6`y69aEgf~ZMJn%$vRTJZu&^rjn6~}PPl>+@(MjGuYF~G9K zyC5;ZSOKEV8RJYS1${bFe(l=A7~O28&?FwT&M8B}<6@2!;I0vi$~1Bu_cWEQvTdMO z7Tzt6;U+52?+)O1*#KaaoRL9JFAx|yK44{z<``pQy8O* zvP6$sI~vnP@C=wxNHWvr;u$0sVS;{n=@GU9gQlPX zc_U#(&~^@qxxvB)CvZu=wyM~KsW9|?D&6|GBIZy-1Im%gZm>i!=wT~D8!6n3!NO2! zU*|2^XhInd$?Jt@k6*2f+0hS4U4?TN|9B_{byko~5@(@jz&2sH=2u^2O1%5eM_IuQ z3qvuR+DW+CQM(iEVs8Qk24Th|QM-*;m^ymIBBQ#!8LfNiTkG+v$WNh|wNX!GHj>ui z>PbqNW9Kwt;cNvAIxAyPz)VjIqLOirS>ioeEpMV(D07@7C}=?4744#(c=5@$p* z-7OhMp%II89R8G^U<%MQZx+8CS`=NR=8T?heqQE`VRbYRXdg9a%pxJgH0Foun$oB% zhqi--rRx7DXu()8J@9Iv-2c0Ooj=p}x^F(R?YC$&X^0h ziR2_0ECOc0q{}-0wq~B+_5Fcwe56xZOjV-N6El{pdC}p9pzMc^8sH6S&tSs}3oB06 z-k|P~FXO~)S6eee%Y)lrpi(IFeNN4 zR3({^;kA#mu=ZpTP5sxD5U(0A`;(U+db2Kul2ijwJ0Cqk${4GIbq3HxK~5EX zhgElW*2HzqoWp+f_T2Bql`&PUN?>~nA!;#UxVje$hdBh^j85=43lm5%rG7V>(Ao7C zM!;9aP_?Rz5}Woij}D+9GP*G|=okWAvxQGA1ULxGl{&-JJPvz|%KZjb#Zb3u*gyQN z2?hY;kui0&XGj?1ESL_Y54t^Ms;imd%;5i?x}-GLM|DN9)%d|fkXIbt&~lAggiynu z^u;yFnRD}hV<2Bq%&2B1DDo*hO>p&PI8-31U|qt(nF`M%KmWz1RKmqi^9Cx4sq$4o z4;|V8v@BZ`8;Ib{z#Yd~Fo@s-L)7g51Ie+z6#Hd)66ehwG7owH8 z?M{WN0;e-rWaFZ*^qE^sNYzSx1wGC;Hl`|70n>J{{l>U5s{aDG4Gx}y@pF|J1rIPi z+1j=rni|4-$A^D4q?c61j8av)DgJYe0eS=gNxk=S$;Db z=F3fByRv7GVSvSvJhCqetZy76!3jM3&Yy5-vL;4_tke_yU7Z(&T1EVr?a6G~D>%u7 z1qYkrBj+AzmgjK})kObrC=@fQTM6Cxw=sz@)v@VeKl2iyKZ%Ax2+(VW&uhky>-Q(@ zrW<|bF{8Ydo_!))6Ne`ZKs+}Gla^LYSO|i_Cd6)qD$8s6R1%m=yqjs2#Z~Wten#~Bn*PpE#jh+nwY9@r3Io(5!hLxiYlx*LK3t= z4HoNSNS*gUvleLJv5;qHcWUxF_HMWyrq^5P zWigbsa>K<=OAEAui9%2p<}vJitg@OYVZk&beL8nbvdb~(E_Tn#W8GxyaGevWXx=Au z`l3A;%PP=;;z5o{QYlS_w!Q!=Eg1{&QqP6sKAiG?W$f&@CeSTEq8Er3Uo(z_0CLPG zQk;c@6QZ4&b+w7Zta2eLw3qx`8aqpM1TeEkwcz5!z%zp=;glp}!eC+jaX6%B=i+Xt zWMV_pmY`IUDcF9#4JVto4PC`HrWE zl2#A`xXsX?%Gwha8mD|;yK9fppZ_spN^1Biw&K_ss%1EpmPgPsRF%XtIA_E|u0Yqy*y(7UOeE+=2RE*y3T6uIVRH01WKZ@mR)LHTOZb?doj~oM za5_Ctaqa)d-gieyRW1FV4(D|4o`6UY1q94GIS`bnM2Rv)oxn`bAYmAS$5)^T| zdsNIhD{>7e21F19MMYFVQNSFp+4rleeVT!_zL`Gr*8Bc?ce$v03f;T+-nA=N?Qr#3 zY8jYFyGu3*WW?4hDuX%L!Pk>eux5eE_q||crtXs|RP=gfbvs!SJp?SQLzVQ z^_glW(w3+pl^A{y_XwhZ#}FGcQP@h_&8!)%6J?h3w!l-n@Op{=&(lJ&P*(8GU?sBi z*9OkXIb^bFzzaTM49E)%vhQ(M=pLBz4>k<^07 zr+-hXMujS=R>(kc+LS;g5kY!2IGa-8DQ+q#SSDEm8fH*m@(+d3KigbhttwQJ)nHQu zDVGhThnp={1KDp}C{3yedQ{wL$!zE|iT%2xs#T3DX(Q~N1i#2(w-gvTI33MpqR5Myr{~qkc6RF5fJr>ED(}LaTK%KjJ(kwHt4*j@JlOgDymh* zDpi(gJB1)<0mRC2#n>LjMPL*iAw{3HTT?8VD9ev>C2_c#%2z=f96(o({y|}6j}?G#wq=6kz>!pV^~epz2D z{CC3DtUrdURpBbu&J=k`)r_3@fj-F_Cv1wA9cgpC4Af^4Cw;wVgoyO!JCVd0SG3}W_ISzW9S2}xwy z4;YoLd(bnz#6!AWZ4}lBZ2q}R8+3zGKO$ZF*u81DD|eE#3=X|D8!lFyQnTzVs=w>`RqQgxEfWg zJn%@DgEobKB67LNh{FLZnE+oe=W2yvqVi6p2Ah0_MZ$w=$}(+Q0EB+FrLCcD}uR--J{<^j{f6T>_%F?`4o zNPA^ti42gd2-QAUyLW$sF5Zn#;Z@%68k6J*xO=MtPK7e7^!SqsaP1HAa-y|C=aM-M@x)yUK>TxL)H0^ z2X?XSBW!t&CqrfAz%}V^oIe3w0QN+7hoB{jp77g>Q)=23l~!M&C>q|r(=ZcmsD#*H z6;s|qZ4?WHtnBXNEa(E0e!w)3UR6>(L@fz!Dtl0tge5@hKpcY9nUhf1({$UFiL!#0 z*U^i0(@U!d$5-PVAU~agymTJ8jO?($0tE#%ibAnvH(Q{EMTHfJ@b)aK9;8-N0NK13 z6M+%ns^Wlf@UXV%s>V^I{9=KPUP#PgF(_%%`e{SjPplrO)&jeM+^{U`iMD9Bgm=bK zxVBJ4(=NEDL02jKJsQ~U&k9!$P^+S#|5un22Lww68V6`uMXKT`+B>kfoNNN$TX>j@mrF_%P}okKUT&Mp6ZlU(E`X~}MSFwZ7fhw%Q1V(!16?qLaggSSXSG9*$_fv}lccq+b5CT)2ti*h1 znNg%P;__yVc>aXNoy1%hoJof4q4Mg!>U=0~oU8;9rwENer-4u={Yg+_^ONB)?PE)9 zVa6e}&^Y_ovg$tZ19DEmu)=M?>zKaTJsL-WP0$@qx8%OAx{ht=!s^~?A|!BjiT{51 z$jE_JORs^JC><%%b92s5>>^z=;vTq4c9Z>omNrofZ3tbAm*Xa&K7W0ny?>WKjF;5z z@OATkfcMW_o<}@AP-{NL?QuQm>S}z5_fuNxujmENpPV;3+c+L`^tFFxA8!xZ?zZ*O z4q>?e>3@|Gl?Y3|Y#ZGo5Eys^H&=r*i%&^V%q~ck&ug)(K^OO`9vt5&iYa+zKy77|#D>%Jyh9Va zuRR{&#FI%8WW+Klz(!J%Z`y-G+WUSC&W2j9BVt zt?W?PJK{hfO%A^U^d~4Rm6-OK-&ta?0Cf}atD%4KldA~CXwh+ zpHw7!tV;B&NPaTK-;8eLw87M8Yb+7P@-d*u6(5#+=w z>(|g0VO+kmWSp@3P4$W;-y*l#A*q@+YC(Nhc1l}LWf>VkCafCW!O;%#UL=A&&El6m&nk3#N%59=Z{eexCrrld7 z&s^B6HU4)=Sp?~={KJXuZ^N&IPGM#iQOJ*ecr6t5@6B|aX+ho1_=VK9)5;=9Z#9{7 zf$Rot2&YPsWmg-8lZRfIe_@Ul<~;dpk}pd~Mv&hcKj{BrSWz;-UU1Q|^~y@%xF*@E z5M=3TE9*Oo(za-+_4D-d2$EXqR*;8!GX`gY0uMMeYVw}NWehpRp=6V%L%Vj1B@3?a zG93ErXIDljtCb>uu!Ap`?SUo@Qy%_E95U&hayP_$Leq;Z^Q zDoq-T%)6qe1qm~4)O%n2=Kg7DH4<7?U|w4<+k~CM&4X+P0n2M;-{RJ}za)w9%E7a~fTHlcjyoG#@@0VB4Rtthd!ZsMgv*4BF zT=H|#^*2%2D$JOcHM6B927LYCmjCSEbmPeCS@CzPG;jgG zKN%bblncd@cu6f3yHJ6eXvGmVC!HS(3Rho)iO2~FGq4v1CH0c;IT)hK_>QB%igD~2 zDX&^u(tu&_3_oYAWhhmA*Dgpj>b{oCy)i1GjFZ$*k_(a`3p}} zR8KvAIQH(cI~WbNAH@biLA6jM(h)1o(eBu6X(bDypW4psF)3Ua)kgB-VpW*wIHLp#^adRx zzqhBpJ)2yCMCm71POPrN=ydi7O;YXz-*2cYAYS5*Bp>4_a1>&gu0Vr$Sl()?u)L|L zdXicX5`Ase7#~awI|Z_4scAl3A}Bf{_*a{@N=#k7V$xjHz*Uc|u2ei9?^4+@1#S+p zN(vLeKsuhzPO=&{6L?#CkJIXnophQF-gSr)Nu%JU8lG|bMJS*=YAWV<~F;Zb3fsJ(0!LX=3a+hb4%R|+%w&i-Q(P) z?kmx4Zh*U&yQ{l{JIj56yB+$?HE}1qy>5r=FV`=wZ_#n?fa^ooZvAilSN%Kv2>QS6 z!%KqO_1E+l^=I|R&;f1(DhqDXSLjRhdHQtpfE%lq=%e&2^vm_j^v-%dy1<>Mx6xa` zCvmUdSa<6-bb|ZI`L*+~^AqR$&Rxzoov)x5+*8hno%c9zLx+OZ&g;<)Znkr(bAlc+ zS~|ndLg#SfLgOswAZH&V+u0o*;NErYaJ=q#$+6M#xY5+{0J^|M4WH{R*Q>4e555z8EBI>gh2S&6M}zkT?+8{0*P>hEwZXZ;Yl4-*iePbYB>E-x z3tk$$B$yXW51t!56CD#z2?m3%poXf99|K>ZXX3|!y@8#9t$~*Vn*vXwYvSGL)^H0d zHkRSlhFR#FI6hDoxGFF#Ffh^u6NykMAkp!@hgaLvg)tweNc0BHwJ^RNn-2Q7rTg z_YLy(@pbog^5vk9;@Q4dzGl8upWml@|3N3k@4ZL8hrAzo-}UbBzK%|c8@-QvAMoDk zje2kPu0$`z`Q91cD(}_aG2YSMq3EXA)7!<{-ka$?-`m!EI{GOlc|Bgc=TFbio^L!~ zprhgko_9RkJX<`^d!7#c9{Mr#Rp|53$DzHUouRFvmqVLEPlg@}-5sjIo0qH5@p55k zR%l9Se5fpRRcKgfAbMVQ^W5&a)w9NPgJ-d4j%OPBFqV6YJR>}VJ$*erJQt%AW18n2 zPis$ePa{vjW1tt~Z|)!5$ME?4R^w%3lkueSka0Jo6)h(`<7xj$$`dCbf(G+t*P?vYHAxn%1f)M^7whGyv3U;Ppqaoj(NWT)b$*^{spz6ToXXTwlu9Apr*k>=*EXfcFKwCt#0& zHwC;VfT_oDG4&WOrXIuffczcri{ZLMzIa~@*LwN7NdRw;;bK}cT>Q+h;hHLcH$lL7 z0V4$r7cf}B00B%(hO2{o&DukID7Noo&oGM$M$}{Mx1@d=S z2pA&Zashn=2+^kYkgx6nIt#d1Kqmnm86*qoC(D?1KQ1l*K??!R1T+>Pj}s?*H}WOKp7gnVeJp?vRZn_HzFrr=XR9Z@Dqoug2)QNwN4}mF@U(y@ z1w10)UIF(A*dQP#fT=(~;k9i;kJ8^6x=@IIoqX|KG4uuUHCw<;0nA?v{c8CdE1*;W zUm!!jO1?%3;4L@w{_@4gXz2aqi%-+gFOx4mO+)V_UqVuPd-)Q|(lg~t2rT6s`qEQc zFw%LwfMo&}3z#E7*5I5aUsVDo31EseoFn9GkbvF-n9d9*6PV#-0yCW1@^|S1E)sB> zfKvr9p%~7Pd<6vr1h6179KZ0FgXzU^{3u^cHim=A#&9sb7*IIrj2znqyd{7M#c*tq zuU7=TEPx5caBP&XX9V0O;7$Rz3%FUp8UcJ^4ac?eB|GR?Bws=d4%tD65QAfk{GA-Q zLypQJ+vAY!ame;K+Q`4j_sDQGlCM+&vRMwFeCY!CRvGqx_{;v6fIkHMDnNG7E_-G_ zDoDO+hW&H-Iw0U90q+agBj9ZTvaNR6R{N`hl>M~7AYZbd_NV3R4gs=*_G$ zRtmU5fZQs(Y^z;J(LPQ7PH50B_sCua)Aw|1vtG%TlPZy9Z zAW48zfKZ+7U;eWFEkLNw_N9D%B0zS@_Ktk9j6?FGd!t z1auV8K|r2>3;}5ZE);OSfO7=279g8!J4L>Pc5SlPHa{aZ*vP3s@##k?yeBjOqO66#jD}|2dBT z9Ls-R#ea_CKZo$2{rS%x{AU;bGn4;J<3G>hKU?yjP5I9z{O76sXA=J@TuwiWe}yh= zQI}2AWt((6|D98Avs0Lz^Ozvz8aqFiuLA=13;0lg5U6vneC-w>1nPWKzFrrwS-?vI zo)_@2fCmJKHso9_UpGqr|3eyVq4eMf!HGdHI`Mb+AMnrjx9~lQ_y0ceF7Y<^-0is- z)_0s$ zT4cAKE8Z43^WZXOk39!K;2II{D7-E7k9O;0C&XG#${tzV4vhdete6-<6)Zc}(eUMQ zjLcqiiZQ}Bg^^ zFpBDL5IiOymbS9DB?sT!5uBY@96?Ded-sQ9~LPoJJ> zON4n#sJgebC}NhzQs{OqCSk8M_7ILLIw0k-L{2kjdr+!zesXGJzOaEu@E*&lJIW%c zisfTMRV<}_;h0cW$uR@UiBXwlK*2qZ;65C@xih{y;r#1Kgy`+yb|{k~D2ioQBHGVY zU?Pwe+{nCP^uUx#*A*0ey$D(4HGQ^$c?xCc)3edVBO)k^<;C@JaU7SzIEYzN2!?7z z)Z&1GM-#q3_(pTOx2exJ+9`z^o@EzBPzlR}4|i#I%Fa=d8kavIYM=;QP_#f0Me-{N z2E9+GH%psODT+`XEcsv2dBcY}KvJBDK-q-1m_sRFFSe5BUB74 zfpy)s&|C-w@(`y7Nm1Ac8%NPgZa4=XkahVWOL3j~2dQRfaX|!Cusl^a<=kb@D~0eR z-b!w96GhQBx};;yuPgF0^-Ad;5$(vNmx*bBOug^K8-irVj(uUupj3Rg(GG`ED|L7 zm1xR1U?u~{KBW)wb`gq`wsHI!c{4UzxtvS>rW#34S;VY}rC^c@9U?E6qX)U z^5eIiWf4@u^1VUA=S_EkH$P|C9`xYH?BHd89`AjF#!bL zZC_(l7M7Hu4@QZJh@edru~UegTUho!TsHcsIs3KaFAvx?SofV^UiUi7aO|}XYcLt(;&eLlCNi=hN{x2LoogASG zS~|BGNRy$=La(Dl4PV%pa)VV2SgiqE>q7$C(d^+4M$q3NvU!>f=NNj z@>Cl*5HgHN&c;!EROuPMCRX@Gx4}p)J36N@(o!i6Q0aumNK8SuLSiQFs!|#l1)fM5 zVOb@2C8p8!=t3>p29J)Mp-3AiWGAIT?j;vbBam*kgy5@6%n!Tf;lurYWldY{ewca(%+x6g%?~}hMiJXS9vc-|+YNzP2!>#5|rsQ12vME!xHcD)i{5DGxRNt=J#XXJ6 zBF$teDC4r9A_GE9b1)m5rTW$4@>6*&NEc>9TDb8EH@zMOewLe)<%UL@D$?!dvc1I{ zL=_W|-JEP`KA_1ca8T+6liBqCx^%a;hrx?q99t4;q7HjFDxFjo5fmzH6Syu0C$1PvVpqKL#9ZmA&DP<;(f?%Oq4gCHqODssR z1@E3gxxJGA|2Vz>mmmBTulI!l&jbqm-}-OxpW%DWH`x2NccJ$*&*Po~_uuX{?kv|f z*Hy-^#tlX*{SAG*?s4Ab?Cto*ajoM#`y2LAw(o7rZB4bu|8Fh*I{y382)jtG*s43I_{mn3mb$J_{R@PpxY9HLi>tFOzlJNd<#|lcj8uw=pHB4^iiA&W>S$8%KdRLNsQ0`z6M!y4*xwhej0< zRVzzw2hQ+^GL~@xcojy&E#TG|Fw&r1fTAfQ2g}ly9kyh9%o!_1ff1^l6$Qd!-gX%n z@`G}gcvodEFfM+%x$n7_rmDPz8hW=ail|yz2iW|03=1hj>563KAh()VWuiz3lN{D= z9h=zJEpE}}@8_nrD2SM)w3Mv;5GKJsL7#vqL2e@HB(+hnqv)y5(BTu9*GXzhpU2Vu zxnKX%h*>#{3WXN(>%juK0tV&R+z}POUQn#AX5>s7X~ED~Ix!2vcFc$fifBy_!~2*A zRx(^puqvR7qY#2Pd>jRmru6Q%^=4mPRE-!@8Egd5l(ZBZPB)e!3R z>2+;>Sf_-_x55##1{ReJGvxQt_SeosB+Y|S&x zActOyOAG?SeG-{RsxBAxkxwl9%2G>HEgtqo8T~IprLe@As6qc>tO~!MRcH_<%wCl$ zC@3k_e7Q?q+G}!9kXQWXIT*Rw99>XgD)3=soqCa_-8*wDC5uN!M$A%J>VPGQ zmZU(#;RY`V9DAgYVnYQ5y8zNF@3QUnc`B>+-1SHQyIYk1>6*#)KA5)3u0FF-nYU#mZqCgpB_hw|> zRoAU4`-Zf{#|k1;=1Pm8k?-Y2fEm)clS5KmRBVeNcrJPERG2Idqg~NcGJtM2jUQ63E{Zq84N$y(`p*mMu#7N}OlfMYtF7g70 zOfK&eQN(BR^`{fHJGwox=l1dWAA}=jkt-MsDOI2xZdZgKPx-B6JeerEiV)~&qsVJq zIg1MF$4#lKC@BRdTsnD5(d5nxW-w)qiS&~7z>d;w zKVV#(Ep9zKbm1-L>ILBp8@kGjBY+L(oV9jX$oUEt zC2MfW3AeHy^K;TR15`bARHU0ak$%u7Cdt0hedp3`uwF%Kq+@W_HpEVL)hYFQ1f~yV z@ijEkRjmX4?WXYP7sr&Rp?T9 z1v`!%0`XFK4er@mDBMQ!n51RxYoJ5x>Syul((~!VBAsPY#C4sv8+isYwa^I%90e3b zEPz=Wx%i$PGtTsVn-V z=^A<2!7?!wcpWY*XoGedCSYf zTqo-2DW(RYFcD~nRI0gWS(vBy(OdDq>dL5D1B}2h-$fYMG@=p1tfn0IW)7)ibRFtY@N&6ztcvoH@6eFqt<3NIG z^#Uc6(s@ohR`11MGL4@KEQ^{Iu~hW+xw(C$M)~rbpRKqf@p^jL4ijRfT zLEr?yje)!({S-WyQKV#p=+m^Nc@3iPWt+IiLt%spWl4iTOBPBD0Yi}3gft#f@nu_8JdL1Cp7uYf|QMXD23Q|GVe z2PDcOW`!(lVBS5b_K+fwgX|(_hlh#6Zh|J#^GklTKmb!;#bX%vN5T=aJ{IvRZn%Iw zW2Zmy8)*2-Toe?krm($ww>g0T>e0F?+p-5?RK%>6C0RK^wh6=H{$p5T*R@cjQessZ z<16dEGS}ULGRg4c;RqGX(%HjO@n9&hHjo!|5-23_o+@s~C@hWQzghi9TM|Wc{=}OM z{R_eovs@No5*txAZn-q1fQW<6MV`7iiYv4;GJHi=w(PP`*!rrBP`xa18PrVLl6X-R z(M9ZnqdqEjrBI+W6wGE8H0qeuqg^wO`~VTmfH+`lIJkt7MKoDPG6cIf51oG0-v<-VL+}MFvQX*GbafAwKi71fBrM)a`5{XU14is1$ zMVAOSw)uFhDUlW_1z~!wZB=o^ter)?QCoo#apsr=Y>sjZpr6!6K`$pqc#1p0N&1g% z>iYHY=VcKT&+$@z)i}(4f_UIkcHwpHHl$wc-a8)aWI+GEea6#1D8GATreiK%p=jgmwM} z|FshGpS&fAC8QV&t$hv!AzpRAs!FSnwWDO#bE>XUi1o`a8c@E)|1aJh#Y7$ zyC^bF%>+rR-H%~${-WQ2oMD=&Xau9kumOin>-a-rqI6~toXV{Ec8F@3$Xl=(U&rPk3^iKD=z$#qdxme^L!`L~iDvnz^}$LG-P zlqd)ShV0}-4E$X7DF})W9^{ajR`^I_b1Z(J4ZosDI6jBLO*t?i@kUl@RY1^0VFJ20 zInC})Y>t9#bZoxlrIJXQnu9t+ACh&zyyp&ibm^604iwa8_9v$0cif(+@-%iPEV!-T zh9jji3)za?u@n3qdM0c|c#?3q#330)#s?+3BCMcoIADRYRy4dvIP)Ak9ap1b>puJC zwjXS_*e=jES~l}7{|8l3Gldo@adxT&V7j5CNf0LP99;=Pfn0F_Fg0_-?gpA<(<-_3 zY&c5Ew4wm&ZcG3og06(4fC30DOgNT8LHmPb%nMlyosTSMae{b`I(5Vk_(>r!NBAOO zrbDHGW=5xOqQERElPPoDd3E`%u30(Tx#E=9MnsW3t75=>&CQT3*?e&&LIrNI01{on zx9ek6+|j!G*n%h$W?9^#LW8|x_>Hh3pl6XUT~uU7k)A~^6`(F(T-VQ@vW_10IXpCK z=E!10DC7i^M%vjES;$Xs&;Yw zOyNr-1}E+{Ic5l3oGI>hj&@(Q716q+IX1o~wJ2()!lExOKZFAM#O^bFlEhY=Nl@r4 z4ih5RJ2Nrv*R?Y-@kwRTfF!=$BKV2Dw`v$C)7ewf>o|-g$4fC+xz6J?|pHkDoVMpbUtW!NR}d-q;$`r zB9Nmej>0}duu_yxTi>>U7k&8)ZbUfm2}jM;SJJm0!4!~Em>4%5ZUr2TQh*snen9dp zoO$#Fa;rxRn(%8m-r?C=97X1`THrEi+Tw{P3|-Gw&3>OnRYBCud!wmJJJvUy6#Oz_>`&$v;{btF6eVKH+UjqLj``Hx3$<93P1^aSURV z+9Uc$>k-kF_;Ky8QgP-m0|lK;D~|LLUFd3=a`jwl1Pyn_;omH=C;B; z)C8mAl3^n38*Y?x51A;4aPsQsPjgyw1T5)|2eqC#r6h{vR;E|dafv-)0q_J!w1Av6 zwNcnP-0~b-*F<&PQI+XRUI2cf287D23BR6V<77L>QJ580ZQd31qIx1; zfT|qj9X1nPiHvt*M9=}(@8Nd9gTXO*L9w$FR$yj+R=>K)xaG@q!>l%6b&HS57qNga*MLu>T83lX?ERWSd{>gICP;zjm zF;}{F&!dTM|G+mWrLkv0v{C%`J8Vu&!-q_S%es_$M3@wRpl}Z%hB5{v*Y*1+E#N?V zQ8ZPlfT#t$2?OI0p<7BzS29#;qa>D+<4H7uIfO*R1y9AwqA6+)T<<6b#2JDcam0k0 z-74pfpuoG~Cr@v*`vi_!k5p*IIdvID(PXtv1qhQ|h?yWjk=70W8^**xB-;c9=}jCL zH*ab8+hHS?&Bsf;78DAFoaBRv!UWK4)AO~p57nnN&zny# zy%}ZE5QZnS0@}uL9~D->@iFu9MUWUTip+}a&_XMFHR46E4e#0EXi)7{3A_}P!HQEs zdqD2oQvOi&M5ydd5_%k&}_) zX{-T;6Y($RjAVdA9h?7N}vwP5eklRg}_T#aJ-Y zz_CT3Z+HO6lI3|wkK>>|C7fqy>n?8~H7t6HLf+ZMQOb}NZ@HaUhnoso$E@JW6n4a{ zCW>r#Fo^U)eA&1KY zB!wGDmmz@!6^$reShzw#{f4fQ-+77!TXI|qIT5l8qDYZddcK_#8hIk3L(;p2kDDm6 zzd)KKP1hoON{p>i%_UKl42$GmhmF!p*i%Hlct~ivcePP`yR$ORxF8o>yeZDAIqCaXM1LE*+>Vt5{In`Ox=S}-~SmGiB#sF?*zHFGh{g{y!q z13#qDg3J|1fqwy&(enWdBrt2T6e>hjQY=62uon~IyrG6elc7)?sGt^#XbscnRSvYY zrfYM#Ja0tQ%!x&lbb20_U6I7Fpm>63k*Y2zn3*&UR&~w3`bePHYD!@|FfvNnu!_LA zHw4x}Vui#qG(yuY#3=X=NTJR@zut}`CHd5);km*nrNEL_i}+qpb_72an}mChpH3GN z^9hR6Z`1SirxMf4yFDl`OGhaUmKH$C3%6rH95)09)MQ*d@Dk!$aTEv+sLazE_IU8H znJ~((npGG@f-E}|hPx(Xbg{{>V-UGuG^F{(QCKH*vaGbu35xnGs>A*3=ZdJA4huhm znIFM9OgIB<9y=dF$3U^mlN@%L+RBZV=e~P2Xk7X|m-a?`Jc#Z~;uLpx- zDiK@I)qtXTNFZv<+aDL;ZUDjaRECPFGaZx*Yl4rO zt-L_9GE)`a7-+lvg7Rpd%min&(@tC%cM;bF{DAJJkh7p*GLc(`Or|@EtW33X)@pjb zc~mr4U7UU{j%4!!B!>yRr8-g3#er1_xrUR=($+pzZ&Q1mmL`2 z-q>^Aq-eT24ura{mc4=xflDpPIwAIjy%H1-92_ir+>;G7UKcOTAy=~4|MRs_ER+>| zGdMKx1K$7Z;Qzos*YEY+?mOT6vbWgtH~OJ>b02ljceizIat$&5G}fX1f1`dWy6&%W zUg+53sIdQIueM)k+iJT~`$e07lKo%DXH}H)X-R~`e`8mB$&MoMY|y%?CJ2fXQ`53~ zoZG-f&v}PZ&&HQURfa7EFKvHdTOocBw1Vynjs*5iVlR$@t^!t(SMZ_*iL~%2YBD2x z!%>xEi!>3ty&pyerEsPO?l2`ZsRfJjh|n0toV7(0EwJ+i9p>)ew7!2yR3+KMTcq3O zVkU40Nqr?tLCh=@MY0b}K22NGB|(O9J z6nP2AQ>`u8U`aKZz40YH;j^|VO4+tV8C3P%OHKrHL7>9jfuB!%heHw+WCm?#QE_6I zd&V-3VwXqFJX?AtXsqlSLQb5vMU|7v$HpjHBB>??lPoP~;fH+D3ZiDBEo^t&ModMf z31@V$KFq3C90g7bRX+XP8&6P9PO{yL&ql0z=|AOBGoP09MTa>uc?EIKP~ymvuZn#G)wjXj%Im>FSSBanx7>awRYkbU6}Af`Vt9WqW#7V@IMU zW9zN7oJc{`OsGW;iJfM_Z$}&rLrPvCu|X3>>K14$ZP2B4ZPe5?+^44?iu_roC^~`^ z3ZwH zWjv6X^Miyl$B;G%uY$yC>htHZ6Em3DN!)1FN!$O&Z zk&g? z+Kr#B%)hrFTBufsOUZ4{ge}73z%t8heF{#=P6)`-w7fQd)^#Z-9^yz~Ve~3>R?r?h z;FsfSGlhxNQZYF~NfQQ2k{ORq4~dwzmrX&~uSug3(JPN%&3%{xza12T&Ed$ASt`#c zQZOjuFm-Z$SF=|TuGz;Q9vU634%InE)Rt45zjhdz{ZzzoabF;e=)S~J;ENbJyZg!n?S%3B+19I!j#QIO1!Gyr zdqZ3uIvmV?Fh!EB9E~9qJAX4WI<;;PDUCgac-dR!(GhA&>K;VZdC)}ZA36^5XA}9F zC>#g;+iAHI##$1G*KOy3Z&`G>Ivd={(=jZI4f32J_Kz#57+N+L9<$3fEobOvD_3%U z@dwbiuPBTTQxn03PVFi{VRS)Q6NWp3h`>aVhYOl!PPg|HBjP!ia7w#3wAHr8(NPO+ z4s8rQ7P>EVduTmgMOzx0ADSMj43&op@g~}UP|r~3P+ll4batp^s7WX(#b+ua^qA6yk&8k`@Tj(6?KgN4Ci!2!XZ!OnQqE-iR=uw}4G zFe&K9n|8kiz7HG;90+_6*o7DEHU~Ba9t+$TxE=n7Re_~}`GM(y%0PLb5U<${2=oke z4&(*W0%zkbyC#98fIFc1fAfEjm+TI}SFy{#)xX)l5%1XD=fB;*-oMJf6z>;I_gDJM z{e}Kv{sI1;c*8EwpXNW?-_qa2pM)3eG~aK&?|nyn2Yesk{kpA=zP`=AjlRcx_rdG2 z9+?kIee-?OeU-j)yk0lVH^A4^*V&inOT*iBEqzUVNj|qv^ZtgH>yCI2ct7y&@@|Ev zyny=S3krl-m?)>Dj^=LUItd%Aksdon!d;@!Ds zo)nMQV|V}I{?UEReaOAfz1zLb{fc`N-er8iy}@0LPL0>Q7vfdMD)(4-v3rDjkh?eD zWNh!waG&dL?QZ5y!HbM`*B`DQUB_I9T>D(R(bw@6_;#LfJ>c4amk!ssu6HeT&2&|{ z#^RmB5w1b5-mb2$_O1-qxvti(X08;M*JU^UFn%ohUcP(KxYLLj zYtbQc5#GI96X~ z>rd$q>38W-y#KgdU#!p8r|9GKF?juPh~8K4u6NY4^z-rd<7s*$-LE^He>s14e(mV) z{M@>~M3Jk0h z=#jtw%5|4~J7o`!1-|3&V}Tn5`nn6$pB3obq~PNWkER?Dn0&Utq>w=We-w-e^i2`y zc}bwVM4%&2!4!eE5e$zcUm!3b>pl{=NWMK=1?qzZIwvyxGG(a1AaePze_#486X=n3 zf9d*Nz8&gZ?dQt3?I(s`r1n=(_Th^Zh&YV%Md-f*gFOU#=LmFvB+zlHKwDFWhg1Gl zQ1EMf&Om= z`s8PS?rSCAUiq1ydxU&HcRwL`*QEmWCITrL7|Z?KwppO|3&YQnTMJB@CNL;J`!oON z^6jq>=#}MurpvK?=8$Xi8B9wW_fRU_>;wa{eTV#o^6jY>=$7L+r0)Eyg+RU!;g|*5g52xpkHp!N51>z z+bd-9k>^4A_FN)RKcC^gklfOJ0U_^wUg28%Jn8cH4k7n__Mhb2ehCg<~^&CP%RL9(2~2mV{-+jp%%?;!<+3xD7q zAm6&I_XGQXYB56kMpF(Dz=yT+6+_ zHG+44tl*sjjphQKLg#xOLZ^G}a&CL==kedam-M}YD;3NV7|apq7rJ`Svsb>|_bIqa zAbLg7^?R=;f4-Y?jlksZ1SZKgy&G&S-(LCI?|S7Lz3bjBc$;vMcWqnbTl-BxJf?-= z-xa2{Cq-7fCs|gxC+The*Y*U3dF=5EGuz{RSn!?|0^M>7dz?a*d+hHqes}U!3d(Np zPU-c+@N2q6)`+fO#T`tfND%xepcJH#MF@C52T!HS# z1wz9ks^4kk3v>nq+U57|v_3+ymNm)W6zA$Wa;KwE!?+mo*r zn6yElSI&96XS#g5Ul-_KEX zBlxY!GT+va?90|bg#Ui4Tjt;Do~piuPPQ6b<=c>ZwACip^o`UWjDI7wslcF|(;LBy z)pu)wcrBgi=M7!1^BcNI+c#`~GXC{sIgi(aCGzc`FHrwdpyM2Y+CGM_rO0|;OP1gH zT41T*1M=^^=Iblp9-))h+zaGeSO2|3zF$qbfbp*;9S|6LTcEG2K!h6UcW+4@Eifqi zwZ$WJxJAES@J>0GE!uwmekHY&Ku@VaZ56|pQx*vfX#(BC#$I-d6n@zqQ+WCBUUqem zZ^tx-o0G!=lV%GH4GHhbF1zvq5OLD}EUhV1udeG>kC zw{5fY5{ds$r}zIZ3hoV71zmwy;2gXPe+BI0s4w69o_C^G_e4CI?l;{N-G(dX>Slas zR2sTot>-vDbXLNGTIc9w-(w$Z*KGILx@h|~No67v>3{rRQ4AHfoOp(2pHw!=ehl&Q zA(@2}8yQ9Y6HqUL*m9mWe{EvEjXvWkD)(q{?09XhQ&XwB7?hK;k>7GW`c6HZ;LfBR zi)pR|U0qB1Tt<&k6~|Cpt3p-wrshJB=15sU$D`D6cEJ;c&=pYWU1}1I^>p|bq441? z3Sv}ci)bITWP<_g1 zRR4|*Lb#i~zLcCr&MXs!h(7$ZO=oy0-i~3#kxulenBJ97D}5#bc19K!p<6DE6FU>k(VvCw~M5 zKN_bfWMpZ#yl2S~I3R<%ybc^1Lk+GOK@7;TAXbGXajq$clyEG9LX;ob18E%`C+yux zA^rRdsBHMLk{FfXQtktFDi&Wa<&rVwqHvV+5;!(V`LyuR;-AdkJrbp=`Sy|2oP1IY zCAc&TdHmHQBXF5gYCb2KaUB!3icu6d!S1DND=uk}n|5VY52RB)Ru-dTTv{38)*);e zHi!FKP`piD$vBE#-RYSjOLc+Gh3AZjp&FMbLgw@yA!0rV5HU576%VwW4HiZzGU>tR z>hsPwZAqD0uNTFr0GCJ%tsE%X2D_BU4LlL{!QjdR43rd|Aq^`{VyMbh=WpAEVI?6-`fXG0Z*3I9$T*hVwm(?$ zou{QybiXPbLy<0DArPAO7ly^@;|g&}E@j0K0hlO6Q25xodQZ5V^)a@A+(Ne?cDzv6 zX}?4kAT*6TPpMY5P~e|n4%t~1BYZiOHKm1ZI;V zpg3F(xtWz&|-XZC3W`VovIM zwjgF!>mqTQbAg$xNH*fK1Lv&^%Vf+j@w)hj(zgN&<+%|qxk zZO$AE$%8Xrqli}XaO`-su5NoqjvwL?@n&3g%D1b9f&}2|fWAMSuvhg^@ceb{*H0UA zb~uJ=UA~lPvTVbsIB1AWE+nL@A(3B&Lb3|ZDQ~pL()I850XH2jiJ9fPs0OoN&I_Ph z$vLi=N)=GVQ51L1b)J)`V9RfdjT`b?QH<(yC3VoDeT!KT(8PiuD{(&Y+7pGsY9M&B zdtQ|od!BHBw2Ws8V^p3C5kNLWPfUR;!v%9xnFVgF)ES_lW`GYsD&$lq*plsZg{Uvl76|q+eOM<9K|_MM9kURO|u%LEX{k^fz~C(;h0k`kL)%gGRcFJ zd^8+6NtQSYS^?-dnY zn$q0y0aBXR7sc#qDqI{hqlH^}F;PQ=VJeUwqj(oFdw!q9E@$~9&YBt;vtbUpoJd$3 zZvJ$sJu{ms@oEGGd6u5%ytOWgoY@=I@>iyp#x%t#(e>uxlQ&TMRE^mG zqqWezp$>@U&km*r9td>tZ}*3NfBV+@&hftPE%E&0x!cph{hqtR^{?x0S0Ce$G1E9z ze^$TT`Gs?d^BhD>``ZuOXWARv?y%)+yS35E?5~QMMY$+w;O70<8<;I@37e}hQDnnl z$FfGGH85jpfd{#xNk5N?p+J|@@rqo;#V|g%$|M#>f~yt^5|eW1vacC!;e>ikFnw}A7+WhSA>mIWmQ|7+oZhQ=rr_MEDiS*J@+%+k#izQf&G zs2-1WGeJSK0UyGGGA5Taa1ZBp*@{-fn~GvRYu!3HffvAl${4pP zu{(NAI^WUqm|3n%_RT985GRFukA*=(Kx>nvQbJ*2WO_nKj2>!9V_R?s7l(&qDA+ai z8&c3QWCF(%V6>nLR5?%Xo7uDhz>UD`KP=?{S1~YLc6;&Tmx)JtC zP^f?g4=2*YKiWOb5_CB&c=nM-GY|Hxic#GzO#(NnGMJ!kVnp%`h_yypXY9qHL|{;a z78%_@l9<1Sl#$;?#8A$wvRr4HTS(Fm8Wof@;zzuzgu*ojA*MH-Q*RPy+s(`>M#oek zFWzZ(*zUzxd~1a})2&qo2kcuEb zK8_;g2iRtnR>WvK$;mE@b(T#+0b<-OAi2y`v7#s%Iy^KX$>U4c?(1M>qJcl`f_Xb= zbPRR7?7E>&_j_f-NE0B%2kgdtD-S5nIwucDp0;3By&HA|yulq%e#Vdo}x|L5Ne_eS1-OJk_kWrMBARUr!i8^gxnp2Ox)`ZJ>_ zyAdajN}^sC7--+S`5le2m|3d}QK}urOu{Ysbs@4ZiI)iq_5o(bR^^`ew1tU^?}le< zZHIzb2RRe&_4l+a2{tElFKC{ohf`2oVV{xFW&a7Mf0AuG?$K-TKYKyUtks3pY`X{} zV+^qbapSQws-S>TL|E{ar;U52u8|inr6PS#ICi`?*KX^Jk;NY81YBZJv^nDWf#U=+ z44P7Izk98y>QhFccYTWq6*03m7x|gmT^JTD3jc7fIo4qIum*~A%|T^wHb!2w#D!PA z*_7&9OJZhm?noC`jTD{X2#hs)VgT}sEZW*A zQVB5s`7hLU{F#m6Bis35am=jFMV-y97zHdGq{XLCg{;a7q*c(($Eq?i`}-5!r_LXf z6|=oCrmA#pNX0fYbHQ%87Z`pk_W)sCR-s_UWK|`^OD_4F5^_)NUl>D?F7uKC*BP=5 z+-UgWDH~pNKCFyU_=Z-Vov+=H;QO%sUgd|A!!at(700!~!6f`-q{rhBxGMGdArJ+? zVuBFNuvzL?8EQiva~=xE(iCr}Hw&&a&z8gpS^cIrmL?a28g&NQ+U;vBaI6KhNcq1s z9J?sKCObd8hGT*Gj5X02KxAO@IEuId_A>c$E4P18JJ{)Ke=m+*h?z)RM#r?)JgQWc z(9OpoDMcM~LBwE*Fgi1upO+}16+FO`RK+e(%b}j&iYKzFg;_{R5px;l0P$onU@-f% z=@&FmFPeECREuw)DvO=3CZ$?pDj@}DK~W_35Z923+-sxYb|g)<|GQQWEBjPXd3jmv zy!b?hjoRCQFLNO}seL#ZTCjGjys`D6g<)C=~pJE zD_`<6UU>af8#yd?w#-5Kg*I{p$`-+k2E`w|l8#4EtdWyBkgwg+zAj(D{8rRHXJCO)l~mM15sRR@v&P5jTEyTL6lwgG?EiM!CM|SRC?~ihSd91oZVO~1n|`S8Yu^H& zAGz!odfxR^cnsu~_jDa}jYr=1T}ChDZBN$&&ikD?j`tmt9gXZ8(f$93ZJw z-o{%?9%)=@a11i2YNNm=Nvp`)XGyY8oyi5&WihI*B~uRNj}o?^*avqvr!b$KE1{qi zagBR=ns%#|N4OSWgh*23jU_Qu)bg6pv(|i|#H>&uxvuO;K|!YgL&-|YNt6Q*)TrCX zx^N67wMs}jwRJ+2S>!hW<3I}w4)1_;1%)dCCY*KpX(yaXJt2#-mv+xN>z#wcW2muZ zbF`0Z1m+=Qms(|z-xul}osgg)Px$Jk=Xdhf=e}<51E-zy&8V1JSxXg*Eie?62hxlv z@Iopi*(Pxmdp|NVbL%hZEcc?I+Wk{ejH+qr%u&@t#p2)*a1+2z1AkLo&qU#FL&wX^ z*4DqBkV4dX)%3tAHD@J{ikW4#WFIQ2gqS2B1U2s?!onnif}nzVl6&Uk^_qthUWbGj zoKqPyi)qn~lDjs8?@6cw*8y}(I}}I3^+EOZ9it8Mg-?7FZ~e_YSQIN)q=jAMqH3G~ zF>ZcZLTQn46jlTqDEpM}65F-nqlkkS4JwbJ*p|o_=DWid#u_jI$cS4`P&3@PHAI>! zv)PPHZOsi<=yzT>?mST$Q>C})7+|+49RcnQC?<&RfTl>w;(!DNTL}A_k#=5#cCex= zw?7yeLm@6(5olSYY_g)5pXL6-^`OR=buDft;V% zJhF@+ylSDqZb+GgHSt8EoPR+Zcn9~bE{LJtmd^+Ziv3bq8qONtK+qAWOEDavM8n4} zX3eOxf#{6y5Sm&ywkU=&Tg5I7@;eD%;R{Iao4AN1{tQzHdzPy$O7NYq+5h48r-d<8 z+w#>wHX`j2-vj#bAZAs(3?o^7e9IK2`ku51<6mQ)si$I?w z_Ht7@6LNZB5rTrPgPWPtcz%Pf%3x;6nN=|=x1}i&uF@WqrGVPtqM`ty_$0A9K|yDN zHJ6c8)j<1k*oWkSJHI?upe7|Nf|68mEOIiC0;Jv7LJ?iSv!0#%uqC=Ezl0r>;n+|$ z6~gk`N3wrxa)Zoqb(qs(O9e#^OI+LJ=d3Wg5pTfSKk|Jzc7@CYwbsy-3j;ir9bv34 zV{*Pwgre*?vbq+WX2FJ>-QsW5v)5F`h8#aLei>0}*Fh#&6O=*X7;zMi5v(XbxuNfX z4SyaFq*Sg9#|FnY(r%+LFlGQ}!=!LtvFh0`(t)FJy`Q*MXaBU&Ry14VHvoTh_5bfF;Tkh%7Wt*NWtfS5V&uF60Yk{rg)vb$f0j?v zEJaS{wO~g|c@0u#SphIjq_bcs}jF4I=66G*=^R;_^vcv=yyv;&4T%(d^$sNHS zIb0yg(gIOqE_G&%f_NZ(F;`ofV}Wlk_=r=)%4<~8EVORjM!hX47Q||)PzPD?CJM3_ zLLw_eySv1S?|Jo}Ea;{^ht?pARz**^!U5ADxd==FItwHM@z6L5%p067Z}ypW*(JSa z$%9Wd$eLBm*q{)1O9tf{cAisQqta#_C3XitIRqgf zrXiRME)MOMiNdnzpG@ugPb`RT+rH8o%AlnSi|3{1V?f9n2r>4l5J8i|B`6L9(=E=+ zGb|i^?w8bkv)7~=K*6?gnZCjj3pspkR$qnndf|2J(dqGSC_LhTP&B zGnbZJ&A9V8E8HV!A0*x&28aku6ftZV68U+na8_nR+m@9RCr>S{Ds4G->f|XCTNX_@ z-tM?_`39!xa1GLEiR0n+=(g)Iq9`655yb7`x6}p29tC0)8QM*+TiC)??mfY6OOgv} z%rsguwk5ee9(15=HlhcdmBb+siGsF}<7j#UIh-V|GoGW2%Bcl4X6h_Pwo&&*$R1D{ zYzHU-kUh#=6cqgD#DH>J)pc8@?4jt)-bpn`m^F|6Cf*#7my|Qm<|qfGpdcP_?3wOc z8hGphSoWuH98^_<4BEI1`V*7ll1OSk(mu#dP#XnGpa^%;js{LTyrkF9xApE;Ttlg~ zbdQE3>rM4;0?&q~%hb%43$Z#zK`S9=ea;67(%KYxT(`J}QfwulrjM6ng0w+Efc##N zHnmZ(Q%GYwWm=*}eenYSqCY3+7uF!XmX8c(@m`FG8v@M@u@i_*a5G_W3Wa&0&*hx< zpTx}Kvln;o-o4whiW)PmmNIyUVGfWj7J|(J0e~A5S(qr|bXZ%ucFPP)Ty*~2Vt(7a zhSF(8nRayK<&ba#zoz_SQ)yEuSgxRl4EGL8TGhqgPLu~WrUvP>?42%hohcK6t`V(; zAcNK}F(;yaMv+G;Pyfk+4&uan7e|^qM%Eyemd*$7k};t{`rseLy`k(-QUj(oQRJ=! z5u|H3%&Sig?e_}t%q`&>N}?q?hK6IOst{A7Wd|F$e=QVQrsOEO(0>AxsmGaL-j0h9 zN7f*#mS@5`Z1eySs~u!dL2-tmtt2VvfbBY@CS>O2JY3&8#y!Mu^AyC)Tv`egU53$d z`M`!@)?*ylxJm&K6cz#JLU!tpC#>TnW;yUG$~(|1V{?U|sDSP?85s8iHWTf%&@Scz z%2eGmva}6PG{{G>dg%}F7pyCb%~4xL@7#39oNTa@JC85E*%OTT3p+4D$JqndSr9RE zcjIDqpI086ttKL~O{wqTo~*u;j;X{nqaZ=4yOTzOtl_LJHG|9fSPdRl0rdF&sr$Bd0~#E zrxlD&jG0y4$sVM#*o^q$IBX;3aA0gg+7nG7H@7wlVj3EBwtIzzi{FV_YOY&Z8k;UV zM#;ybxI9m5r36frl_W@b0GFwb?B{5(iFc8y{%B`)pwp0QJx;;D)`V;e;! zqzIq3>?tcsdcQ*QSymOqrm30WevouPQgCsV==x*xuydw@3reS*%hT>zTjwQx6*(>? zjS6B@)uE6#^n4x%mWIk_Kq`>v&Lni0xsY2WchZG*X*AQ@a{j`o*c2I=pM`9Oky$=c zeK~o~L{~CV*e;w55&&-cp+3Gf?{zxmcZy?^)t2CGmLD)W+505oNoA#wa~uWbj0qM+ zrguD-;I$aonfm`}UQM&z3=tgqJ@jMftI+46k3)MyJ40JTFNZdTo(w${x;s>Z-Tw3&}KYHOsTsOH^xR$u)p##(;bi*rg zjdERqekzx_I=k}G5AQrz8&?ZgV^_%KcG=Jo?}H;q@&6YnYGVdEa- zHe7E<-U|Qd|JA?OkLrie z8}D6xhyJ?$65d;QTz^2n6W#G{)>rD+>GSm&dX;`P`s0n(hwA$j0?h)c z0e?XE|KtBJ`Y|5$AM$_Xe;4mZzV3es9T^|@Kj6R9ANAktU+KRNJsD^CtNd5{$M{G4 zhx+@YD`OXbdw-_?e1BX2>HbsEm(k<5`~LL(?EA*|h3`{zW_-uD&9}w(yzgn>Bffjl znekTN8s81R#lAVdX}*c*%~<3c;T!Dh>+9jW*q4j$jOX}T`d(0i9R=3VE#(Yq8q8fSVZd&hZ8y;ph*yaT+w(5111H_Lm0 zx1IM4Zxe4a`ZPK`e|diKeCzqrbHMYVXZQa~a?AgptO-63s^7UW&ukDTqi)tCV8QJ$<`&$kgov( z`U>bRpqqeB0`df;3rG_nTabK~e4Q$wk$`{zzW|>AgF(_C0)7|pqk!)P91(Cxz(E1B zSxM-chN;;rLs=_;xrPyb}(`SAzUPz+nMQ1r7(V-Qn0H z$ae%Ve{ncokgrVwmE$8Pi-Hs>0TP5HBpZ4P9i$5gsDKs4EwMZmd+%6KpY7T8*?TX5 zSg`W_XXe}`_wjF|G0b#c-moH`@yG=I4Rwc+X0kS=|sq)1%fwFM<8ZMx} z02Us0TQ~V)3bEVT%U3%AvLQBEyUoT({VxGRD7x%~eo&CITKz5gdQpHZQ@=;P?i9dm zVb^7~x-3%{V$fI1f3iYAjez+AW(f!jV1BUcQ{?MR0W2}?x=?{WSdg-4Jzu_Z1@sZn zTR=|%odw8-=*%*9y@eos0)!@X*%w_Gq{cw|Mu6~&_J(}DB!H!&U3*%-o)RE5p~)_3 zw+r$%0k;acUcfE^LK@md^2I{cu5FR8jRMv>?YeGX&VMfBKkN9W ztQW9Wz)Atj1c>+zGP&7;%v`qMZ27xM0$AGEf)nIxoPf~+P7=^dfLDNrLEx|eSy6z+ zg)JZp3%oCX_m%)A6kFgi`FcozEG)njVhhN^0yoRw-5{V|0JD=Vuv@;a5Ww_e3y2a9 z>=5LI0=5ZY`C<#Oe6a!VZZ9EUn76_;XLQokT5+!x{U6A{Bg zZQH8UVx8w4rs~Iw!V#)=mCF>{K+GYz4#;9hgq9L38ln)DM9?;`#a>I{;faq^{C-x#zm+03q}O#N>w68LP*g=~5iVxmadLRREym)Ba*;bRBUv(qbzBC0mlMlYlNhMBNusKSNi z0fGhtONNg^(TS0wn%dZX*rL0gw-r10g(IpW7T5ZAZq7yq2VYV`M3bt)WDQW*%gkJF zvtve1a;-n`r0F}K{I$3&a)JmUDs%k>F7>vYlbRAb z|LxY}gU@cSicn=NWhsDBh`^EYAty1SEq&sm<1LvcTRvU&&{h$6jUx` zL1sqPO-)jJi=P8Copk)9NN1&*a6`TfGqP}lK!7yB(%4K*ph(N&lDOI7#;&nDP)p99 zKNm$hskM-(xl;}SSqn%hL5e5HSl#lQD5`)ibcPxmd0;K|-TJgN((&kt+eog!q(vJs z-0E}M8;ctDs8s0$*fp~%J+%{GaR!&7&X06Z*1#f{8#92_kypi6@C`DD7zNUWZpbN^ z)xJsRFl;Wr?pqpZuT%h%;wE55&=WEhAecd4SO8J6iK4t1(00DI{mRr9HTo^2cl5a; z9BHRE4l>$CIkN0sQH?MWv8e0@D6$SRY)h@=U3B5h;_t-&-$nKRC*i*S?7&ZfD+5{n zr~T#V|9>I+|2^uRh!^UXdYtav?mX9jT=QKHyzkb-`M9&l@v&p6!)}k-2ie}UEwgpe zAJfaUKeb)|6aT+7Vpgjn`{51j0Q4U!ETqC4mIFyZLBV9=?3&!X7572{rjoFG#!T+% zQWl|_RkB?XSg6K;;2+qHoJ$3#$gGKH6zg0taW!7QojbP_>A-!cV=j)6I$|7b_DjZZ!q2ZrOz5;QjWauYQ%)U9bA8q5s)}(g+Gu z*}8!VWmCC}+!>Wl7e1qsHi9BzLLq6VU8xD1*`-`#RvR%ZQ>nw`Wc*yHdyoq|12_xL zze}JXX<*71bUm|)hIHv->J;&AIAYeQq85>@Q0x&b91s8uG)jn-ZDSOu3QZkya+i&> zVufz}`Yc4lcGO19npISF>Ytg*qvw((Gw!G@1r%ZmV35oc-f!aa=6B{yz^aH@y-I1# zREY+r$DShF5M&0ON|Tx>GO4i5`BR2iGT`c}@zU`-8%iT))hb2%NQyxKu&WOP2c7iH zCSHQV`Y15dZhUGaGHEeM4V8#dh;j-XoAbo#v* zR`d$!AEW>#rtISc3Wgk+_3aK?I;$RkDyLsnt0K;1*Jol}P(2JFOa)1;7GfB;2CwF%zUIlHJX)5|_;NRy-F=GicCmQV$PX^QWf6|2legM)^$T=1SP3F3*|Zoc(+I} zv5QaMwFEH*1tucq%{{?Nn(MsHXpYmmS#`uLQAG?(e^ibon;-As<7QgJ3K>OVe~@>^ z;*2I)k>@RYYwW-J-yDvpQdLlbVWZ0ck`DBUJCPT>aN0o=MYK)oR$FVOo!80^pCdxo zzdB;ps?y@pFf3#_x*5q?!ImdUYoag{jw`3&#OIr|xLOAb_o|3lzzX56J&p0fsNf*B zNubKjewB;@*`xd?2R$2}u_E0!f4UVtTQ*llMuA4ighP?S0@*W2I!JQFL?GYExWYCv ziegaYE0?}(Wuj%*QL)9O^2o>p$ve4Mo&*^Y`oT;Tpu1rZ+@}Uf)!o6er z8Zrl_k!r7?P~c#ykI7tONxaTF$ki~Vkr8St^pe&8lx;%W26XU1aRL>rCQxhu1C^N( z_$)OAHQ@*awc=Hg;W7uxNgXz7*~9`#*)SL|A&`K=>7Y~z$WU&mWg_aRqpC}!BA=obu>NF~ z^7;#rI(AaqbnuuZ{jF~0yqkIC%E*wTOQ1)X*aw%MMpy#hLL^WSM_5HpdrJ|(nYA#z z&RJX=8LWKeWiGzB#l|2I#~vGe6E;mul+qlz+Rh)EXg7?Rg1F;TJtsv5C0rKmOF1A| z-%=a_NlN5~Ng2g4CfE+sK5uN>hPaU{f7ksLk%4O4puI_IViW`xppKyNV2hJFGEsE$ zxB$thebR#NUb+IkmydsXR%8I?p^Io3kJ=%O3St)97_(sDzuCr^3n7LbQ!H5|tN)3v zO}7v6x+D4o`z5-wH{Mn6=xpT-Io(d(@tfn2<6FmPj`tmJI$m--<9OWhAYN9#*>RmC z>bTso!?DG&9&f8Ja?E#x9WxwL9A`O3;&t_Yjyy*nM^8srM>|Jzhu`6}|7HKhe!%{f z{bTz(cwhYm`&0J4_WSI&+i$dAjaS_#I!8N)I|n!m(Mzz#Ioo-@bEC5!?+V=J{L=Z6 z^KHZ7{L}ff(e{6pMefXUmOEEFuf*%}H{iwj=bcYFA8{>lRpX7{33%qZm#dqrgR7-0 z=yJI9T3Pj&7$((z(_u5pTSg3-k|!>BZ-8 zjQ^(Bs5Gcgq1qI#O!0m~RF66pRIE@9i&Q5#2blE<&O%N-GyI&+X81EHQ^#<170_Hj zngIDPjf4D!!V~&0jSmF8CxDX^4dWI0;uIvqcuu}Jx5@w!^Z&O;0Oz9_#y{nYQ~3;o zGp!8cVnK3#kzs6*FV5&Qj0N&lE?}~NA_1JIXBel+*Qo;X1mp_H7LX;Nw*XGKG7Qez zGYrmOG*BbMKUcQE2*{UTfNY5KC;oEEzBs>@FV4v_oL|Zp=bRbNPvz@l0dET6tUbed zk9^%FfK$&5=NO9WVbwwfrYj1dJCjR{n00e9aS3EJ#k& zGTci9$r+M{o6{x@mu$I9{y(=Ihx=Ul|KwEMob_e6mval3V27r>jIt^z&WahSIFG^m>}iXd-uqfoQL-^`4TGf zo-bcD0!jo-6(DEq9V}n{1^h#R(3Dpe<~>P}vOaG|`I2RNTgg`o0YOc(cQVrDAkyU^ z(q$>>huv>zK_gw3k}gYWuJixbT=TfJZbte;{FQ#M09jc2HS)#Dlt%NA{10yLC;A>E zeYgDGb^+@JoFkx4K(&Ac0xAVm2oP#dpD15v3K%9}r~uiXblINteu5OSk=|Xtx(PT= zfQY{IPV&`WfDl7^bNOl}z{?=*h=892926j=k@lW^y({1~0WS%7M!-`7o)qwifCmN0 z_M}}ZUqTaUJLOCEEKP(@+Dbt#6|h8rkWJbg`Kl5iJD4_Iz9tJ8FJO#-(E>&a5MF7X z&tJ_&)HctQuTxOW`nAzq_N=)OQS)>lUpLa<0r|#{0v;b3;0FA z&jMsxmr#~#zaYOC@U?(11&CC2eJo!hms~=8uKx(~O##Bwu2)!%Iu>?dC1+Ew5bpql7t`cyifb#@w5+E!SSSDXf1S}FzD?nH#AiNT&5Tu-Y;4JwX zB_LP8DFTGN10rbyCkRpqEFd%=kR1%j4hGWrKk@%2;8y{1?tVFUzwDC#8~MAh1biYu zHp?%E>X!}i-!Ff6uYkJ+Tq~enz%Buo2-qP&l#O3D%YUvQ*9j2$;$J9Vvjof(P%5BA zfJkA#2oL{IL5dXi_mi(40)*B4!WMqv0KZrM&M81+;1gQ%$@chUdwf61-yIV0p@63Z z2<`fAmM@`Q-wpB=6A%?}g@8*1$WHjS%GX8#D+I{eee>mOo&XW5K3Tg@*6tJ1@SP$5 z$q)e|Cw!-R?KkO{+fUWtoj)Hs96AvCD)dq4ts4MFK(?Xt*9{e?UF!&X!|KAF}9DD{91@{MU zN9}(+ctvn$a7%DqaCxv6mH%bIsll^QnLa4^5BQJ0g2$mMJw50}P5N(vA5fA0ao}y# zqdyDZ@qxe{s7R;}ToKqA*os>86@j|IoIrV?I4~ZS=z{}&1KEMzfo_5Js6+P!?Ec^V zhy368KS34xtN!QwPxv47-|63j8uTmu7x}mP*ZWua>rmTJ?l1O__mA`s_V?xb^X~o* z{uX|}-{JcM)#u;(KJ~rpd(HQ}Zy##U@ABQ`yT-TMcZu(O-?^wfU*wzTtMEi02p2e$<HmJnFrmc+=tx<++VssbpOZw zlKWrw3GPST_qlI#U+<2&FGpX5&FJK~)Lreaa?e0lgt6`s?g92w-G%N<_lfSV=!%f$ z_PBM|udW|lU%Ng=PlQ+O`&`eu9(O(9y3@7Cbv1e-T;$s7TJKuns&majM}%V6c-KhR zU{_yPHu@oSbG3Ihcllg)<9BpJ_{R9ec*l4ZF6R^Ig>a{_$G95#BNy3s8{6POu0*6{ zt})XnF(w$Jj3M?7#;Hb*agx#9=wP%!Cj^J{59g2ei=E#(KXtx~P6*FC_cr=0IF`EZ7g z=Ka-oAwm6M^8)_v(7ZdtHqAo}UCliV+ok`+uyym<1oZ>yk07t6nf|@s;P3ApbNKJO zHs8pwS@X4m|J&bp_rJw|-`zJ)pz)hP+jfTCe1`>kM+@@?PA@VNAI6&xxs z?Nx#P=LGt^0zL03*jk|LI)TPs1zQNTk7L+1#QS0E8k{fR{_b=Q#qzCpW!NR{ zkU;N51qTQOWzu;ITKexhr(Gh@`-4D3#_g1T0e^Q&ll|(H zHb%bv^51vz)yOxZdo)fbdKZ|WZIeK~j$y~N_5wpf2OWJY<=cI-K}my> zer)aBU-Ng{G#R&Tv(w}|DF0ns-!A#~EfHv!b8nMY!ryHI*9-Iq1$rw4qKgXso;Jq4 z3f?Tx_LG9pZ#Lc5CWAj)r}Y)+dr!f46%;aQ?RCkwd!T~30=4HAe2if$?^uDJO9i^0 z6zKXupnVp@md*YU7!Z6*r;v3^eLUk^gwg~CTM7&W1o}G*G`3O%Tx>}n`@rF{FB3iJq(g*?Z}w^N8Yq|fGILh~3M zOp7VlPGB>kp zM*}Y5b-zdE^Sl1yzxTVo6=-}X&?)2l9d`cqr56iKds<*nh{uQaLPV>+KvbajLIp)u z`5fy1)8+7enylZOegpq^-e%tj49H=6y{qNh^^1ZB6ns&jQ+UVg5T5Ybe1g}nQZUTh z?Jea`Pr96fCtXB?rHk&+P*BdlbBaq1%5%pidL%trzGyiJ>bfKi3ub zr+j-K6zGwk<#Ne5F5?}+JI4vswlXx*dk9RMC@>(@X86ySZ{M{7J--QbW(#y25@>Vr zat)DG&Sq~g+SyDri8ItrzJtQrPWM##cJ&iz2roGN3;EmO?k&&|s&P0)VmTZ?3*I3T z%b|DVZ+rR%fqvOOyQfON4LNPQLzu&E6RNfA?fB=~Tk?NqYj&}~fDo(Azg)ik%M_Gl z*}To<+atSXb6+9f4)|nXQCm;`)YCpxaF)QJY@!}`TfPIW1iBV0*g>E}c1_nm=5MXp zp8`X-2=oaNXr4>t+tW#)A^)G|d`Z4-BLwO<@z2tjh4mxprx02{;(t`Z9SV*U=;NQI zA2HJ8TW1p2|MCm|FAuYV{+D|T|NURi2NYx$(EkkawECZcT*m+DNXW@Y_%A8(ibyXzal8`A~ah6vP`GyE-hjzB+)Ed4jnNcncJ z5NI%u=)XDml=a{28G@&0DzG2FXHJyn`|22)bS^u@!0>S&`_x$SXC*Ssq6r4oc ztN-fY&%^0VtNP()_sDlhw&`%FSbZ~f=!b)w|9>FROTHc31={Bb)CVx! z-%O5Wf9N^+_P#67Bfn$6D=go}Hv*k<9Q*C^yY}0k6}(NTZ@=~^Z|8m?-tW^`hUni1 zRsa8;TBsiV|KA9%47Lh96d3RS+P}o#96i?xz3+Gzdpmfp^JKYSb1!!LU3a)98-L;= zFc+Qa7CM8DYaIh{uRqoHsckhX0G`k*wBNOz|4juzWfZ-wIUBRwrLUHh3M7glH3$hV zsNhgp7J@=PJCzW2eb7pN%}M`4-u8Jz!cl5S_<49FlGnql( z6S6|zx2uj)gKJ95rK&Va)25m&IxI;?;$mz;Aq5NPheWu{k#$W<^{k$c^vfCbWl?mx z=Do$s1Ue;8GaXZ7Yp(xh`Y=(*E=7`DX0EpGCky=B2!^i>ziU7?EU5%mIN%RW{yv78aBC95TCB z^(cxQ?Rzb}#mQ;(xG0W>URB_UDE}NN2sorqXFJYrED`Rs&h(c1T_q9fdrfpk4`jZL zVUVDPWO6iMKq?v-h{^#3h4fg;iNq3WE>0>GmOey1X}eWMj`p}V^mAk^JWL9qz!gTE zXoukig@kh|4yruSLZaQ`{&*U%=+?;*)#I8nEVXr*2#Y`|4evWnoO60j6a*3|09`UJ ze!a0R+ScJ6=WSCzD2h-|YuX}eI!@2SfDiH14VuXy%^yWHBJzQv%0aLq@@sQSJdG7j zE%b#@u5e{h#B5|uBVUY>k#&rJk=c)tAu>tcCr~(6j;);eTUzKDx6;ELZ-gUg zUrm}9Nwm5PqhOEubsj1@rm+(!E|j4R{2XojW5?`9lFIPWAJJox9~MWbe>Lq6HGF@L ze=x*8HlA{vvC)*S%UXp{l#mYA%N>_YXU+K;@An?qRUARrYBjo{k1~fsLv##9dPNFa zNzXunP?jdJ$Df^S^clnO67&7LYa-}WO`{{=XXqnjPq0(WL+D0i77GByEozWzoQ10P z8BLP1tBym3?oUtDMN|)KG&^we+uxu{WQ6j5A@SGj76BAi1a?3IccvEG-9!zxV+=h6 zes@uX`dh=W0DEvV2PR?an~$h(*FLoj7SShV5wy1E6C$-w zib_Zg4Y*PCO^Ib!v8Z|l{Ni~m7H4b16*p5inrzId$-$X&+W@Y<7XyS=X%JM$4i~mz;i2X=DK=A$xL)%XSt9 zgfM}af&tF8f6C$$6q$R(u7zi$ILCb7njQ6b_N2 zP%M5wDyv}2!DQUCu~uqg0-V0)mK|0VxibYj2S*WdfLx7ORnbC+kT`!n}i zcWc+puEEAv#wz1@=Y!5-$1jen9KG!O>{Dzv!o>v;*2E%l?05l=@>MKBH zr9h$j4{TGg;3^B&_tMd@@u$^Ij#6JNG83q)1{r{uh+q5xC(=tvWPO_`Y6AeBl&i)6 zYK*#fETWpXjH)OaV6pb3r&(HH9EdFx&$E3@MQ_A62^97U22|FRVU{8nYu!toi+`V zf`cR|0E$Po9ocUtilh&IN2a#zJPSVMfD1-?$pa{gQg19VaqQ%2fXcYb5Lp>j;ffJdyB=sAjFcm^sg?&a^I;4f=izj3? z)61i(6&AIzV8b2Sg%k`ocZ4!ka#T?85U^iay>ClZ4wbv$9Rv~2K_Rt1A6Hc4gA8^7m;18JWZMUOVYvg=gc35J0f=MWtohqwuZ zV*W*RC_DemXk({SeIfe&P5!Mk+EXnC9T>HXF)}ts7Q^nU`6@c2NW0;H^80?9T212Y zwsTuOHfQ;?sM-Dsv2Xo5%)vZK8>T{h4N!2V@GlDZc5O=PCa;;n{glE{v-1^7Uv;8X zP#8P_7!}xkl=0DlBv9}#L19@f_F8e*R0qyUQ8d0H=?qy$0Sm4cCU+dC4sk=Lz;?8B z@u8LxIog#y8asxu+xTsss%Uq03{-_WPkyg7D!>w;LDi}lMf^x1;K_3>3H~L0ZrD^Y zIJYQDeXe8;Y;a1zkK6VjjH8|(BqIUQIB6%4p>1k+%x?Y7ZC-oNl^=Gmx;h*+`&yNQ z{W{Acu*8^Unq^5Ew74Q)Y=oGOU{OUXI@MKh2Rf}Ag|ksRHgkX!{{a8kwsB- zx#D9PjP{DJ%l@Eb7M?!rFPJZ6+hWTE1>wbAQ+ggl)PG}_8F;dA^x6RxQR;1leWAC@ zpbY8fGuM$o$B`4uCs1U$;N~U$1q%jPN!4#?UG!~Pw5_ZF-3J{yXFv;>eIKH~?>Il(lUW_tq?pwo%&zJ;H5}!G7#-5gC!*z`TNDZ48GzBV$2DYI7_d zboPQZI}VmaTdRdo1F3s?A*5vyFy;eiryt++F9)R|br52AB=Z2a8Rw%x)&1dUE42ht zBYQBsFgC`*v6B7W00qfNJuNefcUciCOC7lD8Q?69woGi6U6bfK*pNb26r+@=AC6v7 z9AnDPZF3~G&6+rYS-mLQLd^lQn}sn}gE3*S!oWs2M6CuWHdsN;85t}7)fnAwxrO6? z<6on%k0{_1FS787iY`YIN$T@-RlnK5zp8_&K<4GaGX-3L8f6 zJZ6_aY=QaKz43V^{s~8$sfo(5j2)PW36yLG1VE@E%&{oUm6f5dw?b4c@Xl(#>T9Z^ zAvIG4Wc$w;mh>#QGvz3sseJ{7B@(BHw^*{XH;!A6fJ@(5(V$F)x_qa;7gLE|;!Y|c z0`|lkm=(}0Il0>Tr&>~r7u3Y?AmTk0(E#QWDQQpRsjw<)umc)~0YNqw%t@>Y77QO$ zR{nx+77l93-LQlA&XE3pTd4nkM)2w2_`p|z3j^K#_xq9h5oXV)#I*T6cCi9cMTe^LQ^da-3Aqvb# zk?GcxE$s5lE<~j@Got99#pV>cvC}O#*bb)&If0{uu}Sx3g5uybE}*juN8Ov6;2SWF zvHjzOC(Udmdw1VFC-HjOJ3w?cU2TEvUqf}%Pxoy zjE0?}#=}rJm{3sA8*I(weh66LS1MU&l{@2jG!M;6jD4E-$`7hDC>hvkvzPD0m~ zKp`4{YpAS#Gc7Un?1wYHy}taF1yMA~;$vCl()-Fxq8LD4Flba9MNp6%B)1kun?!LV zK1xeVYxZ$CYBtUyxp)nx6vfSreL-FvSQQl16Jk%G`+~W5Tak-PGN`ksQ5ikjJWJX> z(K5KXB?W6G!k$1;5)JI?j1w%-_QJ=xF;ZpJY@UVeRDCCwBmx+62^>nHnFc5l{m{6D zqe`rVyUoxYwag-s2=C_<42oY5`3KJ*CL*>@8IFP?Iza?yK;1E#J&Bt&H=X;Ag`;N2 zEIOopG85X?!21CaV4qYxg;59&5dG(*Z?SSnV?=q?MyXL2`3PveNLPtqP?!|BVS_gS z9#!!TpkQ~?#Ap<#?RdM1j>-DY^wjS9aMWy>1zs-2q_9sRXTi6ak&>Jmpg@1L;QVub zvczATp1lu^z<#NUnmw~fVeaQ~iQr*^lfD-QlPD6bV^BM+`=pOUe(wkS$%vPd5x zO@Xf}u_;8}bU8(tU8K$h1-{~(2*gH*AH%MHb0AAP;(9G3QW-^GEOl2vEfgVcxSJSs z0gO8i4hst7N^rPv+U=Ieckmz5qZK*X za~G!SX)lRz3f#0PdSuZocy`FCk*Ula0aKaos-WCRewru{6!-+u?)sOKh6#6U!|UoN z?X8YdJ1pXiWv<>B3MUG|#ry^<%2eEpqVyUVKAH2sZK8x3egi!Nv8y^tjk0Lt7QxNf zEt4XX1ojRHRS8g&vXn~54dymUu})kDQR(EC0V(s5AHm6pHd#}r;eu>V)dCj70L}_9{(xCQ1DS3L*!VM`7zTEloAEHTvb9u%{?mC>ALq?b@Rl7P^_p zmQIE|XWBCpMcW8l3Nfo!8Y8c16^<5Q5yT+S`lL}{KjYR8EQvKxoVXzhkC_0Q zU3tAFvAKRGJ90JAeAzClh}3SvxD;w-kwaD*L`(w|L=FtK%xrDP(@kRk)osx*Kf_TO z%}eM6==+3XAt_CC)c{3t8C-^B^?Jn8;<8Sq8;hys(Og9f$c)fu$x4vDiJS|T*8Bnt zdyy2fp`Xr1OET%u20b|&tcqX zjKS;lmpKa^Z=(mG$9}cFpY222Mq4L+pFTr7tVNm@0rvtZ1<7R=q65_ZIkqDQl3ZQ7@ph%G~L@F{y(&KdS2web`wk~jY5W7PNG zjKxI-doHtzCaOsnNlB+MoZ z#1An#4i~Hn1&wejsiB4_HcvD2&cY2tY7a=%ytnW5&kq-sM=4pBju*9GlVu5zIP4^X zg7}I7s{#sY0`pRGx3*zfsB3XLumrQhD5C z;YyaFB~g_#%MW9a#Kefi(=4e$zjQc)!oGnM>0)v9;iUG*=9BpBi=&hrOExaNCr*03@t~n`639>}C@SZ3zDF@7%aR@gwICH+vVvA$s zb&c^u^|FA4WP0TBL$*#yahlP1eEfr4s*nVB_Xe$ri8m&d6)lcOpt z7NW?ok>&zN$elXkBoq&7fD(=DuQR-OoQ$EEw`lp#f=V zCK{FD3ri!bFy7dn%xe4Sh?l2aJS$2GujGMI76PT}@Guk+Vf&6BKr&GjX@Z={(=L7A zlD=L4GC$J`XPC0T2S8{7X1syh~DnF)ujnjE{bU12ey_Vy6zQZgK7Pv=( zEQF#!5Qf;zMA3PV!X4)%V6p(2L8Y6wd-X$?|LloRUP+sQQ zO*1bSUVSik*(0r{Mkyaww1{Jnxe$5ofTF_0G_p4AAmUg!xcn8l$#{L!&_{5I{9rh0 zrorL@mM<3BrKCI_zUxszC9-?ifrjGE${b}WCcLmCyJV$NN{gjsAgZ(wzZoGV_y9N} zE@k3!7ZU|BOs-$v)V(Q+-6hZ6n*ZpC!_`qUDV9!WIv)ilp>PJXDc^O#wG|ZcDE6vw zV3Umhs{IGjbBbCPMa>LZ%7Z4K9Mp_(gWQ=?%(Nj21`!#K?sFTHIj8Skf`3l0jaGT0)RQ(r~OHfdavi=JZcYs&6I^Nza+I^rFgWsTv+Pc-kWv zUMw)aXQohw5)WWbk>=!XmX%qsP|wZbT>IMS3^frFVQqB#Af^M39TF5)*ii!mC|VaH zH0hg~ND0M9_>{uY>1xt)5cWn)igln$g6l=;=3m)lg2JS5Q?h#Qv2slFAK({IW<}4I znb`WHo@8HwU4!V|FWle@LlGb+PU0GB(*y;xfeglsviqCJMCT5F=*+Vh zOeu+(X|hzTN~K0PeMlUbTu?~xEs1jzDC{YW-puNYl5XJA4c&%@&%3-bhTK@zsgR^Y zF%&#)aZ~a8!H*<>OcdFUFjR8~9Q#N|lE8C~i|@sYV`i=_)q&iGVIhHF6ToR=?X6Nm z#7rQjAxXZZDjD0X9z#j2zlUR#9!u^FM2&4UMrXqvQWM-xHS#2C5-9KtU3PVN!iuP= zc#uP!;n>l1S*Ly-h6VM?-4?bBmXk6R1;w^wZpP4RD>I$5fBv*l|9H1DW~R$htw$6y z;UMrYE^ELfq}3Xr*rn0E~ z&dM13{V|?N5*k=r!_T@`$IN6|3ayeh#gTvvAPiu2B5tK*r=VcwK%y45zoSXURZ%^? z)95RSsoYseYa2g_4$Xj=J^Q(|K9WBs3f~a1xmjBD%4Br6;k&f@JNG>wj!~AZqC0&N z#)7Ad%P3wE1&dYI52J8Km?1M~eyJ5|*zneTdbej{%*>h{?4nvNusl1aFtTANm^BjO ztI9I6SFdQyMjV+%8#JymhD=%V=X7LajhkVjO5F z5|RlNseE`ft?o=MY+TxdTf5APsZ?3md=9+>2E-Y_vH)j8Y=G4g7jzVd#TkJ0GPJcX zTeA5!pF0`1&=V_TDtQ)xWT&pk2mKombmh2*XNN|9dL}W4cu29?Xv=xD}E%E~%8y{X2Gm~lIcABv+(#d@9 z1C~^0WTMz?wjXRvq~yPXy4e2&t7B&N>>@D8IxK?uj~sR61HhqGu|>XjreVni>6F(b zF+cq_GBaPTj3JGd<5KAT@D*kxJxOaJs~HDRGbT`28%&P;%wj9MmK%g@`F=-9OeN8h zY|&oAu%Nm;6@CYYaM3JKAZ`vBkV~C+d~$Pah8yd+tF9u3yjfmHsZ0L=qeB|wtl7dO zNw1_aqga>VrgPG03v@PUmb}m%^CyI{O?wIxaZCe5MOklTwKPD9tDBjf`Lz{cJp1G+ zAJ0B?N?FXOWi!y6ZG3r7w5v=O zL4iTRkOkVtk1RN3E1&9vhm2n?kGa(vNUdCiHQ?-}ES4|5OmCUfb)d+|(9bAuo2pt_ z_6H~ESH)avJrFRqgxsezY}_f3mnR1=h6HpYm?vMm?$`>0B-Y-JUvEJT>4?&pp%#SF zD4n*BLrt*u$j1Z|(o_i)?qf*dd%j9d2@H9b?#GrE#hhvms+U;H15$hjY79Gs-!5|~ z6l66vtsr=HV+TK_A9}oe@^o3ufss|znZyTiG`n5oI*7GmqDZ^o<}Ihk>#3f`%xT<5 zqBLetY>}PY>@eMe-$g8PGNYg*Ym3gP(v#X2E!xK=B@1FUwGc!!J7Xat2f^tajx#+x z90V&FO47{jaeR}6@r4g2xoH;#3dAH~tz8tKIQ4TCkLiy|@D`EC`cU^s$wb$mMjtudcYvy`(|1X!T}_|O%$XB9E6-~E%wndES5}QOnMd9 zZ#}AGlmttwqNjdek*x!@f-d25l6QS93b)-E`L6*wUaq)SRJF3SXrFiOen=YnaKjCxhg1ZBCfIW zoB!OT#f@{5gadwu4Wgp3!ypWmRm3Rxb}-E{bB1rWa&q&@wfpAW zvY44II|a5lWlDqiWF-`OmxPoA3Qib#TpgV$5`D#Y^hj=t88IZz@>wAlh2%O`1D`?& zoE*D`C5#@>j_e3eVO31!&w`b0+)E1MV_8DLsOrWrV2$L0d_%W$lJedn)pPv+?tXWc+@F&jVvp^&i%nMaU zad9~wg#xkR#PUx3vN3mJ!413c&lTYq<;D`{&^1C;;&&r34K~5K(4olh28v>?%p^g70@fTpOe0Sej`WuKx#PSZFTTTdWviI&ji=0)5Z7Cog`=BUF!={ifmPs*${3~1()r+qo8rhID#R1` z8Ee`_MQ0LXz!oAFpX1D7wZ3dhWRT5_Bp;~8*ts^kD_iO7$S4$Ms9W&Do9)nlwIZ);8O7Y?MK zRvk0bY00~-!?2X2s~U-zA-@C)?jO<3fObui5!ElE>?&dot=r|g98T)Ic&iPIp;z24_#e6DQ0Hb4tCl5U{KHq<^e5Ix&wBs0Se*)A(gDm z#g4{!c-hIQ*@<0W5;K!(Q5Veh_-vg}*Bw|ZDkNZ{V7im+$(P6QVrJ4Tc7+S=u-7192u|$1 z3dAQ+bgu~oH}&hrwxzCu_2#S?5@qRNId$iC{7MiGE_YymV0R(uiB$vz`vYzuc}d!Z z7g#bt*1bYa(dvp~*@}n;gKo#mj({WBW~2ZdndR6dpg8yow#>?|ozd7T&iNBfmfmk$ z8p~26Q4CB8`Kau>x&U?T4Mw?1X#Q$(j>}0i9bb~{8Qgju`8j|Ab zl1CsY95sABx?$0Fzu6?|x8?!PIjf4Dq;?I?4pmIyiUr;(Cqm+SO0}sY6h{^y4>Jn1 zH5Xdqkmy{pyyfA0SYT@^F)WEQ-mIfVt)P#ba4GW$On_-Ri zeD3!V`34^r#Z+1?TmvWfd}eJ)j0MXKYE3qcR1Z*;$p%j0XeNHL*-DU6;2b#BIfdG`_pF$GqXX21|M{Yr%Cf~8Z2GU53TGkr zJrEAqeidnyuHFz!IHa7u^N#UC{uW|h&};h6Cx5N4i8aWnRgEJMX+-QAW-0^#IRJ92 z3>0o)$-OywcoU^g_BiTbJ)tT_Ikn{JP`dn1o`K{zBq)vpWUMSbMv=Kh*bbLkk~))T zqT;ai3w1G-S&JfZ3DL6cPh^E-CNje8CW-B3nVzbmluzqZbN)}}~fcR(+LHg#s5RNM|EC2q+7%2P{ zHM*&s8beMk3z?BFZ97I03lJ;7Nx-Z+Y7_zmVkGTzLW^Y9LdH#??87gHV`effl8bnM zSm;w)9vPQ7XPFS*6HpvZ%*bnFsg2H*yQzlg{?eG4LrZPgUX|S-n~jYS6@Zc$0Z<$x z&B^aMB{dVYq9@enzrL@Escc%v8Y!88SP?UdFM(C79zwxR1nS`<|q`nS9sx{3#>LZ8qLSpo%t3ICa{x3!HgCiQ5}DTcE=VNF$x@@Iwp z|n5G!P$$Nq{bHRMAg@=(`Uw1RxK)y9NHEhAG=4FLWpo;fl6}%1$l?1QD&uf{_s>U zZ?@b@eQY;}V`fe*g&a0v8c;V5ADajfhKRcguL=shgMH0Dv)qdAS#!&Dz1z^5s+gHb zOFd-{U?z|@XU*d5X(pvV1;zElS$QSa8c{4fj&Bxew$d9 zr8Q9Gr9=4@RR44gjsH!}Eo~!Z$T7;B6`C_liH!IHr|3b&a3&VkMX^rWL7FUgbgd=J zsiq|bI-d#0%p_XK9q!ZyAuO^Hn}gFPsb->-vzXoL`ePQ6gi(v0p#Ia7OJm5Qth#{QEqNT6Wh z5C>`NR;EhMo3Fj*w=vgtEssr+rNB9Lm?i->jf0zo!juchqmO)4j zIeEe4*km;e-PFhM%OMNUAcp&DK=qrsS<;yf8%Un9cEOQj&gO3fwz@l4VOGV85(MVZ zMVHYj!)gN0lg!6#EV9YD0^=YFJZHrKOE%4>;V5%|Wk+#rlG2*UYVI%Fk2o1jb7%q# zOEL{7A}E-l5OalNh9s5F1HM#OMHAIR;AEST8VDwDcDMPURDb`3Y|@$Ws@PaH z3tjI%gjsML_!njc-AO{^H$;(718i={5+yJ6aT!2SY)oRS>^jLs@FQ3pH5!zrCnZ9H zg5iXzDJHe*qQ+@&fMfqLs4A)b5m+@!gBI88o zBhJZqPkpW91pCAG@wP8*t88uc+w@7=_ZaNI{by2~GF^$8Kz=p6aA<2fJ@|wW5M*4z z2oe-~BRSdj{KmF4@1zf42V7SXM`EjaRYbSdAU?<#7#tiIGL3acu||b1n0t~1cVl>S z>fU&5QQXXMB_aKx93AOPq5xWfG6Dnzr-s6ceY-a%qh~#irnX~>D&xp%IDBY& zWfUofAwvWxD2@$a;N0wSZ7h()fPtgfjwy?q`K|E(^z|5)iI^M$2v7twl!YrsHFy`O zoKt^HQJF0t9(;D=z^}t`N^_-nZn?`&fn#xFaUPHabk+?}&oP@|>PO9ppd?XRu2^0tN&^NecV=P(m64j)>P!(6X zt{?%2-U&aL!$M@jVrxLYWSW~Oo(ap)c3OJuVe8G$;9;Y0X2p@?O1360iwx~gj7z}_ z&LH92In`oG1pz?i@Z0jVYcxx0VeLDYjSoJ%y*zHFxl%IX>oOOXfgk{T?&cGDf&!_- zt}W~yNLA03ec9Ia^XD~Hab&wHm%zzs;%xuJ5yWqYO5j?$1PY6U*^%4dZD~O(PO7@; zVD7)daU{I5;6Y28F<1}i3j6~*9e2_oEfNPNiu5*EDCf8XmYk21sd?D!Z8PGO@JiAj z^{?YGESodb{034+8Mp})3{&JVWK={_^9UAEoVM=O^0=AqiVj%XAGQzgb3bO8HnsyC)G<{{e1sv7i#m6_&lrA(!7E8{BtmF!&Y zAZ7vuBghHM9Z?miF%sD(3U)P}Sx(V@3lgWUM+H)n?q9n>}rNs-!iT9Gsp%SHzL`stg%3yoDeG zf`Dw_@+p9_7zLpTY)(dbjfGPfnn!n`|0#`|xv%hPhsa`Z+PJ;|BjT6SE@3f(f*>G1 zS@=haM!?24N{b=emBlh{)okrBBIpN4!2vGPT6EHkqWdp!JM5(l?b4eptY%F*_;Ks& z)p1RUOS*jh1;Yy4v%Nu~G!+N{+XDrfA|cx9rzZJ@<1gdayB5S&tAkqP(%!_3ta2nl zfXobX*AggjC0*xazSN`{b16&dx=+eutJES9ytC1nVSLzC*iN=JQ~*k$=*|oOP8ppl z0j_zU-~A28&Pk{P?JZ0KQx@q7B5+^|lI3BdKr0mG%I;=O@tllSL1?%bj;&PdL6lH` zl*gqjD)Hq=B{V?E&0a>~Iisz}wyieS+10TXn2D@WkVhp(h2{hkVdEe~p#*3mMo|+g zPzM=~+O~f;>9AILNZIwNiY-@D!X)XA@j;Ntl?5L|njXy;Vift4l+&tRpJhp{t{Z#a zo)`8Xsf{gD`-on{T<%5Q2b-l>NW#!kDCB-3aFluaCr!LXs++FDB1>g`P#5;*4fl_- ze!yT2QSif|7c-_5Sm5kB&M+EX8C#+jM=2;BFe+>jtPf@{woF|vnkc%NfwssS(bd94 ztJ_e5>m7Bm#WE2t;~Y9&i{Zq;V9lHyyIBv z2-vT&53{{zTW@Ql->Hw#-p7EA{ZkpIJX*Q-vuk(Dj=^HVdJs!eK`d;dpy;S@&l$YO z!fBmO#b9528;&ET)-+nZm`ExW8ZPRX<6vY^Y2y0yXz zFb^hy^ilH&J(CXx#b0+{Qd@QNLEOgW-&`3t6K2uUMB9kp3^ImFfEI#b3Tudr2onXZ zM3#7dmo*mHrHbxi3Wit33)M*=sX_Z4ql)jyzBpX~C=G~Y3ZOW!ote|$S}N6?0$5@P zKba9nqAX|1OmWr8Dd31XoB(IbJQzlio~5j(vr~Qlc}OnX$W?JOWfq2mc7f~>q$FR> zV>RsLVcUR$zQw7)KhD!O*{qQ8`R!akfA-o7m2srclAJ-2qkb4OK_f_09(Y8G&IH8~ zKwM(sg{CWQ7EWmFX6g%AJtIz;wBj2(Ct)JkCy2mdNla+mO(}r_aX|27<_t*9H(tDP z3q6S$j;oYfc&(C82O$IYZ#L}0W7yjO@=z=xm+4A!9n zn1X^^0aSjMH&eBPOA8TD4&PoHr?gr+5D4Sem;~a4>)LT3P{S;XO%x0s&^ER#)kgh` zPk9Wj!G{*dRemiu>Zke*=tk}}1i=fL#ka8u6cIK~y~RVx_UWt;g{s~T$0?^)XxGr5 zm3_kPC?q0$NlZZU42;Mq4#<%Wy5)EaQgT&Gt_}&uDY2FWJz4T+U|8lpiXoBjC0w6C zAsB))&u-nyiX~tE1rq-r$}EmkmaR~hp@LOVhhTFkV|YR2=iyWsMF}BPiK1-ySLqyq5_^YY9?}{MpngqFikX^I7SJYPQdFO=PkDT?<{6T$Yekql(Al)ejO zQ>lr_RpUf`suv9&+tcN5TR&N9)hgr1V`LIch^td27Fd|ZCYu47A?$gXC}KbGdQO*T ztx!+ZQ`G71TK!c?ZMPj8Z<#i#@3xY7SG5RAL|b5<1MFz%0g73YU%=-;w~&~R zrp#HBbofeop8LI7#qloclrj8r3?)R#w_0?8MxnC=3KAA_BD>|UNk<>wo!z(McxUzF zN5a#%7GvRX!K<*$DL9abmjMOIKqm{?H>^i8CR@G+bkp^Vnek3)>nTt*5~D-eo1LX7 z_(5A^qS%Fk?V)WbN>xK|J~9nk^jJ9FQB6aY`jPf5zqju(-z4ur?1*PYSxe#(HI`I`fO&8<@MXF5$$Vf*P#l=b$zJ(I6QejQvblp_U0mhZaw2yr(+9B= zR0u-p&{@h&WE2P<;iQaw?dqmJ#Yk5K+4mHQ)7E~KqB7Wg-iNSI>%(y~)t1t&56O@)64?}n_0Czt zm_<-@nGW|mtL7LsOA_;#y$~M#-ruX^D#I3?8f-c_a$sm242KaRS^1VIfuh?R+7z7jm=LGH|w82sLK~Pv3h4<23P2{9b`{>n@*=2D`vL)_?Z%0-W=ET2Hd!*pR zArcf@nh?LaxnrJ6RdFo*rd{t-&#bMDoB6h=yWp@3m#$F~4>$+fj=YEj3T_yzs4U-2 zO- zyBp>dg9jET=m4sV7Mei8zrm$Tp0*~%u);`w=DnmUZYJT9Kl2Mlgg*mD!tDxZ8(NGk zT@ytliA$t>?b2aZEYdY4aA$s8IwMYrxI`UDAfO-u$eKo`3ot64Q?7xaU^hbH7Zl!} zT9Bmbs%lEktBk8GT)GA~Q)M|7%5=prEel#IIe>hkAW-Lt7UW*>$WyNCcyV<&Zsz2Y zNE$Dr;-U@qHOIwOXp!#TU<9*OmXkR#VrADB*FjBWII81F$z|0Gk;F^|+l!4oNCOHa zk)4!4VcSSoG_lTL>AvUim|};jxS6L*Y5AvMe3)if3FIxLI@wGOQQTKKGkefF2=&yv z@XY;%A5p1_Q+_V_>{ND0L35bJSRD*8^3D?>T1H7cJ12ML7E4oY+QDF4Byg zs!1+m4u>ox)j5nY#J5CW?3>H4o(zV03m7gL?Fs(9fCh8I$j+SLf&oi(td3p^| zFexxy!Ku$$ux)2A?}%3yUMq>4dArodeFsKm$wz7DkOml)_)Jh@&OrEQ6uFXm8tWb} z;qDW0Ghvsc!}}Nulm@otSRlyS>^>nV4ohTYbv!l%kwo)OxuPGjZdu&S*QMai9Xu1s zAt(w=L!zJ|if*#u=7dwNYg>GkJW3un+Hk5rZlYEoxl9_TyL<|=f- zmWqiYFBCdIw|QSHd)K9k>jWmn&%laEQ4DrDreY%Yswkc16I*08}iGG9z6*Ta(f@Cv=UdNC zPiOZ{?xC)CTw&vgahZ|he9k$~>2~aKv3#3hU?9ITB>_H592d0kxvOB z4LL(vE`b8^z)sC>H^>U{6mOvd)lhLg(rH=rpa6T5MWunMW!@mV1eT|W3-OcE= z)uXhk-prh(C%wgzq1!>WqwqsbnaLfDhNJj9F?# z&)pv-c*?BJ$`)t?6a{B;>;sdjz8*-! zoAuHUW%Wp?WsV`Ua1KTXdtfyDe)tCnTv2S!L=l}s-sDX8r5ImYcn3c;S6q*5TBhhT zT{;D^Fd!TbHa)?D+<-BGg61Y`t?bUEOg}V;juf}wTv@O3XekzFw)sQYg`@RUUmzZ~ zN`N9(#S&&cX~90*`1~%UJ3Ka{9%;0^j3Sr570bX;v)@Ew$P@ zUk`4n@|(@HStL|Ef+<9gVy9p#P_@LdD7xLtbJV3y$^HML&LO>@sfZ(4mSb1tuGV}Y zL{*3sK+nRzB@NDY8lkWf(%>yDl^=C|fG!kIsEM2DvPizvFe+xju7P1-jw7%`6D3gW z=w|12-QGlgIf)_^)z^gMM-yiqww9OyniBjEWr-PJuajqBqCjh$Uy=9xF-jnbPqpw) zPKulvN8&8+VyWvaSqaD(bO+&&sS>=3C>D$`y4ooi8%h1J1`Nz1IVvPh%eZ( zvgcPcY08y9ASN<6w z%1$wm*D)@{8m)$vw+daFC^Avt86ZD;oE5u$!z#4od8$=Oe1TdO!EF|8;CNoFk%~%6am>c|y{mfo; zPEul6b}3&#mBwdbcnVL!CUszR96VNnJ3?qSb?I-SXhJYh?uo7xP14f$xybOux6 zgx)|6thl$Yw!)sHUq$1ZtFmh9k-W+(4$mvEk(r2H*uhdN4wEyA#2g&d+}?jS(ay@+ zNB!A0&8SBjD_cKPToe@*R}nrPC>Y#Ai!)IaB?YPH_Dl77mVeZ6x4D{ny(?1QASD3g{tM$>~d3Cy_w%iFWel$un+=ZPWHXv#&ICcL{YjXb}@U-q7(^z<;ko8 zi|bXIE5FxH@`qT7Z4%B2HH89DkmV4zbkAAYC7JuUxF2pue%~0bM`|k{5MJyc3Kzu& zA|MxDgj)hdsdiAWIbKV`EiVTp4R5!quBX&iIu?54;Y*nc77mj*1Or#zVJbl(fI;r7 z@A@VwhzV^UK`+3)74?+wN^o%hxvr4L29HL7*PO0rXuc! zYan+0t+_IYwHNa3N#zl<)|D>Im~Fnj64xLDQgFN>fJj^;0#ndqP>NJsA17IJb4|o7 zdnGMOZ8@gz4HXK1gnc>_1TjE2TVzeS%#tlyzYHIwIQhZqh^m35aFVtWOK~!Q2#rg7 zRHrvd1uhtYAkWFsE`QR38r>MV1<}f?(g+p75>D`0g=M@D2^#V1$g^(*!oeXziZ4uV zc{iyVU7drh!}c?aBUA}14wYTIO3sX(Z_)tNT#^7uw2I8CrHh&fvcFi>%U5#sQ*8vL zuk3wLFjP$g!37f+JOHW5$;cjG96>cK>(C)CjcU$8 zrJ~u$zQu_&0)YeJmt^H@7rocqF2>a&Zg|$!H4#+;i`2MbwlqQ)!{+2_8qWHFzX9U< zZ!$fyOCPheiN!hmDWydbRSk>4pTqX@1k0N^pL3$vuqX;Oy7iAnG&ccACv&8sKUJH z$p<+@gi+*A+wX0T5;kN}yX5aB5mhP+9qCl}K2o?)vJ!tzEg&K~uzW^#G97vTd5_}u zrH`s3R31xe1P-CYA1t%W1L}r!$NPD#ul}NnKn<8feWDJ2thHN7c#V_%bx5m)~?Mdvhl?!ph;D)VJ zBJC8SRJSpkuXOJiv>^fmw2%oRc}y1B;&5{+a@h-9dr=l?tF}NNw2NXIikVPE5|S7{ z>ZS(*Aae52lAW$Gc!iLR?lT;Lb5??8qSr^GvXlQ__dJEG+f*=;h z=RpB1TB#9;1b*0&-y##TXc0b4_WA1SNQPPxS$XYo%#U-Eqr)NLsFV%a=rqImm6l?U z7d#6+nRT=xlCF@1LSe4j1V$hWp}G(egw3Vm3=9$#lZ`sVwvaQ68WvU5*3~ZVShrwN zLuGws$Jq^w7BA>nwz%VxhPibOi^~>Q4%|NNHoAqgI+CVVp$MnS4Z(GQq@+qeh;~L6 zIT-MA5u_ngRa;kMA?UsGC~ipmZBA(N(faYhqR!7WwRZf6xv0)w{)tJzTWcJ~p5EaYr)1%4ENt8$J(_H9V95D-4 zp*(D(fz!f0Mp)qI`UAX+ zst5{E*^Y->MMgU)mW^%N2FQqHi3!3pP7F=Y2drG(H0c;pn>|O?L{u3noAuK$Elvxk z&XGvC&Z??gKqO0$EeVfVX35^FTfq6h>IkY(nQU=4oaD~wtk{DlQ}<701`rO8%^oI&}rBB1h0k`4(FB9&0lc_+gDgf?bZl}d-z zA`BuQoPs_L<6G#wF5iak#rN75N2o?sq8B!uvbHQdN$yc(RH)Ge!SaC2$jMu{xH);c z`te`uzbN~}bJpQy4rRkeyx zl2iLsP(h|F$H5*@$bxj;`A`k6{Ml0+F)LW91@i`TPq6;T0z>GX zv^4LD+<%10DQLau+U8Dc*-d;?SA9ekuTtn@fouU2;e8=z;D9Mdk3qlz3X+i0zW9_D zB7e;mj=_~g%wksRaz97j9pOsW{tys+H4c_!5IBh3z`_-`Cv!Mf^x%5js)$*@N+AoX zcH~Ss++<>Wzz9nbE$3-F)>w$@jX#Uc=9);K1i2Zuwz3k6$zj?6+jxBmx=^h@SJ9A&F7&)&i3A39 z2%|*)`jSX*MFQho+67pNNr38V$>NYA)C7pM8sgSzU6MMKC+wzLjDosIk=h6a7aqX$ z%#l<=O;$7nqkv$H!v4p3k?S)8Ci;|86aGMW|=+ z$>6NOFM(?UgZ%IMSNmK0?)MGx{@1&~+sX5gXNLQTdzU-M^_;81_}SQN^mRV(tZ@9~ z*n#~2Q}*+1M{PT7r|Qq@=V`xayZ;Y9e^nGUvTT)56|*=Kh!oLjLsydljKctVnjn(s zVIyW?=Y8HKse7O-igH!F%Ip7&-Sgq zuquj5S#vYCGcco6cXJ&iQ%_S6*e|QVyU>F881||1x=}BytVD;IfJSf_r^c_Yz7a?9 zVt`;?!QadF%x@ys{}MSEyB5q9`%i5I6|yulDJPw3(y#@NrIV!zqkt?b0by52Z3C38 z#e9||)B3gKM^3Mbs47`XIkv~V5Ut2Ha3YXGwMu0$h_o;)g?w$JmE#)gT^!gy$Ws+D zOJfnf(_Z2oz-*w}G?+cWKZPUNAOVP!JK<%Sm6Z01DR3Ds?^qt8%2=|oaTzq_YOsh{ zjgYm_t;&@Y5T0>(d23KAom$J&=wEm@Ga^(MODLk6v}>>m^9O8G=p#l+0tCYmyTcxH zwF8}!%D+oaL3ze+-KrvH5iGIfGnfqT3ncJm1qgldE)pP$RKs_e{vQi0x%4%vl5VJp zn5D1eoGM=jCx##lwatnk7ql=3Q6Wfv=>wKXZE+U6BsCG#!ZO2PDcf0cK&17^yn|J0 z*5U&MM*)%t#}=OXn5Cr}(z%bNBBH8cK?8Q}ODu&g!8hf|A+)PW)(nEWqVP!gyf;!M zg2jEP7A>tVqAFog7h>0+mJPuKg0%p%9S4=jKT^gB)CW+w`t%m*-6isYnz9Jh#S(F& z_*VNT(*-F;q;nv2AY{m9ft(f)&JbLbn|3_4bk4M!rIRv(idbeKL`3_^*)gM%>7cX$ zQ3&xap@{;#?apdWu63e--*fw`BUBMfMF+Sa%gM(@79?anO8^zX7Z8L3#YnTYi+;9Z zLT(K`cIoH+A1RNhLRi!&8~S-zN&;~pVaPDpkwuL_$WY;!kg$#JP3@f4zk+KBFWOTb zQ6;c2i0xdh0X_jwg2-SgQU)4}m>^jsuW*E=+v4W)%?ArhB4!0F`J@p{oA83L47Q^j zBUxPPTyk<(Bs1(Qk5V7bturGifMtB6w;8XbmnK-dX8NG}K~eI6>F|`M4f1sdiC!1Wt`m zl`Cm4N*L&vpH&IPI8e`^f}kddYFuHx=V+H*-W-pwy_;_qu87nq9xrhj)MSY_=Trpr zIb0F5JV#OyET6);+I4MGmAvaa&;^>$lrh^AyVWzBpeuA5+za?1a7C^fjn*By~${6LucG({8*dH zNTr+`b&?6jMTk@B6`3f!M*&ev9Y>ej?{y3Me#L%#Cd(Bpjm%OfN0&;xhFQUO;1Ko| zsUMO8!~$Yp5!4kfT+lr!WoM+C{CJ=NVb9hdE&pVQyPThwbLaC0-l<6rbYskFBD4b8E3&c*){u&*Pp!Wz0%0}iH#WCn2Pftv{LL8x8md{z}XPfbtJ<)8VjK}BTBkMv~?&(rzrXu+sW`ReN2kwgeSrYlmrZT)+iZ0@;Gy2=$FXbW>=K z{RAs2D(7kRw@v;@{Qo(0|6kAGbHVb!G1UK`=6}#X-uI<%v#-1N@7{@??>yT*r@J3= z4|o07wb<3cxXYO0{KBXmkEo|GzqF7RXW! zh-#g|rf>rp)3Zvk(muIx4JR1RKD|Q|PDzqin?K}hTr6}?b=0hqg)yYxB3OY60l~qs zSGp0nV-REk{p6M|wzATqhp2qw@tUYvBugvZg_S_Zu)5idB>byr1%n_e$?DG0Hg~qN z)T-AhmT-7V6jidy5;lvY*|G-fL*=U|6eTKQ0)R^3F6FP8+GL@>c=)5~lKgj~#nC_O zW1alnT3FLWT*URzlz+}lAn_;*eBU-V^W@9uTeX*zL{)t(>Iog3m6JdL+bGnCLu4!< zL?22OQrwDc|oVPS3t90Pf>0BFE6{XTx ziD)|##v-IK6b3nkB#oK22t>{Z)b7Z4tZ@9w3+R^rjGAc2#If19WRXaQsRs0k04*u3 z1PGf$<}a`GYD>qqY$WfgG-}qwqOhB*q<|qVhK3BIn2N#>gNWVf+-hG=D)^UBfy4Pz ztD>qBmdq4wKVF;dJ8E!-|EZ!YCI|;cIEXroz&hZ0P&DKg&W+?$BM{LV1;Dh}-ejz~p6u?w zUpzIc%3@*DJ8hHYc(^Qtm%DTf00(ZA7Is36-5~=i$U~~3C<-P z38nz`nl%L+p~PM&&{^nxw281vqC_${^?G)3l&W9}6-Zjz=(c*G7$s9|egVs7Arsg8 z6Urg&v%+(mtn`;H%vmG%TSd(>So)sFyK>SR zRaDi&GBr`Ehcq4=EI=tFT3BdVUs54Obx~CrOV^p&PT|*w+fMp| zv7$bMARwF_g6UZWBPO=U1**TNBd$*Wv^q-FvVsQgAkW~hAc{vJyaWg;p8|U$Z%)x)S+I|?f1i{_sXmq%A3hf~Kta?a z?uBCmTT&3B5eUeG(!{3`uB=ZglXsoH4R^O?Er?QKEFBg~ntEa`ip=pBmw*B~oW>xW z6e6wRo-Hdat{8-`;}p)QjG{o6gDe%Ugjq~MTex5FEmSm$#2arBzD;4y)mE^RcO`$x zZ+bMSc82B%(ve&lAznUN8^p8;5Cjh-W>HD2WCC;JjR@Y3$t#NnlvG19>Kn`i84tw3 zuY>0ftkAKUAXo!1JF@cnU7RB3dk^8dwwY1C+5n6g4<=-bh5EU`_7Gl*VXy~)z(8a{ zwVjxn5pxdhqU}Gcqdv6_v_XE4B}jF#s^Xxk3QH&uWE}#Ah1pM9Qj~dp_|oC(s8^9O zJfvl^89E@gjqv6~3D_TlNCtrz^0ZwqTEY9aX`KD7ih9(s!Ni{+d*V6}#HN4>)|AYY zum>po43i*h&?8nBn(LEpvZxyiQ6Q`mX_o(DC3r&6E}Z@XAM%~#CWuUBLXdV%8%xKx z>0Byn7+n)}$y%^zZ00wrp)(N!h4jQ0l$>J_1U4CK;nJ~|(jUum>HhKA6;VSiMc>-} zO}3S#0+l#0wl{fO6s!hqLk|^=s7W;lx8^ziwoF;nsaB!hQA!=L6GMjwDT7%_LnJ_u z3y^l+LzVw{2@}ZS~vr(b`wqCI74cpCYh0YL?#)ffoEkj+rY1L5Dyam+;}_0b=Kfk|^0d zKW(De|6+Gl>|`1#iK5(A#j#;1D0_~%7Ier0TueNqf$>Z-T)XVwNj2YuC+6X+G4*9p zRapxk(WxJ2aw7_XFlDPw_5Ugmv^V_3qLc1RjRI@0<7i`5)U2qbOzhXP2QJyfDS!mc z_Fn-(x6(3Wt%U3r>r@VZePtB&v}|SL{;h3ThXpw?Cf!N(!`hxpEs5!M*R`du$X7*G;VgN$=FJ(9$y}#^LuWMy8WRwd4-B@V z^qVc<;k*V!Hj7(VM^QIR_@*0ld~!4tEGK+ZDXa2G7)1UWG(@4c^C~N{ZTuWkr(aHu zQXMU=1oqZD%Tl0MkQNS#3aT1`V1`hWe^}f8ZgVJKa~od+R24O=W#K&A2n~>tFw5c8 za)`_nn*xGiMM3SJw_7-^iiVG7A6eD2I%*cqQh2sI7J}Ts_x||XT#!=iS2Y45*OZ&< zTHj;~Nqmptx7~^R3Qn9AMdd6Tw`KfsC2$9D0Jln_42kF>Kos<%3cIyd%2dXTMk=w@ z%F-yRXxW*fKn;Dg9BF$H6+&sS4Wa&NAptR-*z_6yQ&Q1b_ZEG%@Y1@dSxZZ`3D;pR z&@A*B_pl+=&?uY@5NMbZ05tnVfJi}Nt$c06=H_PJvj0H@;*XU?&8k?) zsc~`=CT>v-#tD5Kj(}jP0JnKv?oTSFW_}0f^B%ZB@ClLIDA+rxqT`vaPyM7 z6OkS5R2|Kgg^=ZS=)?}tOJFdl5KKwM0tpcB2?lm{juwB}f_%Do61e+FyXny!C2_~N z%&W6e69Q9(B5$VminR#@9|ks9&cG{@#Yg(=#3zPMxV1c*tMB&ok27Qh0{c_4(jB*AcMU$R?CpuqWmQLe zB?#d!Hw&U*2PV9V%rOy?jSQ^iYFC_Kf#lZN*qEq^o~iakv4!#26X*^650ZnT#i5Cq zS0HdH6i8md`<6u5ym1@^E|2z9G>4mA2xHJw?2!XKATTccPk_L%uq%qv_M|G2Pn1MJ zRkVlN53x{%&>mYakX&Rt(8+=J#Gs*20~RWrY~kPP3~AF2fg2JZ|M2C@QO104hH0%-wvK=c3P zKkWa^|AGH4|I7a8{7;}N;7CecOB+ zeJg#7d~Wzh-3l0o+^KcxW@-~1-0N$?&0qK?tFJI zcW3v>?l$hA+i?BvI_ml+xGdN)__FJe>pj=MUH@`D<9f_>zw36_KG*fGD?%@ao(LWY zyzJWPy3n=8wZt{wRpmO*HN`dFH9R!Z)!&ux>gDR}I@#666~u*YzX!JjYXa+nPZ&py zZ;V66d&a**n(-2Dad_N#z_=s0&e(6EGkbbo!ij$FHHcLhir;#}UVuj*lJxalGdEhvP}dBaVCU^@(Wc zr{KiETaIfTm*FPLO^#KL#g4g-S&mZ2-HDX!}6&8Ui+=~nEhJ&W%liX5A2)ltL%&IbM3S2rS{48 zvG$>XJ8hHwFQ7=#W;@5Z*mf%a->Jd*Jf7?MRgG7vv09BMs8N&Axmu0X?1z|#eT9tr zTRfU8Om+>EXJfC7&hKS(T*>1({+NvJ-D>PBqjRy0_E9{Z9XwOUz-Kc0C(7uXE~Do@ z8QoiCH2yB5eUFT`?P^4z5AQoE*h$8~EE)YnWpq9-qvIbuPV_yhM!$@nx7DcNo|*FO z?kb~ef{aeO`POEes4q594r-g2#*Y(xJ7n~JqDDF937&7{+0bQl+@r<~GTLubqf16x zZyB{1kK+SWb!xMX_Y1nldk)Am*oWZb+%o?-msg%`b7a)z*v5Jn^7B~ZTQ%MNvAEn@JPfKx{Q#cG@_qx%jSor0|~_IKpj{;C@5WV8u2jnR?5$7_z!e&=zt zSGFOGB3-b>=RdDebbqr87nK=4xJZ_Dr^k0GO5<}Wg2A4QH40=NBBjWtYGMHBe3FxX8-?+h6o zLJozFv-r8dCxW}cBV<$H7IYN2*UIl5^121u?fjhYzgb3KKt_kKZoc*%Kj#GtW%M3Z z<8v~4gyr)*7pmu=jK+&HI_~7x&J)fLyXDlw?sEQh*gaB4o4jUN6Tz7q7|ifopI|4~ zcb+^u2gvC7A2oK7(KeIE9KRe#j!&pA$Ni+hjg@K?uFP?s&d=F_k7RTURc5=(6HunRY?rnfCDl*PrF%KU1dd8F-w*Jp&KO z=>1qmk5EHTLzu3oAu*uVz83*~d<1*gkv1^7LQjd%m z7rTsW_IM?k4l$)=A=*N=P~I6dF-Dq@0pb@yq1$L)=QT( zk+4~5B0d1Tro9tUR%uTRWe#n1gS7x366vxdhpnNxWjohf)5lR287 z`)20zI3!JE+xRq*O~cX@Ri}w8=$0n?K0EXS|9#hxFio$}2mBlk{hP-@Avxc1p$+_8 z5R&yrgl2@cXlXXkQ)rjwww)R(;@?jU_2jWAbP|%|?QEc1jA=HLo;rnW{QEH>q0j7~ z2#w*v_ZglYlye;re1@O1g17NFAb1&%&f>i>JXpMd80-F2y}m+`bQ-T9sKLT3-hD~>r1 zyZtJA*!H4rk*&S{us#v_|7FQj{R^Y$zvcW4F6w_+LLDd=L^1{;dFsw;3_>QJs$c!v zT6m{IYq}fgzF=7tjkvrfimcit!YhbNQNY!#;--iTf4T?(<*dBbsiq%BQ+D%_FRP=} zhD$NT3YS6QR?LR*7qVuQ8J4;l6NES=72V}&8$WN7JxP)pT=g(_e3nMd?px~m9*LzW zJIh586c>^ZvI(MG6xG4ZN&X4J%{{pqsW_?{ZxO_|QQJSJLXeANx73x01%&gVP+q8ga}&3J3aOu= zvn!+2XiEtVWXLixF%FtPLPiw|m|Mlv1tdY~9Oo_zak^ZJqz+hK9<5XBKrGRui2O;F z8=}s1141JZkrt&mN^VRw>bPh-RZy2!N6j`{bj#?k%amLVhSR1fDn+BQuz*mBN*RJP z*I0=RFS?d1sw<;r%Pl^CcpqjZG;`P+XGBr@rXVW8$nkt+;n>n}k=cLWvsXmTzS|PS zQ4eBP;748`_`&N-Pl6AVZo zq|~S&m8yI$9?=|mt=UDzD#p~P>bM0*IJxQ449pY#BcnkWF+pHB%1jh#2kM*f_Fpoi zmvp)Ele<laV}J5+!%y-IbzvGMc-N~DVbmH!B=HES4Gio zs}ketJLsSx`fdof4KtH4S;>t(9Cog0dKrEa`80ea$IWY^HHkerxttN1C$k5ZQfDOM zO%NrcuzvQUHI~u};B*a83eR*qU(5I5d zmq*RMTdMJDYi@w*WGLG!HMG1Z5|oU(tO9M@^3;mFcBa+huH=z5QR=>xS{y?U$Rc1! z5CfP3>XC>6i!cau25B5F(l+Z&=qw42R#%~Ry5P35sM&x^7jiGhykJRW7*MlDB~nr~ zX@V#tMWSc&!>Mg&Nx@b4@9L7M*@cU`Y5g9ofd#PB})v> z<6QIT+Nf&3c){pVe_*Swns8 zqK-H+3=NAqHq2dIUt3WLNOk3+#bt{IuD_S`OP3i@)sTzQc{`UklV&HK*hsmXAP9b3 zfq?|!y7QCfpI4T1Khwgf*^!G+yKpBwXEf=mXVeN(&leva2}mpn#6zNmwe>f&C;_?c z@Uyq00<=78_U2Me#$~bqh>Lrqsgqpl4Nyh}5Y7+S31?5(m10fCnjkK~xb)vOQL{rA z;>||n%NwKGd5BHu6V<)}IRp^#D>8zixFsA-ta%atp#oj?=-Oa^@l}J6q*x8q#zp|! zEeQ~rM0kOLgRSt+oa3x$DxzkOE-n|)mta%eNkI)Lzzf)v)IkEoq@GjQYDcQ-Y|^1G zU~PO>9W7D9fGPm{V@k*|a3rcuIQgi0&jbXC!_`yThJRbg-*3--W!guxKd6jO$4X*7 z*-CjKDW1cWgeJlfsDO*-NDkmQ3;TDnuuk2W`7_4$ezzn#O(`H;KX;Q%%6CVAoRPNY zRtKUs_4x`cjUIt@NqHzwF83&pPE}fmz9ltJrWc06HdKiSre_e3Pu; zT*x#tTCBE!OG<9V8X_XOG>!@=lp#ZMj>(SbVgA9?q~?eNtUarvQxrDvVTtxwg7d!6 z$vAp8w-X?&ii0CropsET@@zN(_j~=)w>EmNqA;>O&gR`IYzm)b1YgJhGNAmSTP zalUr>s}{I%*BSRMe@s6}{(o1kLmP#y+wNuTG};YxZ;9WqHhO{-qCgr^$IpIVY)jh^)pS z2u8A+&!RTwR4(_J^$01)k;O4;{FPcrr~Wf$Wa&$`3)C@c{754wK-9#F@|<9w=A4PC zTVZ%TF{m`A8hz1!>$K7JgE$G0K7tV-6%;g)mNY?Rc~QZycH!YNw3olM`7)T4wgj6&H&%?Q5-|xuR?|Fhz)uedVDly`vz=$vd8EEVSgO{YNPxl5--MV6#0P&sNkQE&@55@CYy zOu^E^ieXK*lf<81;o=a{^cc1AQuKF>D?`o*my1DaU=i@QIyMH8n8#0^7IP$8tIO?1 z4X%^TSxElESO!xN{qn>Ff`>~1uVI5)6n^Npovx#QsU~K&?ZRZ&ALaR>!s$vaI86{- zvZMk*_kt#JM@1|d5DR9L6v!)$QTHwp0mY$eu@q1vSO1YFgb-^4qSzM|*lPPHCgaXa z+_+ZZ(GzQ9s%IC0D2GNnf%S&zAu~A+$e1@lq|bn>LIiItZDq^KLv(jrbLe7(GHsT{8>!-wYMb5|qoq<`w_K=*gSUA4TBF6;L znUNJxaY}O9qdK})p`C8jArl_D3^(0~R;EVq1c)tRb_%tPMHW_Cbt&t%>gbvTR_r!NWMzQC z8iq*$$*I^!?r8)&QM9I{j}@#eT0-crDvqvJE0Lje2UdcNa ztd6c!`Vs7onScsr5wcoH;3q&F5g=O|_j_CG3>bSVeZ2VWDbW>w5-y1{ECpQ&RScy| zu76_?SdkRVl(x;C(BK)k>1J`i>gaNu5Lv*q1E*(u~TxR%XLbMpX!YsobL zO110Wv1ClI&*DhJqL^wM#+Q8z4jV!4K$>zt6zNaGvI!EiEiYTUB(H@=>Do8h*(r~i zJ;da$Q8tt#eB_&PmmGTwCI}ekCZ|q6r0A|ylp&MU|BAckZc zB|{P*#FIg(V8iVGdm9@u|82X19 z{U{u{$b5YXRKt+wusiRF%x&hFaM7@bo1p(MTCd*|EQhLMX3sG7aDRZAg@nNrtQvS` z69oH;+@P%f9t%jR8;TUmkzNa8Xd&huG`RGuWe2$XfIrlL&R85G;{+fa8Y~|8j9JU( zwdl-7-ANv4T}jMrCk7c9m>wYkun05;0umrMijp=0@dqgEHzuWmUWpR) zML8E@DzO$Ir4hO!Q#1i$gPtxH3eRgu)qC5#r?r3MuNPLtsGXQ785aoY|H70&viQBQ ztf^|D5eNc?a8@upBh^*b*?sI{2f1BxfF!<1M8)C+kG^#Xn&gH%A0ae;Ay zPd)8UQCO~doxcT995Y*t$#=USQ-UQit3;y6cuRml0TP@l$ggXThLYX;Cuo=Dd$_T~v>Ts+ejcMroCU?LsI-{t7X? zOWfX=N7=yy2xbpr4~0W6vA`kAFXa5~tQfk884V@a1{KhWKnF*2vW^2pri{k{k<`r6 zt}IFw+Z$iImv6y~seWQOgLd1AeCXsAKr=%7LY9zm1&PLv0$of3dn;Sp@l3MgTGO9P zs>)(nieynIq8-OvFwi*^Nc2j9@J1kV2Ei`b{amSq0ai>H@IbG9b1P!1br=L|xAnq= zU<>wVp+?~GHL|9`kJu8NkOJ+p_fjO-wSVVJc8g=GZP{D84c6YYQ2>rI&HdPGt;T!X6LSsdULIlz<|2K{`)my%ylJViZ7da$9HP%J#fv8uxW_%M1@QtJ? zM}EQP0I|ZQYcI02-5)k*$gMs38~k_g%vfio$`R-z#R};sDRK%?nXz61A<)M$u0s9vtq(e`X||qN2_C}C-!7$bnylDgqdNGKxAPh#e@uk zSVXn~*FRnEwX&z)h?YO@u8*Cj_5`<%FPdTtm4d!}Ig1%4Xtr? zLZ{CT7spOji{k1N{^SHC7?Mp`lw!(KHk|-L)exne{f}npZ3Nq>lVcs#%wu%Bx{?jz z5{4p$%Bi*mX9?L15Z`wNnhO`6q$gpN3a|G`J#3$=dmr)Mt z-i_Xs-bMC%{ogn4qxbd=yc(!)+(-Xs7rp0Q&+Cp=_80LL!L6PbJx_Ta_1x>ZHINhN zj(++^J@ocZoYw@t_YC*+_vAb7_4I1oNAGx1I_ag4-oD*A$NjAParXo6JKX!-H#o*R zqv%nH1UmRXvTt%<>Au*#*}c}i)G;eC-MzqF?LOaK?7Sy%iF=}ZqbNZM zg1e);ojc9#c5AMmT!$So`&{QgoE!aLxIS}z;CjpTGQMm0grjfZi$HJJgRVPW2hhX0 z$G_gc9R0IfT^n30T=lNmu5w4%`LSzypxkw~YqV>ytG6r1)!lWPtAi^8?ZGxjPv?-p z-vcX*UySdKFN}|jckGuLuNp5He>WaB?lx{VBF5FmCH8lnV~nlF24jU$Z_GwF`cuv) zojr_lW4dv+G1?ex^ma@(a*XcAX+{Sl!?_au@qL^}44+|l{!0DvW>>ubX2-jp3;Y*3 zesDa6PIc*Ae-yp!+}-Zz6u8-cHNMXvJ@2J~tI-+134QXn2E2~f9bci_o_g;WIkyEq z3Y_Nn#CfUn7Uv_*|2SWBeu?`Ic6hD}{e({Z&q5yr-tq572mZ^U=R!~5PK7%|2SR&8 zdqTTHTSDs`Lqp3#bs^jl5SkjA7#bPK2-y6uI<|)fh6-^fMAuNqP&?cyaJT=RkSAmd z{u2B?&;ftr#b5p6|K9(F|0Dl9{#X4k`2X&I*nhV_;=kHH)ZQ1u&Zf5uydw~j{<-zH}vxB39gM+<;Il=D1(}Ep>89{HLd*HPHKi`wz`&hh93)XuaRy9 z)-&AadzRrp2Nv=8O~xw>_uIJ#O8XAnMCAx^XPF#8UAC&5q=KXf9KJi>67P7r;N^rS6~U2iVKozBT>6zn>+hxysz<4#B2;XO;9J@UE^ zL#V~!_>aKtf)tcC!G7V`zI(N&nLr(7p+{Ht<{6X8!%l}Zpk4Jn=>e>;{Jb8BalhKfWe#Dt0 z&)Rh~>Ot2a9^IMK)N_=7|Be4Ad3N8<(;o>k_VKJsEZ8TyPO7V=Umm|JuiDL;Kpp@=^QRU;&1)xBT8Ito1d#bVA!P*zbjq>bRFQYB!k7&oV&pmP+pSxaU_~-V^dD+hg;@59!pW(w?w42Xh zrt)ai$mkd@qrL!tcc=ELV;nz!YI{jW?E}E~XotM}`T3CdBpHn@YLwSJg}A&>v}ue3k+{(^U|mt$|&aNvY``(?(d>hDkH(Vx*qJqx!T%dqitApIbX+V|-ns&N~SzolOo zn60(bzD!?&F-^}*7d(HNF2dmFbmk&mOP>+gfb~92AIbmjaQXlmbz6FN;9kM z2}^GsNMm?wk9u}ZlxL%pjJoK_*7_y<+$v48VP z-fNB1m|5dt2jRte2o~|+Z>uI6W`1fgnV>0@e$mnI^ zrKh>&6w(|_U3!}BdH#JUt*;t8%NY1lMjum|9`eX;LY}h)?%pe-aYT)>PRODD-c~Je z?KU2Rq4qKckINVkEC&N?B>%rh3*8vX3%(R=2>Jt2rEv|0HGsawaqc=GRI^J;1cBI>HvyZlYV7t)PS$|Ql(0+wL`9J?RGlphi z4%JR`wMS$FA%BF@P)vnL2ZD5xj1dr`f=K`8<_)+Y#n5A)4)nG0J(V%lDGXKMq~tgU z0;zf&@n;0_C=z9Y5L!j_CR~(xjfIrP%ul&yx+H$<=c@LrDQ+? zgy0&oe&H@FEG#tZUT*Czj+tG=$XeS1n3Xd!R3M5luqv(<5Yo+vts=r2p8ITb!KYOh zqGxzq{frozhB+%kcMaT&nGwU{%5_YG$RqcNn;=d!;JCDH?ULpZcl^3;cjTV8ieuC( zEcte)d}D=zf{2;{-3Xk~32~{5G{GYLm|dh@`g3zxhKmn2po_j+bnt@vNd7T*b22=bWpZT7ISwTC<;-mf=fkm|k z0pakGQ35nP=e*Pc!b{8e3qEBr>I|kOkfR(WOCY$2+$v(Hs&)WNFo@!fh@IvSdnHA# zm|ug&k&!=5iJ>LfOtL?KHGo#`%LK7fdmbgm0Kzgz)KJ0!VaEB(o1>u-yZLLpH8Haf zmsp*Ths)Fx{bTe%%JA!t;Wa&0a3ov0E{V)3WcwI!~ zAtmrJ1I!B4kgOFDvP_(sL#frpZ(7*G_&ex>jXR5DW*ab-Evm8_%#73vZV!QkC0=Cu z1&DG#NNMGk-qajzZTvY~s;<%X%S4N0Ob2;ixoQ9tgS3cfu@Hm67|7^n7ma>2HQpQW z5q%NmZ>2G_-FF;PNsq{zQqG6J6@w(WWbWzY0K%JtS9023oKyl%?m(6BPfm}a&zE>LDgc*gn^H?(XM6`q|{6Qj0Y z!c--w>{HALmc|BHoM4e$)jt3bNCR|15_q)B(tZ|P4#n8@XhqEI`=x>xYA4~c3KEhK zhbR~!$z_75UXThSv>V(hZfO0a)<{>MUl*f}Um-oa_9~`iq)V2cGK;h!6U2OpVu<4H zEh4@8+wdKxy$2S=s12C3B(8Cvg6UaZp=Xm)rch$4!VY={r~{?L#0d8r)1npYGB#HZ zIdxoVOmzcut=&}f+^9((d2zBR8iBA$E~K7&ZBp5=Y6a}n-slyjW?(wQanPJEVJ;C4 zEF>xOPP~)=K@?Gbv(ucU)6dz&pCc`cO@!#7^D1%GU?vjRf8TA~M&%w4RL90Cq)^H{9n%8?EZ|5- z0tw_km>}#2iR_&0llEIlnNNHD>PO%E3;x5phu^1<0#?PwBo=b$%dk+QI1MjP6&?u?R5s<80qT3$f+4V!3mZ--j*V6e zQ576@FhL)|7~`@{`fxK;h522br^wpV}rzib8GsV!D*$#@g6OwF;>nvX8hInZ8HIbfmhxurM`B!Pp?FqwO&) ziRq2sTu#-GrLhs}tjO+I#cM$$z!fH~2|iKcjEmP`Mzcg)k=*WB!F@OyZ2z#8+3fmNdN1vHO!mM3R}wdya8ZgnO*Vp>I5Hj{J4B8m&29ERrjsE7>4+3m|i3z_d1&e$pV7oLcRqS)%G4v%?R9Cg-@p!9;=C) zeYe!&`xX`vLkfh;Rp6}68iPoU>Ml@M>g8AI{T^z_`s#1han*whtK8tiCG5Q?=dv<Elm zYQ7~kiR&Hjz)E;w=0IxhQ~iA=2nPl^lN}!SOp2U8@XDq5Z&YQRdT>csmbvU?TtaHe z?P*v5id}pq1%e(RS$2}!0#2q_9Z*5(%3m_hD2Urr4Q+ehl2uNZLH@CzR11&v?I~3~UZnYhl9Q>_Ja6hI7 z1EGZo+ffRdL1dIsW6|u+Ej+_be!ArFZ}a2m%~cJwgJpLpnPj~{+J`s^>m@*RQH=Ip zOH9uJZ_))NBW|yX8)}y@4){~Cm|vu!g)6e})B}J^5v_cNQ*;qf?i; zYq^V?q=XWHILz3DH~@lTg(fO&dsPchv(sttxu4r#9yeQdDXWuc7^L7G;SU6Z&B1wq z3C9KroXG4PZO4|>NOBEA2W>xDS`}A~yI^TU{~Kn6kl-Fya23^RCyaM#(jgt2J!^vn zo^JSfE~-(xm&R4sE^5vl=EbLs7YHa+=&KLJD(vAIL7Y78#y^q?+MQSNwSiS}wCb{# zfH1T6oS+?BkltyL0~O1AMz(ZWq}fw^kyYk8K>4=k%2a{<>k5*Af<^p3jd0& zE{NW6QEirm^ICa1-{n;kQ=Pl)3)3|Oz%wjtIH(|P>iB95f~G~FC;!YhEy;?Cov7vg zc2;$)QR}YuCgvr)K-7`X2w#}{oJ|m|W_?YYo$j9-)=YnDqwNBKI1Ob+U z0g<=-c#8UP>zMNvtlNE6RSXTiis+o?O(A%HSa3iP79kvgLj?p!4~9$88DlLFU0o|S z@9JXc=T&h8hh8oR$2UbluF_2rWIfUjI0mTd`$WBytBzALPd`Vh8Zek_hH zQ7h4hM9ItJm8i!CiXAAESQgne0D&ec2N3wEIWKSWFRSq>&aCoSgE}v=^(hnwr3c#> zTm;PndLYZp1ktuIbD_5TrquMpwv8+3UcBXgBf_ zzzJRO2?W9UBEy7&%hGU93(WBK4v^k9mO?wC=%yp_q**iTm@^az=}Xy9^ayj6`{Jxt1&@%3FNYJ zv$e~UU%<3()BxP|)2BK%U+t&@ADE^tII*^%P)V*yv3~-@Vl^+<*Yu|8Bw5QzW9XZM zQzid@l@^MIdIz5gPDbVa?m*c8hJT^o@LlIC^#04c#2fJ3;Th(B-`(JDg9Idh}@P|N75obVAvFkx$ zZ=3=hu?Zq)o>DF=ZfQ<>tmuJz6+W9%5vP_|@)L0nxosS#L8=+zjyaoTJ4wI<2$~*^ zhq+m2rpnp%x!gE6C2qFH;{D4oBWRtQR>{$UwpHXWAQ4donXfdV{=YOUPyCGTh3_^q zPVKNHmmo5ziw%?w4+>@;rvtdJI{{+8%FfP;r6w^d&%*U$hwrG0t6o@`#7@4Yiy4*T za|j2J^&tBJ1p)*qMdpytN=9(<>=!N^^xNGfakRs-Q-Ql+@8L%F+@kLn?7%R9lS1SGY%~Vp7)kG(^h;81 z>2?_f5?YnVsX3PD4dJt+vJV86abAe`f$<3U*aw41U&ArV)2@6rMXXHB;wVB@oSI}w z_L8$bpQj|L4i5wwR0tQccYqZE!SRA%FY0%_1+2{eH}dI&Cf3Ef|H(b1pkd&^BZatP z^O;j-CJ5X^bw)^TzStt}ndIYJ6Q{({Fw5vc`4wFW%vh%k01N`0AqfQmfi}RyWZ;#* z)SS2(){4tn7R0+M3PbvyTp!E=^9K3~FAj1dDVYFKX%wP9p^sa%m6A$);N-#T(zxoI z<%H(bm|wIP1OX+N5()rB4G@`-;Lu6Qzb(^nKD(yXarDkAmT@*xE7s(-AAQwuHGX}CLLAXb7vBc|v0B`AS#A|MWvz`TY)+wo?~ z0_{={UB_7+Z`?lnlWYh&8D~QY0C5#rMUePLK8WsKIQOw8s83?9)!)jomy)<@pQZa( zwVhZAh$lKGEMR7iAvFTg#Si(eL&-u&19#&i>*LR26f3V}+IND@6Ogh9>g-L;Y5Hw%q3-f}T*agHk8BDv}0{_w` z>-@zwYCVj?=6kE-Xp&_ahKnnY@#zq4!F9rkBlxML3xmj>zz;sqFxZN7JEoKZ+5ak! zpMs+zK?W0mFQWk&NqUBZpX#d?3IqW_$5*f;MQK@m4`npYtBRkj79nX#7tJ#p(ly}7 zkEv*R0t9=BqAR@?T8OU9>Mh@DiJznv!fo^V4~%BWV8T4CRi%+7-@uqB@f99#?OK8L zI+=V=mc>t0OTm2AAHq_?e~`@508Y%G0Ksp)a=(mLEG@H|9Yb|euAvQkyZ+Y zf@%?b5Z_Z}flUyR27A00y_k$mwz~LMt?BU&O3>pPXUbvVb)j2ewZet~R+LcT0u$1l zL}+t&B%6FhS4KruynO=a4i4k8kq0$GYD&?tfS^V|1{D69bgcyqx#o3_L>9-}DP9@k z(yOrx7Q#r!QV4*Iu0|lS%W!x1o@T4@CXS4vPIcjecw20N#0Uy-Y;rz`X2DM3OqE$E zWrC>QImb1sMf$4$F5K^W|LBGBHX={#%Uz?fAS57u5LciG3QXe!h~*>&*0Z%;omv>+ z82t^1zDt!MkE$6Lwfpp2Jd zP6VMurolFXuq2t50D*-d26E5-Dyh{s=uXC!`2P)*|IZKpGq@<&I&gDf5bpoq?9cSw z=9}z2jC%k&d+x&he_y)S^Zox5jZckL#%bslpX4~`Snp_Wzr#M!_Py;Q+Zp;J`Z?Mc zT0;^~zdmjj%_6(0Uy2Qgv>h4@-`KLExdnl*Grp*dt9n}4xY{L{m8nroPm1y@I}r8( zTcZ?i%W~dnL1^3MlDg`+Sx$>cF2^xo*}!eWDN=@BqM!+o7$OBlA6eN^&E440w$){E zl-IJdqj+n3#ureAuaS^B2vSe@q6C5xA~R~~{i(_93$LXrgh^F#v%VIYb*aSviWAU3`zo$?dJ$^zmp3Dk~+b9*HH zEJgWmh@7M!DveXAEg76Ba_NhiIC4S}4W=@6b2`add}C4wFRX39FDbijsDA?YxjkJS zS7o)Z&>Z~T5rj){g9(>YkoqU7ZGvDolMk3RuDunUukOe>iOM*Y){+p1?J<;(glsgF z(?FC#qmgt2Is`+Z|yxLHGs+8Jsi1E1q)xSkapX?{41L9pSl zg}l6zsrx7O+Oz3npusvXt&O8{mN!9F==8>HEK5F;j9lW$1PGBQES05gZf^-Qt6t?e zd3F5HI$EjN#Kf>?_!GPcy_*aJ5QiGzist4nvvimFMaSB}WqZFSZr0Hfxh*ocAQzee zHD<02HXCy*$U7%Ib%CW#Z44cqoRg+cjjKvprh@yi6!bE540whEsa(b{Ag&EUbyXO_ z+eyt)!Oq*xr5n2I<7VwFvUxUD8iu$g>q*3v)R(8Q8iBwxDA>y%F{y>NQ~f=&&?Dq5 zkE^m;c)K?3X6y$f3bj|HwNL5~?5 zfq-bzYlVZiw~)a4&VrI@ySX?%MC}aSZTuxEXcUA~U~WKXQnUe!njrGu@RPgd)aK4? zdqUY$&{5m_tOav_QfHK@-Bn!^A1G8sy-s~FD+^#Mq~$O< z7y6kXoDh(e9oDXS*OK_!w16%iTvQStps+!o$_QaCikYe}CX*3E6l{V(zA*lB@)v%R zEW)v6`=jrp`>Z(LU!52N4~H=mj0uv3?5#r^6G8+eF11zK!+bGV+gnQRrUD&9|y z2-dN)El)!RDuReG=*V|e2j`4<-vm(^+|rE=fJfMRq->1X zL;@m4L=YlJ+xKBoqT0~oP28)zsyg0Bv0$Z3KOk=n-xRhT#0OZA&L0vOz6HBr(7!oF z=e=nO)kFJm(n9M0@6kd#Lfzp1R|K7beSuN_kNp?bKICC72J1Xsm?N`{l+n%Bscl`1;>HNxc96>#u9}p~jXCQZNEX zu2|$Lj#lwE6NJM@YAh$a?o3OkvwC)4B=@G(?L&1f$CDAsA_EmB7tWoFUMcoMqR<3k zC5jp3YVpbzg|a5a@I{=TTUYEuoh|RK1h!WI3qYEHh~W=G3}P#U`veFUINX;4&wx~A z`~bB6p^TtWF(t47e?bgY?1hLyK&+2Z_lo4gMXRmR-FMuQUA&JK!i^ovtZ@C%=FG=sA0dSt*-;-ii)vv$@@FSl_mSkn`$G2-c{K*{ zF79cec6TkKiz&jzakIP@nNh9<1Jgiw0YBVMV)o+;2-HWtWu5jkp|-!ckQG_@uHT*s zb#au_DhB1|C~zksTAdS{^a29Uf);@IA}zABWehrn42qMh;;5_TC86-iwC4~T6|q6P zD|Z;q3Iq_AWIc@UJVmXzl9N^?Bc} z!;kR&6*Y0Hs3qr^YEQ41_lK3o)xvO8%uoV@$c4q0aoWu5R)la>BLa#K4G-}+MK(y}-TXc_d{)qFz z+XF8g52M@*2{sc%%R;GTSGV*dK1&>2GgB3((pq6r`)OE!B`1ZD;O2v#N!>L9k?~(( z>)eFMl5oyRWRRbmRTEd$v{YoIzAZ}3k8g_+;ey1l5)gwQp>SB_S#2J&qbR<=g^Ol4@1g*?AwGhWLp+I`OPC-G z0@^r=qFueif{(Ut6;%0bdum!VpyUGIY|yNp)5W(d*Lh#5zS>;95I;{pQHAK5*m$U5r_eH z52`i5Rp27!noFRWB?ScDfh$wBNH6Je8Pc5J7sqF-{lG@xZUJZ^o)ep)FtbujVHHS$ zK*|meoZ5t!{vxpJ-Y4s@wjy4u1a^r_+bHi3O9KWd3~G}6jX>BEWzNFd=9J%m?AMf! zdvSWa2CI-L80T7!8LKlA7I49@|Tp!5uKkuLGPxsyO|Ju9q=qjsg|J|GMPT!XR52Q*# z#8$xSy-5fOiUb0dL8UklW=J5(MX-=CgMbWz3=Tk@5GT}galnGwSNl{PaHy>=1+`9y z)`~o5oQG#^f4{xI?;2d|k^AMJx7J%>vDRMS7SFk7pMCb(^N9=yzZ`B1=Y_U}O7j1j z-;m!XZ+l*4?g7OA3v;&R^v!+&`Tsvd{C`mJVDQplx4h?HO!#Z`#Op_AYzcEgBue*CaCJ%Lk|VZ z1X8ECbVU7OBBmMP+~0j9(jb@C4Fcv&!l+n@=!iHyOwiyJ>Y*TlsLdD8*$ZaLJXEu$ zmU(DzRvRSLitY}!T!mqw;^4x;l5n45COx`}g1n>Gt7vXKL$(}(mPAvZud6f2rS%LE ziU+;@u{8`gAPu+)iq|+;KQXbu5838_1nv*_tun}abp(1eidA&X`7u|&WvCO-_%sGXA_@u&74MZlsM-7GjEm1W zrANo3>W!Bpi~A;kK=Z$#1dpThyvL-e8+&F`=;g{R0s~QN*d9+r%4> z@u9*l{!p5TCJj<(IT3919LZpT)WyKCX{c0qYa$Ap1C1Srr_8!(jvw-E7=}}M-XH1= zb7)l>=Q0f#M_Zl)#`%D$Bsuzu0`+TsBaZrGPJa{5E)lbl0mlZt|?0etK;2qnRu#G8pQT8&VV$gMCdZGdfeoA!AnRj>BT; zofxONx@s_syC+oJ}B)uQEuUb;U|BkfT3X zE_(54X@@4Pfrb+a2xAPxzjGuRzi1g8&6D@mnIAJ}a{^CcWU{T4m%>KIdEhz!%tXL@*}hl;n*-LxUs6HTpdiwg28ZMSj?}{|6NI zHzy7AX!R&&1%~Obhi^<&e0YJyyNLw}3ag>rT5P4xIE+@!C@jlf(FAbA1X@mofYUC_ zBrZVCgyRjt&TGX(frVkd6eX;SS(25x#&Sj@V-LG)jF&gdT!JTcVCep+{*d67Bz$2m zL8&AwF8kS>3{{q~k3g**@wcQw&a6wiTx$m=0gK^Ygt{))v6%cGik=$C7KCLjU!-GK zkM{T1nYONQLCwKim`Owy%YK+rqK-yIL5+t}NteUo34YjN=q`Cwa5}4|x;LkTo$xmDh`R2d zY8>}>BRms43PXX1Ako!!BHf#yKrq@)y{yan`C{QE-)YR>*qBQ%uIx&Ti$e&)i+AlZ`k(A!^f|` zxW?qWKOPNQrAs1sN*r$(8*>y&du7E z)i<~|m<)ykTLS~F{nq^E?f=P!X|lX<*qvqz2!)f6oq#F>u?dxy)`g;=%D{69Y^k-3 zB}ZQLx68~Kv*y$`)^<66&aBxpx>V2ZGI!4OnR8}W&#tXlZ%Oyr$p$I1qHNH2`38m* zOM@AvP%EJKt1laDO_ZoN#buqgXDTwMUWFuqrC*LU$dGl_%OJ~7L}O+uJhUaSfm#93 zn2ExTAU@I#&8nAWf0laxTEi?^3L6-gqjYo}9K=o`jB#Vq!pDS)f+K<1zjezqKbRFO z#vw+tcf(lYrO47mCi%g-G?Qpya3a^kr6|`ic}BlxlTYq@#I4}i%-TjGH41|3XfX-Z-31r~|NQp~})p(i8>afw7D1Bfcy> zdSAEXU5Bo%Gf0t@)gW+AwV7uF(uJ;CFtRW}8U;-NlDWA1m3}rQ@eud_)RBg1u{@es zf_pr)n-P>ENE5C=CaB} zvO!44q~46(fMC%IZ#T50DC`-I3dP!z)Bv|3XBY9!*PI4 zN&jd1914oc3)JK?Yvbm_hHgfEUHXv}D^(lj#!}|PF(QEzwIgt;gI3V%)D2E+1L;~Y z$gz%+`uj5Ti>w-hCu$ylrp|atu_%ZNW@9<3j(N)w8`(qYmLz)3SnXr$=MQ1yv{6$G z@?(W=R9cv5hQJ|t+3STsv7#smFT#U`Ao6W>nQAQ?URZ@hrf0_*Cd-m$O!9pUW8!Al znr{pUlbDy6qOkGcI;hLNr&Km~qg=e@j*fX>#~O{BA&WSFj&&P`!j2&A4_3Y?bCN=w zMq!5_F^iAc-NGv+`3y31s;qj0v{|7XW8xPHndl4yFGGdl64EF&3&GyS)|RzC_UNK} z#ECK9^l@iGOMdUcbcwrA2!OlDaF7Fn0s}(tC@!_uE^JomTYJJW4No0&VT~zwHwU`E z2x=TbkWaFV=;>3%RVhmO53gm}#Az97$FrZ>hO&;8ji$_G8Qj6(*3;pKIs5ZV|rm^Ui>(s?J=;L z6=J*~>LxBg4@L0JQ|C;?YwshRp-!c9s?*Ck$@wMTdGF}7 zaUxE({ZIQV`xE=W?Z4Zv*)Q17*iYEI>_6H&?Az_F_I38}>`U!c_ELMk-GKMkC)?xf z^X#+iU)vRSIbLAzX?L?b+3jt|&bI^TMEH5^!`Ok?{@Ba0=VMRD9*aE?yDzpK^$9n| zu0fZ=wXqek3uAL)zm3($CdEd_&W;U>4UF}TmBvnvoe(<~FSxgjMPoV9AEIAJ4@KX@ zTl23+UyME*eKPuR^#171=pE5pqFbU@MlVCJgNvdIqK(mM(J9gK(UH;N(ZSLF(LT}Q z=!wzeqd$*!h!#Xc(O~3%B40#4io6?nGxADgPvkF=-H`_)e~8>2NkwjoTpQUKSr=Iu zSsa-gnGs1wsv~0}=S0qk42ql<=^cqjx<|T3j*hg8#3H%je}%sZe;R&2{EzUy@Jr~h z_*D3j@Sno>gzpUB8ooY!RrvDoCE;b^h2dG@>EYV&gm6{(H{l`S0pU}^CE;I%e-Z8+ zJ}O)o4u`Ws--o^oeH=O%dMos5Xm9ARp~piHh5i%W8n=aR4sC|lW_|OXnAU&MN`Y1i zv{Imz0<9EirNI9s1#+_j!Q8g16f9M+NWokMGZjo!aHfL(3i>H1SI}EQk%EMRlN5AU z&{;t{1#J{K3i1^M6j%bN`f{s^s7!mt%6Ak&QqY|X>+=M4N_34K#A3+uYQ#(=%(N}1zi;= zmD>DVzm8VWQ9)Y;1q#9f3jd|xO9h`QIH2GS1^X1dsNi`8Pb+v*!4nF0DR@AElBe)0 z{ra7PjS7@zg)8;zLIv{`%u~>yV48xY0;OQ#MEx42;2Z_R6`ZADn1WLjoUGtP1w9lf z%?g!Rg-WbKC03zQsZbfLFh~D~ECB_|QU&kp*Lw;MDtJwSGFX9fR>59HD(MQI(=TPW zf(P|$hl1ZL*skC%1_DRw~)oD)MRts}$&> zZRI6f7i}x|*yHq{oULG(f}siqD;TI?fP!)baRn;;?VkG8T|rl^|G&V3jo;J0-yR;@ z7i)}ljQ&14F!CBK*(l!s8xT4WS`^C3-<%)E`+qgL-{xMP+bw5j&NspOF&p~1~8R20LE2yLPK^SjJ^ zCrdYOMtg~&H3r43;?^Dwr{>d`ME#4Bz6ckigt42VkO!0qjLOh++J)}FO>a~iw~$rh z4L|54a4u;y7XBrAPQ_bC6dY*MeXk+2g)B&*(_1RH&Y*%-Vt|wL{sY4yv>-uD=G-EF zLO+v-f=eBF&53gBimrZwCucn@&vH*PC}J^PD-qaBwplrwNL#dsdtd9N;7Gq;F@kUvP>+ zX{*%aROcyYPynFXl=*F%=GzMC{%;2SPzI94>kLq z=2<67joVD)7PBIYPK#N=P;_lG>CCHh_D~d}NOX#y{Y{HbQp3FxT}_%MRjt+@EC}Za zNyi9Afpmxq(H*TQ@Ejc846s^j+xT&>uWiSe@tAt!RkgB10CNN-DTG0ML?jV8q%caO zaEfuL661Z<1k4WKfj@b~O z`$i3|H7IO#&rz@gCK92;%2137N^wO^P~^KrPEqCzUJ-Zze;&-6Y*6OP1+mpV7la)_ z&6)JK0>p{D|b}}ljB}_z)L#(9<3QP!WB5tkp7X(>9ADvx# zRLnB0f5nN-!c`Guo-HH}Zb7-8nDC(2IZw>F;C!bs@4Ez*d#-uze>8w|XS zS#cxcSb;NmziSF)8imvt2*k3k`~94%njumdF~+!cvFL|B4)dtmg)A7*4B{c3ri5FB zIZ6!lTd8Psk^T{TC|Ng8HYki0j$l(dUOzaWXucz9y;K$`SkoxP!7)Q(SjKP0T)0@& z$!derSUE+gbL4FV?$!`7yXU19E=6Gh2)P&ac{M|=uKZ{op$YY-+T}Q;^N~)N10gmo zq{CH8k>H^i=Eqv%)^cBkj_X5sJ#5W2)n<}QGdyf+aqC6;K#23!a}@9g^c!?KEcGtp&gnmJ2f61r47VK(YZ^pY*@qXBiafJ_5 z{9tIWYdvU(V+7g5RE*JA`Vl{~jzI|ao6km?aW2ClE$V(<0MrF(Rm03UO^~I!AYCvp zcS*_F`;HWR@4uZ76P;FX#-^8$Z9R&Su?Y}tID5ExaA8wWdnig!s9QyI-|}@W_`in;^mi3EXD2?`ne0-Gz41pB#-qvjuEm9Rm@WF}~Pz*ELT^#e{cz2dfmaPrb?&eg(QvLMU`g>CZrAQFE`0R6M#Wt)|Fbfoac+rH*8+f<%c(x z)R~bk_aTLqFR@Ucm*h1t1pacWGz!W$OwN)HC;DOY)64`2SCV z!6~f?w(`dYa{>pUd!SDWG)HK@()S~g$k2-rICy3XHNdbIt^C?xw)D$R4+Wnu6!v02 zS*C&R{Zd-?Yvm6MWLZx%{b_Whz*E_b8Cy6crNz8fesv&=?~kN^kW}v_d^`zSaz={v zx?J7lU{Gr4(u2V^fd{MHhl&>Ha(aqYxd99}Iv5<@%Q;RNnnDpR9JE{%6L`~0qDl?Fix0~ru&^QJ)yzU~m%2}= zv2Y>54J-a5DQr;`Jept$wivQDwX9?7x;Ldle_V=%wDcsQqXoS?beD2Ure?!;;Ogx_ zVM_QE>0-8)^=LL%f6-;_Sl3>Y%5!yf^q13E5Isebz0M}U>Napua7}>86X)#7@WfQ~ z|2w@bZ&jsog|k>{mK&r41g;KPjamv0(s2_pd;|k7bRi9Cu`N z1hx(+dFZd9{Y*=dzh4~6K+)j~b0a?Gs}}mc7A~me3(CnLMy3@Fn z+l87hvjz*4OgNDeT;LpZi-K}M!RAIjJzR<96%RDa%QL?nyl3>=8~g97hcJa&g4nO*T~?PyeQyiU{%$UKj!;PjB!g|XqNXLBDJcTwPI zaAVxAJWNtGQyn(=6-H8L))}w;Gitpc`nWWCm#Mc1;yO!