From 9efc05a9aeb2bc6c6958d3f682d72bf584d324a6 Mon Sep 17 00:00:00 2001 From: Claudia Date: Tue, 8 Nov 2016 13:17:16 -0800 Subject: [PATCH 1/3] added preliminary files --- lab-claudia/.babelrc | 3 + lab-claudia/.client.env | 5 + lab-claudia/.coveralls.yml | 2 + lab-claudia/.eslintignore | 5 + lab-claudia/.eslintrc | 28 + lab-claudia/.gitignore | 163 +++ lab-claudia/.server.env | 11 + lab-claudia/.travis.yml | 20 + .../artist/artist-nav/_artist-nav.scss | 15 + .../artist/artist-nav/artist-nav.html | 31 + .../component/artist/artist-nav/artist-nav.js | 24 + .../artist/create-artist/_create-artist.scss | 75 + .../create-artist/create-artist-controller.js | 20 + .../artist/create-artist/create-artist.html | 77 + .../artist/edit-artist/_edit-artist.scss | 33 + .../artist/edit-artist/edit-artist.html | 71 + .../artist/edit-artist/edit-artist.js | 21 + .../profile-container/_profile-container.scss | 6 + .../profile-container/profile-container.html | 12 + .../profile-container/profile-container.js | 27 + .../artist/profile-pic/_profile-pic.scss | 0 .../artist/profile-pic/profile-pic.html | 18 + .../artist/profile-pic/profile-pic.js | 24 + .../create-gallery/_create-gallery.scss | 39 + .../create-gallery/create-gallery.html | 36 + .../gallery/create-gallery/create-gallery.js | 25 + .../gallery/edit-gallery/_edit-gallery.scss | 21 + .../gallery/edit-gallery/edit-gallery.html | 24 + .../gallery/edit-gallery/edit-gallery.js | 21 + .../gallery/gallery-li/_gallery-li.scss | 25 + .../gallery/gallery-li/gallery-li.html | 18 + .../gallery/gallery-li/gallery-li.js | 42 + .../_thumbnail-container.scss | 28 + .../thumbnail-container.html | 9 + .../thumbnail-container.js | 14 + .../gallery/thumbnail/_thumbnail.scss | 9 + .../gallery/thumbnail/thumbnail.html | 10 + .../component/gallery/thumbnail/thumbnail.js | 26 + .../gallery/upload-pic/_upload-pic.scss | 44 + .../gallery/upload-pic/upload-pic.html | 26 + .../gallery/upload-pic/upload-pic.js | 33 + .../app/component/landing/login/_login.scss | 5 + .../app/component/landing/login/login.html | 34 + .../app/component/landing/login/login.js | 20 + .../app/component/landing/signup/_signup.scss | 1 + .../app/component/landing/signup/signup.html | 22 + .../app/component/landing/signup/signup.js | 20 + lab-claudia/app/component/navbar/_navbar.scss | 48 + lab-claudia/app/component/navbar/navbar.html | 22 + lab-claudia/app/component/navbar/navbar.js | 62 + .../app/component/searchbar/_searchbar.scss | 3 + .../app/component/searchbar/searchbar.html | 6 + .../app/component/searchbar/searchbar.js | 12 + lab-claudia/app/config/log-config.js | 7 + lab-claudia/app/config/router-config.js | 34 + lab-claudia/app/entry.js | 84 ++ lab-claudia/app/filter/gallery-search.js | 25 + lab-claudia/app/index.html | 16 + lab-claudia/app/scss/lib/_theme.scss | 10 + lab-claudia/app/scss/lib/_vendor.scss | 2 + lab-claudia/app/scss/main.scss | 156 +++ lab-claudia/app/service/artist-service.js | 132 ++ lab-claudia/app/service/auth-service.js | 85 ++ lab-claudia/app/service/gallery-service.js | 142 ++ lab-claudia/app/service/pic-service.js | 122 ++ lab-claudia/app/view/home/_home.scss | 49 + lab-claudia/app/view/home/home-controller.js | 18 + lab-claudia/app/view/home/home.html | 43 + lab-claudia/app/view/landing/_landing.scss | 98 ++ .../app/view/landing/landing-controller.js | 21 + lab-claudia/app/view/landing/landing.html | 31 + lab-claudia/client-test/auth-service-test.js | 105 ++ .../create-gallery-controller-test.js | 61 + .../edit-gallery-controller-test.js | 78 ++ lab-claudia/client-test/example-test.js | 7 + .../client-test/gallery-li-controller-test.js | 87 ++ .../client-test/gallery-service-test.js | 149 ++ .../client-test/home-controller-test.js | 0 .../client-test/navbar-controller-test.js | 20 + .../thumbnail-container-controller-test.js | 26 + .../client-test/thumbnail-controller-test.js | 75 + lab-claudia/gulpfile.js | 24 + lab-claudia/karma.conf.js | 29 + lab-claudia/lib/basic-auth-middleware.js | 29 + lab-claudia/lib/bearer-auth-middleware.js | 27 + lab-claudia/lib/error-middleware.js | 35 + lab-claudia/lib/page-query-middleware.js | 30 + lab-claudia/model/artist.js | 22 + lab-claudia/model/gallery.js | 19 + lab-claudia/model/listing.js | 19 + lab-claudia/model/photo.js | 18 + lab-claudia/model/user.js | 77 + lab-claudia/package.json | 85 ++ lab-claudia/route/artist-router.js | 83 ++ lab-claudia/route/auth-router.js | 102 ++ lab-claudia/route/gallery-router.js | 121 ++ lab-claudia/route/listing-router.js | 104 ++ lab-claudia/route/page-router.js | 35 + lab-claudia/route/photo-router.js | 264 ++++ lab-claudia/server.js | 50 + lab-claudia/test/artist-router-test.js | 1247 +++++++++++++++++ lab-claudia/test/auth-router-test.js | 486 +++++++ lab-claudia/test/data/dog.jpg | Bin 0 -> 41326 bytes lab-claudia/test/data/doge.png | Bin 0 -> 212428 bytes lab-claudia/test/gallery-router-test.js | 738 ++++++++++ lab-claudia/test/lib/artist-mock.js | 29 + lab-claudia/test/lib/aws-mock.js | 33 + lab-claudia/test/lib/clean-db.js | 23 + lab-claudia/test/lib/gallery-mock.js | 26 + lab-claudia/test/lib/listing-mock.js | 28 + lab-claudia/test/lib/mock-many-artists.js | 48 + lab-claudia/test/lib/mock-many-photos.js | 48 + lab-claudia/test/lib/photo-mock.js | 33 + .../lib/populate-artist-galleries-mock.js | 39 + .../lib/populate-gallery-listings-mock.js | 40 + lab-claudia/test/lib/server-control.js | 30 + lab-claudia/test/lib/test-env.js | 8 + lab-claudia/test/lib/user-mock.js | 30 + lab-claudia/test/listing-router-test.js | 651 +++++++++ lab-claudia/test/page-router-test.js | 112 ++ lab-claudia/test/photo-router-test.js | 474 +++++++ lab-claudia/webpack.config.js | 68 + 122 files changed, 8443 insertions(+) create mode 100644 lab-claudia/.babelrc create mode 100644 lab-claudia/.client.env create mode 100644 lab-claudia/.coveralls.yml create mode 100644 lab-claudia/.eslintignore create mode 100644 lab-claudia/.eslintrc create mode 100644 lab-claudia/.gitignore create mode 100644 lab-claudia/.server.env create mode 100644 lab-claudia/.travis.yml create mode 100644 lab-claudia/app/component/artist/artist-nav/_artist-nav.scss create mode 100644 lab-claudia/app/component/artist/artist-nav/artist-nav.html create mode 100644 lab-claudia/app/component/artist/artist-nav/artist-nav.js create mode 100644 lab-claudia/app/component/artist/create-artist/_create-artist.scss create mode 100644 lab-claudia/app/component/artist/create-artist/create-artist-controller.js create mode 100644 lab-claudia/app/component/artist/create-artist/create-artist.html create mode 100644 lab-claudia/app/component/artist/edit-artist/_edit-artist.scss create mode 100644 lab-claudia/app/component/artist/edit-artist/edit-artist.html create mode 100644 lab-claudia/app/component/artist/edit-artist/edit-artist.js create mode 100644 lab-claudia/app/component/artist/profile-container/_profile-container.scss create mode 100644 lab-claudia/app/component/artist/profile-container/profile-container.html create mode 100644 lab-claudia/app/component/artist/profile-container/profile-container.js create mode 100644 lab-claudia/app/component/artist/profile-pic/_profile-pic.scss create mode 100644 lab-claudia/app/component/artist/profile-pic/profile-pic.html create mode 100644 lab-claudia/app/component/artist/profile-pic/profile-pic.js create mode 100644 lab-claudia/app/component/gallery/create-gallery/_create-gallery.scss create mode 100644 lab-claudia/app/component/gallery/create-gallery/create-gallery.html create mode 100644 lab-claudia/app/component/gallery/create-gallery/create-gallery.js create mode 100644 lab-claudia/app/component/gallery/edit-gallery/_edit-gallery.scss create mode 100644 lab-claudia/app/component/gallery/edit-gallery/edit-gallery.html create mode 100644 lab-claudia/app/component/gallery/edit-gallery/edit-gallery.js create mode 100644 lab-claudia/app/component/gallery/gallery-li/_gallery-li.scss create mode 100644 lab-claudia/app/component/gallery/gallery-li/gallery-li.html create mode 100644 lab-claudia/app/component/gallery/gallery-li/gallery-li.js create mode 100644 lab-claudia/app/component/gallery/thumbnail-container/_thumbnail-container.scss create mode 100644 lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.html create mode 100644 lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.js create mode 100644 lab-claudia/app/component/gallery/thumbnail/_thumbnail.scss create mode 100644 lab-claudia/app/component/gallery/thumbnail/thumbnail.html create mode 100644 lab-claudia/app/component/gallery/thumbnail/thumbnail.js create mode 100644 lab-claudia/app/component/gallery/upload-pic/_upload-pic.scss create mode 100644 lab-claudia/app/component/gallery/upload-pic/upload-pic.html create mode 100644 lab-claudia/app/component/gallery/upload-pic/upload-pic.js create mode 100644 lab-claudia/app/component/landing/login/_login.scss create mode 100644 lab-claudia/app/component/landing/login/login.html create mode 100644 lab-claudia/app/component/landing/login/login.js create mode 100644 lab-claudia/app/component/landing/signup/_signup.scss create mode 100644 lab-claudia/app/component/landing/signup/signup.html create mode 100644 lab-claudia/app/component/landing/signup/signup.js create mode 100644 lab-claudia/app/component/navbar/_navbar.scss create mode 100644 lab-claudia/app/component/navbar/navbar.html create mode 100644 lab-claudia/app/component/navbar/navbar.js create mode 100644 lab-claudia/app/component/searchbar/_searchbar.scss create mode 100644 lab-claudia/app/component/searchbar/searchbar.html create mode 100644 lab-claudia/app/component/searchbar/searchbar.js create mode 100644 lab-claudia/app/config/log-config.js create mode 100644 lab-claudia/app/config/router-config.js create mode 100644 lab-claudia/app/entry.js create mode 100644 lab-claudia/app/filter/gallery-search.js create mode 100644 lab-claudia/app/index.html create mode 100644 lab-claudia/app/scss/lib/_theme.scss create mode 100644 lab-claudia/app/scss/lib/_vendor.scss create mode 100644 lab-claudia/app/scss/main.scss create mode 100644 lab-claudia/app/service/artist-service.js create mode 100644 lab-claudia/app/service/auth-service.js create mode 100644 lab-claudia/app/service/gallery-service.js create mode 100644 lab-claudia/app/service/pic-service.js create mode 100644 lab-claudia/app/view/home/_home.scss create mode 100644 lab-claudia/app/view/home/home-controller.js create mode 100644 lab-claudia/app/view/home/home.html create mode 100644 lab-claudia/app/view/landing/_landing.scss create mode 100644 lab-claudia/app/view/landing/landing-controller.js create mode 100644 lab-claudia/app/view/landing/landing.html create mode 100644 lab-claudia/client-test/auth-service-test.js create mode 100644 lab-claudia/client-test/create-gallery-controller-test.js create mode 100644 lab-claudia/client-test/edit-gallery-controller-test.js create mode 100644 lab-claudia/client-test/example-test.js create mode 100644 lab-claudia/client-test/gallery-li-controller-test.js create mode 100644 lab-claudia/client-test/gallery-service-test.js create mode 100644 lab-claudia/client-test/home-controller-test.js create mode 100644 lab-claudia/client-test/navbar-controller-test.js create mode 100644 lab-claudia/client-test/thumbnail-container-controller-test.js create mode 100644 lab-claudia/client-test/thumbnail-controller-test.js create mode 100644 lab-claudia/gulpfile.js create mode 100644 lab-claudia/karma.conf.js create mode 100644 lab-claudia/lib/basic-auth-middleware.js create mode 100644 lab-claudia/lib/bearer-auth-middleware.js create mode 100644 lab-claudia/lib/error-middleware.js create mode 100644 lab-claudia/lib/page-query-middleware.js create mode 100644 lab-claudia/model/artist.js create mode 100644 lab-claudia/model/gallery.js create mode 100644 lab-claudia/model/listing.js create mode 100644 lab-claudia/model/photo.js create mode 100644 lab-claudia/model/user.js create mode 100644 lab-claudia/package.json create mode 100644 lab-claudia/route/artist-router.js create mode 100644 lab-claudia/route/auth-router.js create mode 100644 lab-claudia/route/gallery-router.js create mode 100644 lab-claudia/route/listing-router.js create mode 100644 lab-claudia/route/page-router.js create mode 100644 lab-claudia/route/photo-router.js create mode 100644 lab-claudia/server.js create mode 100644 lab-claudia/test/artist-router-test.js create mode 100644 lab-claudia/test/auth-router-test.js create mode 100644 lab-claudia/test/data/dog.jpg create mode 100644 lab-claudia/test/data/doge.png create mode 100644 lab-claudia/test/gallery-router-test.js create mode 100644 lab-claudia/test/lib/artist-mock.js create mode 100644 lab-claudia/test/lib/aws-mock.js create mode 100644 lab-claudia/test/lib/clean-db.js create mode 100644 lab-claudia/test/lib/gallery-mock.js create mode 100644 lab-claudia/test/lib/listing-mock.js create mode 100644 lab-claudia/test/lib/mock-many-artists.js create mode 100644 lab-claudia/test/lib/mock-many-photos.js create mode 100644 lab-claudia/test/lib/photo-mock.js create mode 100644 lab-claudia/test/lib/populate-artist-galleries-mock.js create mode 100644 lab-claudia/test/lib/populate-gallery-listings-mock.js create mode 100644 lab-claudia/test/lib/server-control.js create mode 100644 lab-claudia/test/lib/test-env.js create mode 100644 lab-claudia/test/lib/user-mock.js create mode 100644 lab-claudia/test/listing-router-test.js create mode 100644 lab-claudia/test/page-router-test.js create mode 100644 lab-claudia/test/photo-router-test.js create mode 100644 lab-claudia/webpack.config.js diff --git a/lab-claudia/.babelrc b/lab-claudia/.babelrc new file mode 100644 index 0000000..c13c5f6 --- /dev/null +++ b/lab-claudia/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/lab-claudia/.client.env b/lab-claudia/.client.env new file mode 100644 index 0000000..27c66d1 --- /dev/null +++ b/lab-claudia/.client.env @@ -0,0 +1,5 @@ +# example .env +NODE_ENV="dev" +TITLE="Photo Gallery" +API_URL="http://localhost:3000" +GOOGLE_CLIENT_ID="308322069962-972jlkfpbov9cd88ot0dtdtc2lgfutdl.apps.googleusercontent.com" diff --git a/lab-claudia/.coveralls.yml b/lab-claudia/.coveralls.yml new file mode 100644 index 0000000..7e77bc7 --- /dev/null +++ b/lab-claudia/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-ci +repo_token: yyiFHQhz99RneXclnlk7FeYimmLptZvAK diff --git a/lab-claudia/.eslintignore b/lab-claudia/.eslintignore new file mode 100644 index 0000000..05b1cf3 --- /dev/null +++ b/lab-claudia/.eslintignore @@ -0,0 +1,5 @@ +**/node_modules/* +**/vendor/* +**/*.min.js +**/coverage/* +**/build/* diff --git a/lab-claudia/.eslintrc b/lab-claudia/.eslintrc new file mode 100644 index 0000000..564f18c --- /dev/null +++ b/lab-claudia/.eslintrc @@ -0,0 +1,28 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "semi": ["error", "always"], + "linebreak-style": [ "error", "unix" ], + "comma-dangle": ["error", "always-multiline"], + "quotes": ["error", "single", { "allowTemplateLiterals": true }] + }, + "env": { + "es6": true, + "node": true, + "mocha": true, + "jasmine": true + }, + "globals": { + "angular": "true", + "window": false, + "__API_URL__": false, + "__DEBUG__": false + }, + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + }, + "extends": "eslint:recommended" +} diff --git a/lab-claudia/.gitignore b/lab-claudia/.gitignore new file mode 100644 index 0000000..e588426 --- /dev/null +++ b/lab-claudia/.gitignore @@ -0,0 +1,163 @@ +#Other +db +.env +build +coverage +html-report + +# Created by https://www.gitignore.io/api/osx,linux,vim,sublimetext,windows,node + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +### Vim ### +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + + +### SublimeText ### +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz diff --git a/lab-claudia/.server.env b/lab-claudia/.server.env new file mode 100644 index 0000000..3273e74 --- /dev/null +++ b/lab-claudia/.server.env @@ -0,0 +1,11 @@ +PORT=3000 +NODE_ENV="dev" +MONGODB_URI=mongodb://localhost/art-c-dev +APP_SECRET='my secret code' + +AWS_ACCESS_KEY_ID=AKIAIP2EINCLGLW2GOHQ +AWS_SECRET_ACCESS_KEY=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 + +AWS_BUCKET='tastytoast-assets' +GOOGLE_CLIENT_ID="308322069962-972jlkfpbov9cd88ot0dtdtc2lgfutdl.apps.googleusercontent.com" +GOOGLE_CLIENT_SECRET="O9l0XN3xShCX-wTWS_LHv9Gf" diff --git a/lab-claudia/.travis.yml b/lab-claudia/.travis.yml new file mode 100644 index 0000000..cb34e7b --- /dev/null +++ b/lab-claudia/.travis.yml @@ -0,0 +1,20 @@ +language: node_js +node_js: + - '4.4.3' +services: + - mongodb +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.8 + - g++-4.8 +env: + - CXX=g++-4.8 +sudo: required +before_script: npm i -g eslint mocha +after_success: npm run coveralls +script: + - npm test + - npm run lint diff --git a/lab-claudia/app/component/artist/artist-nav/_artist-nav.scss b/lab-claudia/app/component/artist/artist-nav/_artist-nav.scss new file mode 100644 index 0000000..1197739 --- /dev/null +++ b/lab-claudia/app/component/artist/artist-nav/_artist-nav.scss @@ -0,0 +1,15 @@ +@import "theme"; + +.artist-navbar { + background-color: $app-black; + color: $app-white; + + .artist-nav-btn { + background-color: transparent; + margin: 2%; + } + + .artist-nav-btn:hover { + color: $app-white; + } +} diff --git a/lab-claudia/app/component/artist/artist-nav/artist-nav.html b/lab-claudia/app/component/artist/artist-nav/artist-nav.html new file mode 100644 index 0000000..7f31622 --- /dev/null +++ b/lab-claudia/app/component/artist/artist-nav/artist-nav.html @@ -0,0 +1,31 @@ + diff --git a/lab-claudia/app/component/artist/artist-nav/artist-nav.js b/lab-claudia/app/component/artist/artist-nav/artist-nav.js new file mode 100644 index 0000000..e229fe7 --- /dev/null +++ b/lab-claudia/app/component/artist/artist-nav/artist-nav.js @@ -0,0 +1,24 @@ +'use strict'; + +require('./_artist-nav.scss'); + +module.exports = { + template: require('./artist-nav.html'), + controller: ['$log', '$location', artistNavController], + controllerAs: 'artistNavCtrl', +}; + +function artistNavController($log, $location) { + $log.debug('init artistNavCtrl'); + + this.viewGallery = function(){ + $log.log('navbarCtrl.viewGallery()'); + $location.url('/gallery'); + }; + + this.updateProfile = function(){ + $log.log('navbarCtrl.updateProfile()'); + $location.url('/profile'); + }; + +} diff --git a/lab-claudia/app/component/artist/create-artist/_create-artist.scss b/lab-claudia/app/component/artist/create-artist/_create-artist.scss new file mode 100644 index 0000000..d0c27dc --- /dev/null +++ b/lab-claudia/app/component/artist/create-artist/_create-artist.scss @@ -0,0 +1,75 @@ +@import "theme"; + +.create-artist-container { + + .artist-form { + background-color: $app-white; + input, + select { + width: 80%; + font-size: 100%; + background-color: transparent; + text-transform: uppercase; + border:none; + border-bottom: 1.5px solid $app-primary; + margin: 3%; + padding: 1% 0; + color: $app-white; + } + + input:focus{ + outline: none; + } + /*---- Changes Placeholder Color -----*/ + input::-webkit-input-placeholder, + textarea::-webkit-input-placeholder { + color: $app-primary; + } + + + /* General Button Styles */ + .btn { + background-color: rgba(255,255,255,.3); + border: none; + text-transform: uppercase; + border-radius: 3px; + font-size: 120%; + padding: 1.5% 2%; + margin-top: 3%; + width: 25%; + transition: all 0.3s; + color: $app-secondary; + } + + .btn:hover { + background:rgba(255,255,255,1); + color: $app-black; + } + + p { + padding: 2%; + display: inline-block; + color: $app-white; + } + + a { + display: inline-block; + font-size: 1.2em; + text-decoration: none; + } + + @media (min-width: 400px) { + width: 90%; + } + + @media (min-width: 600px) { + width: 65%; + } + + @media (min-width: 1000px) { + width: 50%; + } + } + + +} diff --git a/lab-claudia/app/component/artist/create-artist/create-artist-controller.js b/lab-claudia/app/component/artist/create-artist/create-artist-controller.js new file mode 100644 index 0000000..7c6b1a5 --- /dev/null +++ b/lab-claudia/app/component/artist/create-artist/create-artist-controller.js @@ -0,0 +1,20 @@ +'use strict'; + +require('./_create-artist.scss'); + +module.exports = { + template: require('./create-artist.html'), + controller: ['$log', 'artistService', CreateArtistController], + controllerAs: 'createArtistCtrl', +}; + +function CreateArtistController($log, artistService){ + $log.debug('init createArtistCtrl'); + + this.artist = {}; + + this.createArtist = function(){ + artistService.createArtist(this.artist); + }; + +} diff --git a/lab-claudia/app/component/artist/create-artist/create-artist.html b/lab-claudia/app/component/artist/create-artist/create-artist.html new file mode 100644 index 0000000..928d803 --- /dev/null +++ b/lab-claudia/app/component/artist/create-artist/create-artist.html @@ -0,0 +1,77 @@ +
+ +

Create your artist profile

+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + + + +
diff --git a/lab-claudia/app/component/artist/edit-artist/_edit-artist.scss b/lab-claudia/app/component/artist/edit-artist/_edit-artist.scss new file mode 100644 index 0000000..4e4e39e --- /dev/null +++ b/lab-claudia/app/component/artist/edit-artist/_edit-artist.scss @@ -0,0 +1,33 @@ +@import "theme"; + +.edit-artist-container { + color: $app-black; + background-color: $app-secondary; + padding: 3%; + + .artist-form { + /*--------- Input Styles --------*/ + input, + select { + width: 100%; + font-size: 90%; + background-color: transparent; + text-transform: uppercase; + border:none; + border-bottom: 1.5px solid $app-primary; + margin: 1%; + padding: 1% 0; + color: $app-black; + } + + input:focus{ + outline: none; + } + /*---- Changes Placeholder Color -----*/ + input::-webkit-input-placeholder, + textarea::-webkit-input-placeholder { + color: $app-primary; + } + + } +} diff --git a/lab-claudia/app/component/artist/edit-artist/edit-artist.html b/lab-claudia/app/component/artist/edit-artist/edit-artist.html new file mode 100644 index 0000000..6b63684 --- /dev/null +++ b/lab-claudia/app/component/artist/edit-artist/edit-artist.html @@ -0,0 +1,71 @@ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + + + +
diff --git a/lab-claudia/app/component/artist/edit-artist/edit-artist.js b/lab-claudia/app/component/artist/edit-artist/edit-artist.js new file mode 100644 index 0000000..e6cf3aa --- /dev/null +++ b/lab-claudia/app/component/artist/edit-artist/edit-artist.js @@ -0,0 +1,21 @@ +'use strict'; + +require('./_edit-artist.scss'); + +module.exports = { + template: require('./edit-artist.html'), + controller: ['$log', 'artistService', EditArtistController], + controllerAs: 'editArtistCtrl', + bindings: { + // one way data binding + artist: '<', + }, +}; + +function EditArtistController($log, artistService) { + $log.debug('init editArtistCtrl'); + + this.updateArtist = function(){ + artistService.updateArtist(this.artist, this.artist._id); + }; +} diff --git a/lab-claudia/app/component/artist/profile-container/_profile-container.scss b/lab-claudia/app/component/artist/profile-container/_profile-container.scss new file mode 100644 index 0000000..761e0a9 --- /dev/null +++ b/lab-claudia/app/component/artist/profile-container/_profile-container.scss @@ -0,0 +1,6 @@ +@import "theme"; + +.profile-container { + padding: 3%; + +} diff --git a/lab-claudia/app/component/artist/profile-container/profile-container.html b/lab-claudia/app/component/artist/profile-container/profile-container.html new file mode 100644 index 0000000..12fa8ae --- /dev/null +++ b/lab-claudia/app/component/artist/profile-container/profile-container.html @@ -0,0 +1,12 @@ +
+

Edit your profile

+
+
+ +
+ +
+ +
+
+
diff --git a/lab-claudia/app/component/artist/profile-container/profile-container.js b/lab-claudia/app/component/artist/profile-container/profile-container.js new file mode 100644 index 0000000..7441575 --- /dev/null +++ b/lab-claudia/app/component/artist/profile-container/profile-container.js @@ -0,0 +1,27 @@ +'use strict'; + +require('./_profile-container.scss'); + +// Takes in a pic and a artist via one way data binding +// Bindings define the attributes 'pic' and 'artist' in the html +// Assigns 'pic' and 'artist' on the scope of the profile-container - objects can be passed in + +module.exports = { + template: require('./profile-container.html'), + controller: ['$log', 'picService', ProfileContainerController], + controllerAs: 'profileContainerCtrl', + bindings: { + pic: '<', + artist: '<', + }, +}; + +function ProfileContainerController($log, picService) { + + $log.debug('init profileContainerCtrl'); + + this.deletePic = function() { + $log.debug('profileContainerCtrl.deletePic'); + picService.deleteArtistPic(this.artist, this.pic._id); + }; +} diff --git a/lab-claudia/app/component/artist/profile-pic/_profile-pic.scss b/lab-claudia/app/component/artist/profile-pic/_profile-pic.scss new file mode 100644 index 0000000..e69de29 diff --git a/lab-claudia/app/component/artist/profile-pic/profile-pic.html b/lab-claudia/app/component/artist/profile-pic/profile-pic.html new file mode 100644 index 0000000..aa1600e --- /dev/null +++ b/lab-claudia/app/component/artist/profile-pic/profile-pic.html @@ -0,0 +1,18 @@ +
+ +
+ + +
+ + + +
+
+
diff --git a/lab-claudia/app/component/artist/profile-pic/profile-pic.js b/lab-claudia/app/component/artist/profile-pic/profile-pic.js new file mode 100644 index 0000000..7da04af --- /dev/null +++ b/lab-claudia/app/component/artist/profile-pic/profile-pic.js @@ -0,0 +1,24 @@ +'use strict'; + +require('./_profile-pic.scss'); + +module.exports = { + template: require('./profile-pic.html'), + controller: ['$log', 'picService', ProfilePicController], + controllerAs: 'profilePicCtrl', +}; + +function ProfilePicController($log, picService) { + $log.debug('init profilePicCtrl'); + this.pic = {}, + this.done = function(){ + this.pic.file = null; + }; + + this.uploadArtistPic = function(){ + picService.uploadArtistPic(this.artist, this.pic) + .then(() => { + this.done(); + }); + }; +} diff --git a/lab-claudia/app/component/gallery/create-gallery/_create-gallery.scss b/lab-claudia/app/component/gallery/create-gallery/_create-gallery.scss new file mode 100644 index 0000000..98d2cf7 --- /dev/null +++ b/lab-claudia/app/component/gallery/create-gallery/_create-gallery.scss @@ -0,0 +1,39 @@ +@import "theme"; + +.create-gallery-container { + width: 90%; + margin: 0 5%; + padding: 3%; + margin-bottom: 3%; + background-color: $app-primary; + + h2 { + color: $app-black; + margin: 3%; + padding: 1% 0; + + } + + .btn-add { + background-color: $app-secondary; + border: 1px solid $app-primary; + color: $app-black; + margin: 3%; + } + + .btn-add:hover { + background-color: $app-white; + } + + input, + select { + width: 80%; + font-size: 100%; + border: 1.5px solid $app-secondary; + border-radius: 10px; + margin: 3% 10%; + padding: 1% 2%; + color: $app-black; + } + +} diff --git a/lab-claudia/app/component/gallery/create-gallery/create-gallery.html b/lab-claudia/app/component/gallery/create-gallery/create-gallery.html new file mode 100644 index 0000000..6e1a944 --- /dev/null +++ b/lab-claudia/app/component/gallery/create-gallery/create-gallery.html @@ -0,0 +1,36 @@ + diff --git a/lab-claudia/app/component/gallery/create-gallery/create-gallery.js b/lab-claudia/app/component/gallery/create-gallery/create-gallery.js new file mode 100644 index 0000000..92c6847 --- /dev/null +++ b/lab-claudia/app/component/gallery/create-gallery/create-gallery.js @@ -0,0 +1,25 @@ +'use strict'; + +require('./_create-gallery.scss'); + +module.exports = { + template: require('./create-gallery.html'), + controller: ['$log', 'galleryService', CreateGalleryController], + controllerAs: 'createGalleryCtrl', +}; + +function CreateGalleryController($log, galleryService){ + $log.debug('init createGalleryCtrl'); + this.gallery = {}; + // method that passes in current gallery object when called from view + this.createGallery = function(){ + + galleryService.createGallery(this.gallery) + .then(() => { + // on success, nulls out inputs so it doesn't repeat the old data + // ng-model is using it, so they stay in there + this.gallery.name = null; + this.gallery.desc = null; + }); + }; +} diff --git a/lab-claudia/app/component/gallery/edit-gallery/_edit-gallery.scss b/lab-claudia/app/component/gallery/edit-gallery/_edit-gallery.scss new file mode 100644 index 0000000..829d2eb --- /dev/null +++ b/lab-claudia/app/component/gallery/edit-gallery/_edit-gallery.scss @@ -0,0 +1,21 @@ +@import "theme"; +.edit-gallery-container { + margin-bottom: 3%; + + .btn { + color: $app-black; + } + + input, + select { + width: 90%; + margin: 0 5%; + font-size: 100%; + border:none; + border: 1.5px solid $app-secondary; + margin: 3%; + padding: 1% 0; + color: $app-black; + } + +} diff --git a/lab-claudia/app/component/gallery/edit-gallery/edit-gallery.html b/lab-claudia/app/component/gallery/edit-gallery/edit-gallery.html new file mode 100644 index 0000000..dde96eb --- /dev/null +++ b/lab-claudia/app/component/gallery/edit-gallery/edit-gallery.html @@ -0,0 +1,24 @@ + diff --git a/lab-claudia/app/component/gallery/edit-gallery/edit-gallery.js b/lab-claudia/app/component/gallery/edit-gallery/edit-gallery.js new file mode 100644 index 0000000..6440047 --- /dev/null +++ b/lab-claudia/app/component/gallery/edit-gallery/edit-gallery.js @@ -0,0 +1,21 @@ +'use strict'; + +require('./_edit-gallery.scss'); + +module.exports = { + template: require('./edit-gallery.html'), + controller: ['$log', 'galleryService', EditGalleryController], + controllerAs: 'editGalleryCtrl', + bindings: { + // one way data binding + gallery: '<', + }, +}; + +function EditGalleryController($log, galleryService){ + $log.debug('init editGalleryCtrl'); + + this.updateGallery = function(){ + galleryService.updateGallery(this.gallery, this.gallery._id); + }; +} diff --git a/lab-claudia/app/component/gallery/gallery-li/_gallery-li.scss b/lab-claudia/app/component/gallery/gallery-li/_gallery-li.scss new file mode 100644 index 0000000..512878a --- /dev/null +++ b/lab-claudia/app/component/gallery/gallery-li/_gallery-li.scss @@ -0,0 +1,25 @@ +@import "theme"; +@import "vendor"; + +.gallery-li { + margin: 1.6%; + padding: 2%; + width: 30%; + float: right; + background-color: $app-secondary; + + p { + margin: 2%; + } + span { + float: left; + margin: 2%; + } + .fa { + color: $app-primary; + transition: 1s; + } + .fa:hover { + color: $app-white; + } +} diff --git a/lab-claudia/app/component/gallery/gallery-li/gallery-li.html b/lab-claudia/app/component/gallery/gallery-li/gallery-li.html new file mode 100644 index 0000000..6103dfd --- /dev/null +++ b/lab-claudia/app/component/gallery/gallery-li/gallery-li.html @@ -0,0 +1,18 @@ + + diff --git a/lab-claudia/app/component/gallery/gallery-li/gallery-li.js b/lab-claudia/app/component/gallery/gallery-li/gallery-li.js new file mode 100644 index 0000000..1733154 --- /dev/null +++ b/lab-claudia/app/component/gallery/gallery-li/gallery-li.js @@ -0,0 +1,42 @@ +'use strict'; + +require('./_gallery-li.scss'); + +module.exports = { + template: require('./gallery-li.html'), + controller: ['$log', 'galleryService', GalleryLIController], + controllerAs: 'galleryLICtrl', + bindings: { + gallery: '<', + deleteDone: '&', + }, +}; + +function GalleryLIController($log, galleryService){ + $log.debug('init galleryLICtrl'); + + this.showEditGallery = false; + + this.deleteGallery = function(){ + // console.log('galleryservice.deleteGallery'); + // console.log('galleryid', this.gallery._id); + galleryService.deleteGallery(this.gallery._id) + .then(() => { + this.deleteDone({galleryData: this.gallery}); + }); + }; +} + +// the Template filename creates +// Bindings allow objects and functions to be passed through scopes in html +// & - passing in a function - allows us to pass from child scope to parent scope +// < - passing in an object - pass in from parent to child + +// GalleryLIController +// The edit gallery is not shown unless button is clicked +// DeleteGallery is called when button is clicked - delete done is passed in in the template for home ctr; +// when gal is sucessfully deleted, it calls delete done +// Object is passed in deleteDone to map named parameters in function call +// this.deleteDone - function added to the scope via binding +// & - attributes are always named properties on an object +// & creates the 'gallery' attribute used to pass a gallery in in the html diff --git a/lab-claudia/app/component/gallery/thumbnail-container/_thumbnail-container.scss b/lab-claudia/app/component/gallery/thumbnail-container/_thumbnail-container.scss new file mode 100644 index 0000000..6718c5a --- /dev/null +++ b/lab-claudia/app/component/gallery/thumbnail-container/_thumbnail-container.scss @@ -0,0 +1,28 @@ +@import "theme"; +@import "vendor"; + +.thumbnail-container { + width: 90%; + margin: 0 5%; + padding: 3%; + margin-bottom: 3%; + background-color: $app-primary; + + h2 { + color: $app-black; + margin: 3%; + padding: 1% 0; + } + + input, + select { + width: 80%; + font-size: 100%; + border: 1.5px solid $app-secondary; + border-radius: 10px; + margin: 3% 10%; + padding: 1% 2%; + color: $app-black; + } + +} diff --git a/lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.html b/lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.html new file mode 100644 index 0000000..d87630a --- /dev/null +++ b/lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.html @@ -0,0 +1,9 @@ +
+

Upload pics to {{ thumbnailContainerCtrl.gallery.name }}

+ + +
+ +
+
+ diff --git a/lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.js b/lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.js new file mode 100644 index 0000000..b00ab21 --- /dev/null +++ b/lab-claudia/app/component/gallery/thumbnail-container/thumbnail-container.js @@ -0,0 +1,14 @@ +'use strict'; + +require('./_thumbnail-container.scss'); + +module.exports = { + template: require('./thumbnail-container.html'), + controllerAs: 'thumbnailContainerCtrl', + // Takes in a gallery via one way data binding + // Binding defines an attribute 'gallery' in the html + // Assigns 'gallery' on the scope of the thumbnail-conatiner + bindings: { + gallery: '<', + }, +}; diff --git a/lab-claudia/app/component/gallery/thumbnail/_thumbnail.scss b/lab-claudia/app/component/gallery/thumbnail/_thumbnail.scss new file mode 100644 index 0000000..84c572e --- /dev/null +++ b/lab-claudia/app/component/gallery/thumbnail/_thumbnail.scss @@ -0,0 +1,9 @@ +@import "theme"; +@import "vendor"; + +.thumbnail { + img { + width: 80%; + margin: 1% 10%; + } +} diff --git a/lab-claudia/app/component/gallery/thumbnail/thumbnail.html b/lab-claudia/app/component/gallery/thumbnail/thumbnail.html new file mode 100644 index 0000000..0782394 --- /dev/null +++ b/lab-claudia/app/component/gallery/thumbnail/thumbnail.html @@ -0,0 +1,10 @@ + + + + +
+ + + {{thumbnailCtrl.pic.desc}} + +
diff --git a/lab-claudia/app/component/gallery/thumbnail/thumbnail.js b/lab-claudia/app/component/gallery/thumbnail/thumbnail.js new file mode 100644 index 0000000..3405d88 --- /dev/null +++ b/lab-claudia/app/component/gallery/thumbnail/thumbnail.js @@ -0,0 +1,26 @@ +'use strict'; + +require('./_thumbnail.scss'); + +// Takes in a pic and a gallery via one way data binding +// Bindings define the attributes 'pic' and 'gallery' in the html +// Assigns 'pic' and 'gallery' on the scope of the thumbnail-container - objects can be passed in + +module.exports = { + template: require('./thumbnail.html'), + controller: ['$log', 'picService', ThumbnailController], + controllerAs: 'thumbnailCtrl', + bindings: { + pic: '<', + gallery: '<', + }, +}; + +function ThumbnailController($log, picService) { + $log.debug('init thumbnailCtrl'); + this.deletePic = function() { + $log.debug('thumbnailCtrl.deletePic'); + // pass in the current gallery and pic id to deleteGalleryPic + picService.deleteGalleryPic(this.gallery, this.pic._id); + }; +} diff --git a/lab-claudia/app/component/gallery/upload-pic/_upload-pic.scss b/lab-claudia/app/component/gallery/upload-pic/_upload-pic.scss new file mode 100644 index 0000000..2c6da80 --- /dev/null +++ b/lab-claudia/app/component/gallery/upload-pic/_upload-pic.scss @@ -0,0 +1,44 @@ +@import "theme"; +@import "vendor"; + +.upload-pic{ + + h2 { + text-align: center; + } + form { + width: 80%; + margin: 2% 10%; + } + + .btn { + background-color: $app-secondary; + border: 1px solid $app-primary; + color: $app-black; + margin: 3%; + } + + .btn:hover { + background-color: $app-white; + } + + input, + select { + width: 90%; + margin: 0 5%; + font-size: 100%; + border:none; + border: 1.5px solid $app-secondary; + margin: 3%; + padding: 1% 0; + color: $app-black; + } + .has-error { + background-color: $app-warn; + color: $app-white; + } + .btn-success { + background-color: $app-success; + } + +} diff --git a/lab-claudia/app/component/gallery/upload-pic/upload-pic.html b/lab-claudia/app/component/gallery/upload-pic/upload-pic.html new file mode 100644 index 0000000..3a19f92 --- /dev/null +++ b/lab-claudia/app/component/gallery/upload-pic/upload-pic.html @@ -0,0 +1,26 @@ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + + +
+
+
diff --git a/lab-claudia/app/component/gallery/upload-pic/upload-pic.js b/lab-claudia/app/component/gallery/upload-pic/upload-pic.js new file mode 100644 index 0000000..8c61573 --- /dev/null +++ b/lab-claudia/app/component/gallery/upload-pic/upload-pic.js @@ -0,0 +1,33 @@ +'use strict'; + +require('./_upload-pic.scss'); + +module.exports = { + template: require('./upload-pic.html'), + controller: ['$log', 'picService', UploadPicController], + controllerAs: 'uploadPicCtrl', + bindings: { + gallery: '<', + }, +}; + +function UploadPicController($log, picService) { + $log.debug('init uploadPicCtrl'); + + this.pic = {}, + + this.done = function(){ + // this.showForm = false; + this.pic.name = null; + this.pic.desc = null; + this.pic.file = null; + }; + + this.uploadPic = function(){ + picService.uploadGalleryPic(this.gallery, this.pic) + .then(() => { + this.done(); + }); + }; + +} diff --git a/lab-claudia/app/component/landing/login/_login.scss b/lab-claudia/app/component/landing/login/_login.scss new file mode 100644 index 0000000..26d2868 --- /dev/null +++ b/lab-claudia/app/component/landing/login/_login.scss @@ -0,0 +1,5 @@ +@import "theme"; + +.form-control { + color: $app-white; +} diff --git a/lab-claudia/app/component/landing/login/login.html b/lab-claudia/app/component/landing/login/login.html new file mode 100644 index 0000000..8fd24af --- /dev/null +++ b/lab-claudia/app/component/landing/login/login.html @@ -0,0 +1,34 @@ + diff --git a/lab-claudia/app/component/landing/login/login.js b/lab-claudia/app/component/landing/login/login.js new file mode 100644 index 0000000..9748731 --- /dev/null +++ b/lab-claudia/app/component/landing/login/login.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = { + template: require('./login.html'), + controller: ['$log', '$location', 'authService', LoginController], + controllerAs: 'loginCtrl', +}; + +function LoginController($log, $location, authService){ + $log.debug('init loginCtrl'); + // login method on loginCtrl + this.login = function(){ + $log.log('loginCtrl.login()'); + // calls authService.login given current user + authService.login(this.user) + .then(() => { + $location.url('/home'); + }); + }; +} diff --git a/lab-claudia/app/component/landing/signup/_signup.scss b/lab-claudia/app/component/landing/signup/_signup.scss new file mode 100644 index 0000000..7cde8d9 --- /dev/null +++ b/lab-claudia/app/component/landing/signup/_signup.scss @@ -0,0 +1 @@ +@import "theme"; diff --git a/lab-claudia/app/component/landing/signup/signup.html b/lab-claudia/app/component/landing/signup/signup.html new file mode 100644 index 0000000..6ebebc0 --- /dev/null +++ b/lab-claudia/app/component/landing/signup/signup.html @@ -0,0 +1,22 @@ + diff --git a/lab-claudia/app/component/landing/signup/signup.js b/lab-claudia/app/component/landing/signup/signup.js new file mode 100644 index 0000000..1f8ac22 --- /dev/null +++ b/lab-claudia/app/component/landing/signup/signup.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = { + template: require('./signup.html'), + controller: ['$log', '$location', 'authService', SignupController], + controllerAs: 'signupCtrl', +}; + +function SignupController($log, $location, authService){ + this.signup = function(user){ + // calls authService.signup- if suceds redirect to homeview + authService.signup(user) + .then(() => { + $location.path('/home'); + }) + .catch(() => { + console.log('Signup Failed'); + }); + }; +} diff --git a/lab-claudia/app/component/navbar/_navbar.scss b/lab-claudia/app/component/navbar/_navbar.scss new file mode 100644 index 0000000..a87ac7a --- /dev/null +++ b/lab-claudia/app/component/navbar/_navbar.scss @@ -0,0 +1,48 @@ +@import "theme"; +@import "vendor"; + +.navbar { + background-color: transparent; + border:none; + height: 60px; + + .btn-logout { + display: none; + float: right; + font-size: 100%; + padding: .75% 1.25%; + margin: 1.5% 2%; + @media (min-width: 500px){ + display: inline-block; + } + } + + .navbar-brand { + color: $app-secondary; + float: left; + margin: 1.5% 2%; + font-size: 170%; + text-decoration: none; + } + + .mobile-nav-icon { + padding: .75% 1.25%; + margin: 1.5% 2%; + display: none; + float: right; + background-color: transparent; + color: $app-black; + transition: 1s; + + @media (max-width: 500px){ + display: inline-block; + } + } + + + .mobile-nav-icon:hover { + color: $app-white; + } + + +} diff --git a/lab-claudia/app/component/navbar/navbar.html b/lab-claudia/app/component/navbar/navbar.html new file mode 100644 index 0000000..312103d --- /dev/null +++ b/lab-claudia/app/component/navbar/navbar.html @@ -0,0 +1,22 @@ + + diff --git a/lab-claudia/app/component/navbar/navbar.js b/lab-claudia/app/component/navbar/navbar.js new file mode 100644 index 0000000..81ee2fc --- /dev/null +++ b/lab-claudia/app/component/navbar/navbar.js @@ -0,0 +1,62 @@ +'use strict'; + +require('./_navbar.scss'); + +module.exports = { + template: require('./navbar.html'), + controller: ['$log', '$location', '$rootScope', 'authService', NavbarController], + controllerAs: 'navbarCtrl', + bindings: { + // @ allows us to pass name in through attribute - string or boolean + appTitle: '@', // appTitle is a property that gets set on the controllers + }, +}; + +function NavbarController($log, $location, $rootScope, authService) { + $log.debug('init navbarCtrl'); + + // Nav logic specific to $location.path() + // If path is /join, hides buttons + this.checkPath = function(){ + let path = $location.path(); + if (path === '/join'){ + this.hideButtons = true; + + // If there is a token, redirect to the home page + authService.getToken() + .then(() => { + $location.url('/home'); + }); + } + + // If not /join, check if there is a token, if not, go back to the join page + if (path !== '/join'){ + this.hideButtons = false; // buttons are un-hidden + authService.getToken() + .catch(() => { + $location.url('/join#login'); + }); + } + }; + + // On pageload, call this.checkPath() + // Run on pageload, as soon as navbar is instantiated + this.checkPath(); + + // On page success page change call this.checkPath() + // Every time url is modified, or anchortag, runs checkPath + // $locationChangeSuccess is an event built into the root scope + $rootScope.$on('$locationChangeSuccess', () => { + this.checkPath(); + }); + +// on logout, redirects to /, which we configured to take you back to join + this.logout = function(){ + $log.log('navbarCtrl.logout()'); + this.hideButtons = true; + authService.logout() + .then(() => { + $location.url('/'); + }); + }; +} diff --git a/lab-claudia/app/component/searchbar/_searchbar.scss b/lab-claudia/app/component/searchbar/_searchbar.scss new file mode 100644 index 0000000..d0561e7 --- /dev/null +++ b/lab-claudia/app/component/searchbar/_searchbar.scss @@ -0,0 +1,3 @@ +@import "theme"; + + diff --git a/lab-claudia/app/component/searchbar/searchbar.html b/lab-claudia/app/component/searchbar/searchbar.html new file mode 100644 index 0000000..4e34166 --- /dev/null +++ b/lab-claudia/app/component/searchbar/searchbar.html @@ -0,0 +1,6 @@ + + + diff --git a/lab-claudia/app/component/searchbar/searchbar.js b/lab-claudia/app/component/searchbar/searchbar.js new file mode 100644 index 0000000..23818cf --- /dev/null +++ b/lab-claudia/app/component/searchbar/searchbar.js @@ -0,0 +1,12 @@ +'use strict'; + +require('./_searchbar.scss'); + +module.exports = { + template: require('./searchbar.html'), + controllerAs: 'searchbarCtrl', + bindings: { + // two way - parent to child and child to parent + searchTerm: '=', + }, +}; diff --git a/lab-claudia/app/config/log-config.js b/lab-claudia/app/config/log-config.js new file mode 100644 index 0000000..683171b --- /dev/null +++ b/lab-claudia/app/config/log-config.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = ['$logProvider', logConfig]; + +function logConfig($logProvider){ + $logProvider.debugEnabled(__DEBUG__); +} diff --git a/lab-claudia/app/config/router-config.js b/lab-claudia/app/config/router-config.js new file mode 100644 index 0000000..cc3dd27 --- /dev/null +++ b/lab-claudia/app/config/router-config.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = ['$stateProvider', '$urlRouterProvider', routerConfig]; + +function routerConfig($stateProvider, $urlRouterProvider){ + // when you go to /signup- redirects to /join#signup + // when you go to /login- redirects to /join#login + $urlRouterProvider.when('' , '/join#signup'); + $urlRouterProvider.when('/' , '/join#signup'); //used in navbar.js + $urlRouterProvider.when('/signup' , '/join#signup'); + $urlRouterProvider.when('/login' , '/join#login'); + + let states = [ + { + name: 'home', + url: '/home', + controllerAs: 'homeCtrl', + controller: 'HomeController', + template: require('../view/home/home.html'), + }, + + { + name: 'welcome', + url: '/join', + controllerAs: 'landingCtrl', + controller: 'LandingController', + template: require('../view/landing/landing.html'), + }, + ]; + + states.forEach(state => { + $stateProvider.state(state); + }); +} diff --git a/lab-claudia/app/entry.js b/lab-claudia/app/entry.js new file mode 100644 index 0000000..1612111 --- /dev/null +++ b/lab-claudia/app/entry.js @@ -0,0 +1,84 @@ +'use strict'; + +// Build sass - gets custom bootstrap and theme +require('./scss/main.scss'); + +// Require node modules +const path = require('path'); + +// Require npm modules +const angular = require('angular'); //create angular module +const camelcase = require('camelcase'); +const pascalcase = require('pascalcase'); + +// Require angular modules +// Injected in modules to do routing and bootstrap +const ngTouch = require('angular-touch'); // Dependency of bootstrap +const ngAnimate = require('angular-animate'); // Dependency of bootstrap +const uiRouter = require('angular-ui-router'); +const uiBootstrap = require('angular-ui-bootstrap'); // Need touch and animate to use +const ngFileUpload = require('ng-file-upload'); + +// Create angular module +// Add everything we need to module +const demoApp = angular.module('demoApp', [ngTouch, ngAnimate, uiRouter, uiBootstrap, ngFileUpload]); + +// WHAT DOES REQUIRE.CONTEXT DO? +////////////////////////////////////// +// 1. Loops through directory +// 2. Gives us path +// 3. Gives us the object to export + +// BASENAME = name of file without folders or .js +// ex: start with app/goose/file.js --> returns 'file' +// true parameter -looks through sub-directories +// Require.context returns a function + +// Returns object that has files that match regex in the config directory and loads it in +// Key - absolute path of where file was found +// Context has a method called keys that returns an array of paths +// For each loops across it and gives us the path name + +// LOAD CONFIG +let context = require.context('./config/', true, /.js$/); +// Goes through config and finds files that end .js +context.keys().forEach( path => { // Gets array of paths - path is the name returned + demoApp.config(context(path)); // Gives us the object (module that is exported) +}); + +// LOAD VIEW CONTROLLERS +context = require.context('./view/', true, /.js$/); +// Looks in view for directories ending in js +context.keys().forEach( key => { + // For every key, take path, and get the base name(home-controller) + // Pascalcase turns it into HomeController + let name = pascalcase(path.basename(key, '.js')); + // Name controller based on file name + // Pass in path into context -returns what was exported + let module = context(key); // value of module.exports + demoApp.controller(name, module); +}); + +// Load services +context = require.context('./service/', true, /.js$/); +context.keys().forEach( key => { + let name = camelcase(path.basename(key, '.js')); + let module = context(key); + demoApp.service(name, module); +}); + +// Load components +context = require.context('./component/', true, /.js$/); +context.keys().forEach( key => { + let name = camelcase(path.basename(key, '.js')); + let module = context(key); + demoApp.component(name, module); +}); + +// Load filters +context = require.context('./filter/', true, /.js$/); +context.keys().forEach( key => { + let name = camelcase(path.basename(key, '.js')); + let module = context(key); + demoApp.filter(name, module); +}); diff --git a/lab-claudia/app/filter/gallery-search.js b/lab-claudia/app/filter/gallery-search.js new file mode 100644 index 0000000..eeece91 --- /dev/null +++ b/lab-claudia/app/filter/gallery-search.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = function(){ + return function(galleries, searchTermName, searchTermDesc){ + + let fuzzyRegexName = generateFuzzyRegex(searchTermName); + let fuzzyRegexDesc = generateFuzzyRegex(searchTermDesc); + + let filteredArray = galleries.filter(gallery => { + return fuzzyRegexName.test(gallery.name.toUpperCase()); + }); + + filteredArray =filteredArray.filter(gallery => { + return fuzzyRegexDesc.test(gallery.desc.toUpperCase()); + }); + + return filteredArray; + }; +}; + +function generateFuzzyRegex(input){ + if (!input) return /.*/; + let fuzzyString = '.*' + input.toUpperCase().split('').join('.*') + '.*'; + return new RegExp(fuzzyString); +} diff --git a/lab-claudia/app/index.html b/lab-claudia/app/index.html new file mode 100644 index 0000000..7e34742 --- /dev/null +++ b/lab-claudia/app/index.html @@ -0,0 +1,16 @@ + + + + + [NuggetGram] + + + + + + + + + + + diff --git a/lab-claudia/app/scss/lib/_theme.scss b/lab-claudia/app/scss/lib/_theme.scss new file mode 100644 index 0000000..bb23208 --- /dev/null +++ b/lab-claudia/app/scss/lib/_theme.scss @@ -0,0 +1,10 @@ +$app-primary: #c4afd6; // darker purple +$app-secondary: #ddd1e7; // lighter purple +$app-disabled: #abeaeb; +$app-active: #fa7fa7; +$app-success: #a7ff7a; +$app-error: #ff7a7a; +$app-warn: #ffa77a; +$app-info: #7aa7ff; +$app-white: #fff; +$app-black: #2a2a2a; //dark grey diff --git a/lab-claudia/app/scss/lib/_vendor.scss b/lab-claudia/app/scss/lib/_vendor.scss new file mode 100644 index 0000000..69583bf --- /dev/null +++ b/lab-claudia/app/scss/lib/_vendor.scss @@ -0,0 +1,2 @@ +// font-awesome +@import "~font-awesome/scss/font-awesome.scss"; diff --git a/lab-claudia/app/scss/main.scss b/lab-claudia/app/scss/main.scss new file mode 100644 index 0000000..d8c788b --- /dev/null +++ b/lab-claudia/app/scss/main.scss @@ -0,0 +1,156 @@ +@import "theme"; +@import "vendor"; + +html, body { + width: 100%; + height: 100%; + font-family: 'Lato', sans-serif; +} + +body { + background: + linear-gradient( + rgba(85,26,139, 0.9), + rgba(0, 36, 99, 0.5) + ), + url(http://i65.tinypic.com/j183uc.jpg) no-repeat center center fixed; + //url(http://i67.tinypic.com/x45c1g.jpg); - fruits background + background-size: cover; +} + +.has-error { + color: $app-error; +} +.has-success { + color: $app-success; +} + + +/*------------- BUTTONS ---------------*/ + +/* General Button Styles */ +.btn { + background-color: rgba(255,255,255,.3); + border: none; + text-transform: uppercase; + border-radius: 3px; + padding: 1.5% 2%; + margin-top: 3%; + display: inline-block; + outline: none; + transition: all 0.3s; + color: $app-secondary; +} +.btn:hover { + background:rgba(255,255,255,1); + color: $app-black; +} +.btn:after { + content: ''; + position: absolute; + z-index: -1; + transition: all 0.3s; +} + +/* SECTIONS ============================================================================= */ +.section { + clear: both; + padding: 0px; + margin: 0px; +} + +/* --------- Clearfix ------------ */ +.clearfix {zoom: 1;} +.clearfix:after { + content: '.'; + clear: both; + display: block; + height: 0; + visibility: hidden; +} + +/*makes every element include the margin and padding you add to it */ +* { + margin:0; + padding:0; + box-sizing: border-box; +} +/* GROUPING ============================================================================= */ + +.row { + max-width: 1140px; + margin: 0 auto; + zoom: 1; /* For IE 6/7 (trigger hasLayout) */ +} + +.row:before, +.row:after { + content:""; + display:table; +} +.row:after { + clear:both; +} + +/* GRID COLUMN SETUP ==================================================================== */ +.col { + display: block; + float:left; + margin: 1% 0 1% 1.6%; +} +.col:first-child { margin-left: 0; } /* all browsers except IE6 and lower */ + + +/* REMOVE MARGINS AS ALL GO FULL WIDTH AT 480 PIXELS */ +@media only screen and (max-width: 480px) { + .col { + /*margin: 1% 0 1% 0%;*/ + margin: 0; + } +} + + +/* GRID OF TWO ============================================================================= */ +.span-2-of-2 { + width: 100%; +} +.span-1-of-2 { + width: 49.2%; +} + +/* GO FULL WIDTH AT LESS THAN 480 PIXELS */ + +@media only screen and (max-width: 480px) { + .span-2-of-2 { + width: 100%; + } + .span-1-of-2 { + width: 100%; + } +} + + +/* GRID OF THREE ============================================================================= */ +.span-3-of-3 { + width: 100%; +} +.span-2-of-3 { + width: 66.13%; +} +.span-1-of-3 { + width: 32.26%; +} + + +/* GO FULL WIDTH AT LESS THAN 480 PIXELS */ +@media only screen and (max-width: 480px) { + .span-3-of-3 { + width: 100%; + } + .span-2-of-3 { + width: 100%; + } + .span-1-of-3 { + width: 100%; + } +} diff --git a/lab-claudia/app/service/artist-service.js b/lab-claudia/app/service/artist-service.js new file mode 100644 index 0000000..68f25cc --- /dev/null +++ b/lab-claudia/app/service/artist-service.js @@ -0,0 +1,132 @@ +// post route to create a profile when you signup +// get route that gets an artist's profile by id +// put route that updates an artist's profile +// delete route that deletes a an artist profile +'use strict'; + +module.exports = ['$q', '$log', '$http', 'authService', artistService]; + +function artistService($q, $log, $http, authService){ + $log.debug('init artistService'); + let service = {}; + + service.artists = []; + + service.createArtist = function(artist){ + $log.debug('artistService.createArtist()'); + + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/artist`; + let config = { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }; + return $http.post(url, artist, config); + }) + + .then( res => { + $log.log('Succesfully created artist'); + let artist = res.data; + service.artists.unshift(artist); + return artist; + }) + + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.deleteArtist = function(artistID){ + return authService.getToken() + .then(token => { + let url = `${__API_URL__}/api/artist/${artistID}`; + let config = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + return $http.delete(url, config); + }) + .then(() => { + $log.log('sucessful deletion'); + for(let i = 0; i < service.artists.length; i++){ + let current = service.artists[i]; + if(current._id === artistID){ + service.artists.splice(i,1); + break; + } + } + return $q.resolve('Success!'); + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.fetchArtists = function(){ + $log.debug('artistService.fetchArtists()'); + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/artist/?sort=desc`; + let config = { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }; + return $http.get(url, config); + }) + .then( res => { + $log.log('Succesfully fetched artist profiles'); + service.artists = res.data; + return service.artists; + }) + .catch(err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.updateArtist = function(artist, artistID){ + $log.debug('artistService.updateArtist()'); + // call authService to get token + return authService.getToken() + // returns token + .then( token => { + // sends info to server + let url = `${__API_URL__}/api/artist/${artistID}`; + let config = { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }; + return $http.put(url, artist, config); + }) + + .then( res => { + + for(let i = 0; i < service.artists.length; i++){ + if (service.artists[i]._id === artistID) { + service.artists[i] = res.data; + break; + } + } + $log.log('Successfuly updated artist profile'); + return $q.resolve('Updated!'); + }) + .catch(err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + return service; +} diff --git a/lab-claudia/app/service/auth-service.js b/lab-claudia/app/service/auth-service.js new file mode 100644 index 0000000..77af826 --- /dev/null +++ b/lab-claudia/app/service/auth-service.js @@ -0,0 +1,85 @@ +'use strict'; + +module.exports = ['$q', '$log', '$http', '$window', authService]; + +function authService($q, $log, $http, $window){ + $log.debug('init authService'); + // create service + let service = {}; + service.token = null; + + service.setToken = function(_token){ + $log.debug('authService.service.setToken()'); + if (! _token) + return $q.reject(new Error('no service.token')); + $window.localStorage.setItem('service.token', _token); + service.token = _token; + return $q.resolve(service.token); + }; + + service.getToken = function(){ + $log.debug('authService.getToken'); + if (service.token) return $q.resolve(service.token); + service.token = $window.localStorage.getItem('service.token'); + if (service.token) return $q.resolve(service.token); + return $q.reject(new Error('service.token not found')); + }; + + service.logout = function(){ + $log.debug('authService.logout()'); + $window.localStorage.removeItem('service.token'); + service.token = null; + return $q.resolve(); + }; + + service.signup = function(user) { + $log.debug('authService.signup()'); + let url = `${__API_URL__}/api/signup`; + console.log('signup url', url); + + let config = { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + }; + + return $http.post(url, user, config) + .then( res => { + $log.log('success', res.data); + // res.data is the response body aka the service.token + return service.setToken(res.data); + }) + .catch(err => { + $log.error('fail', err.message); + return $q.reject(err); + }); + }; + + service.login = function(user){ + $log.debug('authService.login()'); + let url = `${__API_URL__}/api/login`; + // base64 encoded 'username:password' + let base64 = $window.btoa(`${user.username}:${user.password}`); + + let config = { + headers: { + Accept: 'application/json', + Authorization: `Basic ${base64}`, + }, + }; + + return $http.get(url, config) + .then( res => { + $log.log('success', res.data); + return service.setToken(res.data); + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + // return service + return service; +} diff --git a/lab-claudia/app/service/gallery-service.js b/lab-claudia/app/service/gallery-service.js new file mode 100644 index 0000000..939eede --- /dev/null +++ b/lab-claudia/app/service/gallery-service.js @@ -0,0 +1,142 @@ +'use strict'; + +module.exports = ['$q', '$log', '$http', 'authService', galleryService]; + +function galleryService($q, $log, $http, authService){ + $log.debug('init galleryService'); + let service = {}; + + service.galleries = []; + + service.createGallery = function(gallery){ + $log.debug('galleryService.createGallery()'); + + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/gallery`; + let config = { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }; + return $http.post(url, gallery, config); + }) + + .then( res => { + $log.log('Succesfully created gallery'); + let gallery = res.data; + service.galleries.unshift(gallery); + return gallery; + }) + + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.deleteGallery = function(galleryID){ + return authService.getToken() + .then(token => { + let url = `${__API_URL__}/api/gallery/${galleryID}`; + let config = { + headers: { + //don't need accept- not getting anything back from server + //Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }; + //ajax makes request to any url + //we are pointing the url to our API + return $http.delete(url, config); + }) + .then(() => { + $log.log('sucessful deletion'); + for(let i = 0; i < service.galleries.length; i++){ + let current = service.galleries[i]; + if(current._id === galleryID){ + service.galleries.splice(i,1); + break; + } + } + return $q.resolve('success'); + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.fetchGalleries = function(){ + $log.debug('galleryService.fetchGalleries()'); + return authService.getToken() + .then( token => { + // __API_URL__ is set in the .env file + let url = `${__API_URL__}/api/gallery/?sort=desc`; + // can configure with querystring sort gallery/?sort=asc, sort=desc + // sorts newest to oldest + let config = { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }; + // Re-triggers the ng-repeat so all the galleries show up + // Goes from [] to [with a bunch of galleries] + return $http.get(url, config); + }) + .then( res => { + $log.log('successful fetch user galleries'); + service.galleries = res.data; + return service.galleries; + }) + .catch(err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.updateGallery = function(gallery, galleryID){ + $log.debug('galleryService.updateGalleries()'); + // call authService to get token + return authService.getToken() + // returns token + .then( token => { + // sends info to server + let url = `${__API_URL__}/api/gallery/${galleryID}`; + let config = { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }; + // ajax request + // makes request to API, parses, updates and responds + return $http.put(url, gallery, config); + }) + // get the response + .then( res => { + // iterate over gallery Controller + // update the gallery in service.galleries + for(let i = 0; i < service.galleries.length; i++){ + if (service.galleries[i]._id === galleryID) { + service.galleries[i] = res.data; + break; + } + } + // if sucessful, returns updated gallery and resolves the promise + $log.log('successful update user gallery'); + return $q.resolve('updated'); + }) + // if not, rejects the promise and logs an error + .catch(err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + return service; +} diff --git a/lab-claudia/app/service/pic-service.js b/lab-claudia/app/service/pic-service.js new file mode 100644 index 0000000..44debfc --- /dev/null +++ b/lab-claudia/app/service/pic-service.js @@ -0,0 +1,122 @@ +'use strict'; + +module.exports = ['$q','$log', '$http', 'Upload', 'authService', picService]; + +function picService($q, $log, $http, Upload, authService) { + $log.debug('init picService'); + let service = {}; + +// Route for making an upload to Gallery + service.uploadGalleryPic = function(galleryData, picData) { + $log.debug('picService.uploadGalleryPic()'); + // Return promise + return authService.getToken() + // Get token + .then( token => { + let url = `${__API_URL__}/api/gallery/${galleryData._id}/pic`; + let headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }; + // Don't need key value pairs with object literals in ES6 + return Upload.upload({ + url, + headers, + method: 'POST', + // Information expected to be uploaded (required in the back-end) + // Info is being sent as multi-part form data + data: { + name: picData.name, + desc: picData.desc, + file: picData.file, + }, + }); + }) + // Get a response that returns an image + .then( res => { + // Add pic to the front of the array + galleryData.pics.unshift(res.data); + $log.log('success!\n', res.data); + return res.data; + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + +// Upload an artist profile picture + service.uploadArtistPic = function(artistData, picData) { + $log.debug('picService.uploadArtistPic()'); + + return authService.getToken() + + .then( token => { + let url = `${__API_URL__}/api/artist/${artistData._id}/photo`; + let headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }; + return Upload.upload({ + url, + headers, + method: 'POST', + data: { + name: picData.name, + alt: picData.alt, + file: picData.file, + }, + }); + }) + // Get a response that returns an image + .then( res => { + $log.log('success!\n', res.data); + return res.data; + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + +// Route for deleting a pic - (this.gallery, this.pic._id) in controller + service.deleteGalleryPic = function(galleryData, picID) { + $log.debug('picService.deleteGalleryPic()'); + // Get a token from the auth service + return authService.getToken() + .then(token => { + // Set the url to __API_URL__/api/gallery/:galleryID/pic/:picID + let url = `${__API_URL__}/api/gallery/${galleryData._id}/pic/${picID}`; + // Set config for http headers + let config = { + // Set authorization header + headers: { + Authorization: `Bearer ${token}`, + }, + }; + // Make $http.delete request to url with config + return $http.delete(url, config); + }) + + // On success, splice the pic out of the galleryData.pics array + .then(() => { + $log.log('Deleted pic sucessfully'); + for(let i = 0; i < galleryData.pics.length; i++){ + let current = galleryData.pics[i]; + if(current._id === picID){ + galleryData.pics.splice(i,1); + break; + } + } + // Resolve undefined + return $q.resolve(undefined); + }) + // On error log error and reject error + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + return service; +} diff --git a/lab-claudia/app/view/home/_home.scss b/lab-claudia/app/view/home/_home.scss new file mode 100644 index 0000000..849ae08 --- /dev/null +++ b/lab-claudia/app/view/home/_home.scss @@ -0,0 +1,49 @@ +@import "theme"; + +.home { + width: 100%; + margin: 0 auto; + + .searchbar-container { + width: 100%; + background-color: $app-black; + + h2 { + font-size: 80%; + text-align: center; + padding: 5%; + color: $app-white; + } + .searchbar { + input { + width: 60%; + border: 1.5px solid $app-secondary; + margin: 3%; + padding: 1% 0; + border-radius: 10px; + color: $app-black; + } + button { + width: 30%; + } + } + + ul { + padding: 0; + } + + li{ + float: left; + padding: 1%; + } + + } + + ul { + padding: 0px; + } + li { + list-style: none; + } + +} diff --git a/lab-claudia/app/view/home/home-controller.js b/lab-claudia/app/view/home/home-controller.js new file mode 100644 index 0000000..00776ad --- /dev/null +++ b/lab-claudia/app/view/home/home-controller.js @@ -0,0 +1,18 @@ +'use strict'; + +require('./_home.scss'); + +module.exports = ['$log', HomeController ]; + +function HomeController($log){ + $log.debug('init homeCtrl'); + + let googleAuthBase = 'https://accounts.google.com/o/oauth2/v2/auth'; + let googleAuthResponseType = 'response_type=code'; + let googleAuthClientID = `client_id=${__GOOGLE_CLIENT_ID__}`; + let googleAuthScope = 'scope=profile%20email%20openid'; + let googleAuthRedirectURI = 'redirect_uri=http://localhost:3000/api/auth/oauth_callback'; + let googleAuthAccessType = 'access_type=offline'; + + this.googleAuthURL = `${googleAuthBase}?${googleAuthResponseType}&${googleAuthClientID}&${googleAuthScope}&${googleAuthRedirectURI}&${googleAuthAccessType}`; +} diff --git a/lab-claudia/app/view/home/home.html b/lab-claudia/app/view/home/home.html new file mode 100644 index 0000000..946f2f3 --- /dev/null +++ b/lab-claudia/app/view/home/home.html @@ -0,0 +1,43 @@ +
+ + + + + +
    +
  • Search Galleries:

  • +
  • +
  • +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + +
diff --git a/lab-claudia/app/view/landing/_landing.scss b/lab-claudia/app/view/landing/_landing.scss new file mode 100644 index 0000000..bfc881c --- /dev/null +++ b/lab-claudia/app/view/landing/_landing.scss @@ -0,0 +1,98 @@ +@import "theme"; + +.landing { + .join-inner-container { + padding: 5%; + margin-top: 5%; + margin-left: auto; + margin-right: auto; + width: 100%; + background-color: transparent; + border: 1.5px solid $app-primary; + border-radius: 10px; + transition: 1s; + text-transform: uppercase; + + a { + color: $app-secondary; + } + a:hover { + color: $app-white; + } + + input { + margin-top: 2%; + + &[type='submit'] { + color: $app-secondary; + } + } + + /*--------- Input Styles --------*/ + input, + select { + width: 100%; + font-size: 100%; + background-color: transparent; + text-transform: uppercase; + border:none; + border-bottom: 1.5px solid $app-primary; + margin: 3%; + padding: 1% 0; + color: $app-white; + } + + input:focus{ + outline: none; + } + /*---- Changes Placeholder Color -----*/ + input::-webkit-input-placeholder, + textarea::-webkit-input-placeholder { + color: $app-primary; + } + + + /* General Button Styles */ + .btn { + background-color: rgba(255,255,255,.3); + border: none; + text-transform: uppercase; + border-radius: 3px; + font-size: 120%; + padding: 1.5% 2%; + margin-top: 3%; + width: 25%; + transition: all 0.3s; + color: $app-secondary; + } + .btn:hover { + background:rgba(255,255,255,1); + color: $app-black; + } + + p { + padding: 2%; + display: inline-block; + color: $app-white; + } + + a { + display: inline-block; + font-size: 1.2em; + text-decoration: none; + } + + @media (min-width: 400px) { + width: 90%; + } + + @media (min-width: 600px) { + width: 65%; + } + + @media (min-width: 1000px) { + width: 50%; + } + + } +} diff --git a/lab-claudia/app/view/landing/landing-controller.js b/lab-claudia/app/view/landing/landing-controller.js new file mode 100644 index 0000000..93b5a59 --- /dev/null +++ b/lab-claudia/app/view/landing/landing-controller.js @@ -0,0 +1,21 @@ +'use strict'; + +require('./_landing.scss'); + +module.exports = ['$log', '$location', '$rootScope', LandingController]; + +function LandingController($log, $location){ + let url = $location.url(); + this.showSignup = url === '/join#signup' || url === '/join'; + + let googleAuthBase = 'https://accounts.google.com/o/oauth2/v2/auth'; + let googleAuthResponseType = 'response_type=code'; + let googleAuthClientID = `client_id=${__GOOGLE_CLIENT_ID__}`; + let googleAuthScope = 'scope=profile%20email%20openid'; + let googleAuthRedirectURI = 'redirect_uri=http://localhost:3000/api/auth/oauth_callback'; + let googleAuthAccessType = 'access_type=offline'; + + this.googleAuthURL = `${googleAuthBase}?${googleAuthResponseType}&${googleAuthClientID}&${googleAuthScope}&${googleAuthRedirectURI}&${googleAuthAccessType}`; + + +} diff --git a/lab-claudia/app/view/landing/landing.html b/lab-claudia/app/view/landing/landing.html new file mode 100644 index 0000000..8fcdff4 --- /dev/null +++ b/lab-claudia/app/view/landing/landing.html @@ -0,0 +1,31 @@ +
+ + + Login with Google + + +
+
+ +

Already a member?

+ + Login + +
+
+ +
+
+ +

Not a member yet?

+ + Signup + +
+
+ +
diff --git a/lab-claudia/client-test/auth-service-test.js b/lab-claudia/client-test/auth-service-test.js new file mode 100644 index 0000000..0d30678 --- /dev/null +++ b/lab-claudia/client-test/auth-service-test.js @@ -0,0 +1,105 @@ +'use strict'; + +describe('testing authService', function(){ + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject((authService, $httpBackend, $q, $window, $rootScope) => { + this.authService = authService; + authService.setToken('1234'); + this.$httpBackend = $httpBackend; + this.$q = $q; + this.$window = $window; + this.$rootScope = $rootScope; + }); + }); + + afterEach(() => { + this.authService.setToken(null); + this.$window.localStorage.clear(); + }); + + describe('testing authService.setToken()', () => { + it('should return a token', () => { + this.authService.token = 'myspecialtoken'; + + this.authService.getToken() + .then(token => { + expect(token).toEqual('myspecialtoken'); + }); + + this.$rootScope.$apply(); + }); + }); + + describe('testing #getToken()', () => { + it('should return a token', () => { + + this.authService.token = null; + this.$window.localStorage.setItem('token', '1234'); + + this.authService.getToken() + .then( token => { + expect(token).toEqual('1234'); + }) + .catch( err => { + expect(err).toEqual(null); + }); + + this.$rootScope.$apply(); + }); + }); + + describe('testing authService.login(user)', () => { + it('should return a user', () => { + + let userData = { + username: 'GooseMan', + email:'bread@tasty.com', + password: '1234567', + }; + + // Basic auth says you set a base64 endcoded header with user and password + let base64 = this.$window.btoa(`${userData.username}:${userData.password}`); + // we are expecting to get this as the header + let headers = { + Accept: 'application/json', + Authorization: `Basic ${base64}`, + }; + this.$httpBackend.expectGET('http://localhost:3000/api/login', headers) + // it should respond with the correct information + // this is what the server would actually send you + .respond(200, {_id:'5678', username: userData.username, email:userData.email}); + + // make the request to the backend + this.authService.login(userData); + //flush the backend + this.$httpBackend.flush(); + }); + }); + + describe('testing authService.signup(user)', () => { + it('should return a new user', () => { + // test userData + let userData = { + username: 'GooseMan', + email:'bread@tasty.com', + }; + // Test headers + let headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + this.$httpBackend.expectPOST('http://localhost:3000/api/signup', userData, headers) + // $http needs to be flushed to mock asynchronous requests + // It accumulates requests to the server and responds to everything at once + .respond(200, {_id:'5678', username: userData.username, email:userData.email}); + + // Make the request to the server + this.authService.signup(userData); + // Flush the backend + this.$httpBackend.flush(); + }); + }); + +}); // end first describe block diff --git a/lab-claudia/client-test/create-gallery-controller-test.js b/lab-claudia/client-test/create-gallery-controller-test.js new file mode 100644 index 0000000..5cc66ab --- /dev/null +++ b/lab-claudia/client-test/create-gallery-controller-test.js @@ -0,0 +1,61 @@ +'use strict'; + +describe('testing create-gallery controller', function(){ + + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject(($componentController, $rootScope, $httpBackend, authService) => { + authService.setToken('secret'); + + this.$componentController = $componentController; + this.$rootScope = $rootScope; + this.authService = authService; + this.$httpBackend = $httpBackend; + }); + }); + + afterEach(() => { + this.authService.logout(); + }); + + describe('testing #createGallery()', () => { + it('should return a created gallery', () => { + + let url = 'http://localhost:3000/api/gallery'; + + let headers = { + Authorization: 'Bearer secret', + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + let galleryData = { + name: 'exampleGallery', + desc: 'stuff', + }; + + // Make a POST request to the server + this.$httpBackend.expectPOST(url, galleryData, headers) + .respond(200); + + // Create an instance of the createGalleryCtrl + let createGalleryCtrl = this.$componentController('createGallery', null); + + // this.gallery in the component is set to {} + // We pass it galleryData representing a created gallery + createGalleryCtrl.gallery = galleryData; + + // Call createGallery on instance of controller + createGalleryCtrl.createGallery(); + + // Flush the backend + this.$httpBackend.flush(); + + // Make sure that the name and desc are set to null after a gallery has been created + expect(createGalleryCtrl.gallery.name).toEqual(null); + expect(createGalleryCtrl.gallery.desc).toEqual(null); + + this.$rootScope.$apply(); + }); + }); + +}); // end first describe block diff --git a/lab-claudia/client-test/edit-gallery-controller-test.js b/lab-claudia/client-test/edit-gallery-controller-test.js new file mode 100644 index 0000000..3ad7842 --- /dev/null +++ b/lab-claudia/client-test/edit-gallery-controller-test.js @@ -0,0 +1,78 @@ +'use strict'; + +describe('testing edit-gallery controller', function(){ + + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject(($componentController, $rootScope, $httpBackend, authService) => { + authService.setToken('1234'); + + this.$componentController = $componentController; + this.$rootScope = $rootScope; + this.$httpBackend = $httpBackend; + this.authService = authService; + }); + }); + afterEach(() => { + // calling logout removes token from local storage! + this.authService.logout(); + }); + + it('testing component bindings', () => { + // Set up object to be passed in + let mockBindings = { + gallery: { + name: 'goose', + desc: 'lary', + }, + }; + + // Mock the gallery and pass in name of component + // Second argument is set if we want to override locals + let editGalleryCtrl = this.$componentController('editGallery', null, mockBindings); + expect(editGalleryCtrl.gallery.name).toEqual('goose'); + expect(editGalleryCtrl.gallery.desc).toEqual('lary'); + + this.$rootScope.$apply(); + }); + + describe('testing #updateGallery', () => { + + it('should make a valid PUT request', () => { + + let url = 'http://localhost:3000/api/gallery/12345'; + // Mock headers + let headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Bearer 1234', + }; + + // Mock PUT request to server + // Can put expectPUT any where before you call flush + // Backend has to be registered before you make the request + this.$httpBackend.expectPUT(url, {_id: '12345', name: 'newname', desc: 'lary'}, headers) + // expect server to respond with a 200 status code + .respond(200); + + // Mock object to be passed in via bindings + let mockBindings = { + gallery: { + _id: '12345', + name: 'goose', + desc: 'lary', + }, + }; + + // Mock component - create an instance of component with the mocked bindings + let editGalleryCtrl = this.$componentController('editGallery', null, mockBindings); + // Mock updated name + editGalleryCtrl.gallery.name = 'newname'; + // Call updateGallery() on instance of editGalleryCtrl- + editGalleryCtrl.updateGallery(); + // Flush the backend - tells httpBackend to respond to the request + this.$httpBackend.flush(); + this.$rootScope.$apply(); // + }); + }); +}); diff --git a/lab-claudia/client-test/example-test.js b/lab-claudia/client-test/example-test.js new file mode 100644 index 0000000..99c7a31 --- /dev/null +++ b/lab-claudia/client-test/example-test.js @@ -0,0 +1,7 @@ +'use strict'; + +describe('true test', function(){ + it('should pass', () => { + expect(true).toEqual(true); + }); +}); diff --git a/lab-claudia/client-test/gallery-li-controller-test.js b/lab-claudia/client-test/gallery-li-controller-test.js new file mode 100644 index 0000000..a288506 --- /dev/null +++ b/lab-claudia/client-test/gallery-li-controller-test.js @@ -0,0 +1,87 @@ +'use strict'; + +describe('testing gallery-li controller', function(){ + + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject(($componentController, $rootScope, $httpBackend, authService) => { + // Need to mock a token because galleryService requires authorization to make a request + authService.setToken('secret'); + this.$componentController = $componentController; + this.$rootScope = $rootScope; + this.authService = authService; + this.$httpBackend = $httpBackend; + }); + }); + + afterEach( () => { + // If there was a request statement that nobody makes a request to, throw an error + this.$httpBackend.verifyNoOutstandingExpectation(); + // For when statements + this.$httpBackend.verifyNoOutstandingRequest(); + }); + + afterEach(() => { + // Clear the token after every test + this.authService.logout(); + }); + + describe('testing #deleteDone()', () => { + it('should call deleteDone', () => { + // mock bindings + let mockBindings = { + gallery: { + _id: '65432ONE', + name: 'Lary', + desc: 'day in the park with lary', + pics: [], + }, + // function mimics the one we are passing into deleteDone(); + // & requires a function to be passed through attribute + // they keys line up to what is passed in on the template + deleteDone: function(data){ + expect(data.galleryData._id).toEqual('65432ONE'); + }, + }; + + let galleryLICtrl = this.$componentController('galleryLi', null, mockBindings); + // object passed in should match what is in the home controller + galleryLICtrl.deleteDone({galleryData: galleryLICtrl.gallery}); + + this.$rootScope.$apply(); + }); + + it('should call deleteDone with gallery after galleryDelete', () => { + + let url = 'http://localhost:3000/api/gallery/1234567'; + // Mock headers + let headers = { + Authorization: 'Bearer secret', + Accept: 'application/json, text/plain, */*', + }; + + // set up object to be passed in + let mockBindings = { + gallery: { + _id: '1234567', + name: 'goose', + desc: 'lary', + pics: [], + }, + deleteDone: function(data) { + expect(data.galleryData._id).toEqual(mockBindings.gallery._id); + }, + }; + + this.$httpBackend.expectDELETE(url, headers).respond(204); + + let galleryLICtrl = this.$componentController('galleryLi', null, mockBindings); + galleryLICtrl.deleteGallery(); //deleteGallery calls deleteDone() + + // Flush the backend + this.$httpBackend.flush(); + this.$rootScope.$apply(); + }); + }); + +}); // end first describe block diff --git a/lab-claudia/client-test/gallery-service-test.js b/lab-claudia/client-test/gallery-service-test.js new file mode 100644 index 0000000..1e6f8f9 --- /dev/null +++ b/lab-claudia/client-test/gallery-service-test.js @@ -0,0 +1,149 @@ +'use strict'; + +// Testing if the front-end makes the correct requests to the back-end +// beforeEach that mocks the demoApp module and services +// First we mock the demoApp to get access to services +// Angular is a global, so it doesn't have to be required in (set in the .eslintrc file) +// Mock is loaded in karma config - inject is a method that can inject services +// We need to mock a token for requests to work +// We meed to mock a gallery service and back end +// Want the tests to run independently of backend +// $httpBackend - our backend has to be turned off for this to work +// expectGET(POST...)(url, [headers], [keys]) +// Expect each request to respond with expected content and headers + +describe('testing galleryService', function(){ + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject((authService, galleryService, $httpBackend, $window, $rootScope) => { + this.authService = authService; + authService.setToken('1234'); + this.galleryService = galleryService; + this.$httpBackend = $httpBackend; + this.$window = $window; + this.$rootScope = $rootScope; + }); + }); + + afterEach(() => { + this.authService.setToken(null); + this.$window.localStorage.clear(); + }); + + describe('testing galleryService.createGallery()', () => { + it('should return a gallery', () => { + + // Mocking the data and headers + // When the backend makes a request, it should have this data + let galleryData = { + name: 'exampleGallery', + desc: 'stuff', + }; + let headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: 'Bearer 1234', + }; + + // Mock the server route + // Expect that a POST request is made to the expected url with expected data and headers + this.$httpBackend.expectPOST('http://localhost:3000/api/gallery', galleryData, headers) + + // We are testing if the server responds with the correct information + // This is what the backend would actually send you + .respond(200, {_id:'5678', username:'loom', name: galleryData.name, desc: galleryData.desc, pics: []}); + + // Make the request to the backend + this.galleryService.createGallery(galleryData); + // Flush the backend + this.$httpBackend.flush(); + }); + }); + + describe('testing galleryService.deleteGallery(galleryID)', () => { + it('should succeed in deleting gallery', () => { + // 1. mock the request + let galleryID = 'helloworld'; + let headers = { + // delete route has no content type + Authorization: 'Bearer 1234', //only testing if they send us a token + Accept: 'application/json, text/plain, */*', + }; + + // 2. mock server route - use gallery/helloworld because that is the test ID we set + this.$httpBackend.expectDELETE('http://localhost:3000/api/gallery/helloworld', headers) + .respond(204); + + // 3. make request to the server + this.galleryService.deleteGallery(galleryID); + + // 4. flush the server mock + this.$httpBackend.flush(); + this.$rootScope.$apply(); + }); + }); + + describe('testing galleryService.fetchGalleries()', () => { + it('should return galleries', () => { + + // 1. Mock the request + let galleries = [ + { + name: 'gal1', + desc: 'yay', + }, + { + name: 'gal2', + desc: 'yay2', + }, + ]; + + let headers = { + Accept: 'application/json', + Authorization: 'Bearer 1234', + }; + // 2. Mock server route + this.$httpBackend.expectGET('http://localhost:3000/api/gallery/?sort=desc', headers) + + .respond(200, galleries); + + // 3. Make request + this.galleryService.fetchGalleries() + .then( galleries => { + expect(galleries.length).toEqual(2); + expect(galleries[0].name).toBe('gal1'); + }); + // 4. Flush the server mock + this.$httpBackend.flush(); + }); + }); + + describe('testing galleryService.updateGallery(galleryData, galleryID)', () => { + it('should return updated gallery', () => { + let url = 'http://localhost:3000/api/gallery/helloworld'; + // 1. Mock the request + let galleryData = { + name: 'exampleGallery', + desc: 'stuff', + }; + + let galleryID = 'helloworld'; + + let headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Bearer 1234', + }; + + // 2. Mock server route + this.$httpBackend.expectPUT(url, {name: 'exampleGallery', desc: 'stuff'}, headers) + .respond(200); + + // 3. Make request + this.galleryService.updateGallery(galleryData, galleryID); + // 4. Flush the server mock + this.$httpBackend.flush(); + }); + }); + +}); // end first describe block diff --git a/lab-claudia/client-test/home-controller-test.js b/lab-claudia/client-test/home-controller-test.js new file mode 100644 index 0000000..e69de29 diff --git a/lab-claudia/client-test/navbar-controller-test.js b/lab-claudia/client-test/navbar-controller-test.js new file mode 100644 index 0000000..aab417e --- /dev/null +++ b/lab-claudia/client-test/navbar-controller-test.js @@ -0,0 +1,20 @@ +'use strict'; + +describe('testing navbarCtrl', function(){ + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject((authService, $httpBackend, $q, $window, $rootScope) => { + authService.setToken('1234'); + + this.authService = authService; + this.$httpBackend = $httpBackend; + this.$q = $q; + this.$window = $window; + this.$rootScope = $rootScope; + }); + }); + + + + +}); // end testing navbarCtrl diff --git a/lab-claudia/client-test/thumbnail-container-controller-test.js b/lab-claudia/client-test/thumbnail-container-controller-test.js new file mode 100644 index 0000000..0b41cc9 --- /dev/null +++ b/lab-claudia/client-test/thumbnail-container-controller-test.js @@ -0,0 +1,26 @@ +'use strict'; + +describe('testing thumbnail-container controller', function(){ + + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject(($componentController, $rootScope) => { + this.$componentController = $componentController; + this.$rootScope = $rootScope; + }); + }); + + it('testing component bindings', () => { + let mockBindings = { + gallery: { + name: 'goose', + desc: 'lary', + }, + }; + let thumbnailContainerCtrl = this.$componentController('thumbnailContainer', null, mockBindings); + // console.log('editGalleryCtrl', editGalleryCtrl); + expect(thumbnailContainerCtrl.gallery.name).toEqual('goose'); + expect(thumbnailContainerCtrl.gallery.desc).toEqual('lary'); + this.$rootScope.$apply(); + }); +}); diff --git a/lab-claudia/client-test/thumbnail-controller-test.js b/lab-claudia/client-test/thumbnail-controller-test.js new file mode 100644 index 0000000..d0329df --- /dev/null +++ b/lab-claudia/client-test/thumbnail-controller-test.js @@ -0,0 +1,75 @@ +'use strict'; + +describe('testing thumbnail controller', function(){ + + beforeEach(() => { + angular.mock.module('demoApp'); + angular.mock.inject(($componentController, $rootScope, $httpBackend, authService) => { + authService.setToken('1234'); + + this.$componentController = $componentController; + this.$rootScope = $rootScope; + this.$httpBackend = $httpBackend; + this.authService = authService; + }); + }); + afterEach(() => { + this.authService.logout(); + }); + + + it('testing component bindings', () => { + let mockBindings = { + gallery: { + name: 'goose', + desc: 'lary', + }, + pic: { + name: 'picture', + desc: 'tasty', + }, + }; + let thumbnailCtrl = this.$componentController('thumbnail', null, mockBindings); + expect(thumbnailCtrl.gallery.name).toEqual('goose'); + expect(thumbnailCtrl.gallery.desc).toEqual('lary'); + expect(thumbnailCtrl.pic.name).toEqual('picture'); + expect(thumbnailCtrl.pic.desc).toEqual('tasty'); + this.$rootScope.$apply(); + }); + + + // describe('testing #deleteGallery', () => { + // + // it('should make a valid DELETE request', () => { + // + // let url = 'http://localhost:3000/api/pic/12345'; + // let headers = { + // Accept: 'application/json', + // 'Content-Type': 'application/json', + // Authorization: 'Bearer 1234', + // }; + // + // // Mock server route + // this.$httpBackend.expectDELETE(url, headers).respond(204); + // + // let mockBindings = { + // gallery: { + // _id: 1234, + // name: 'goose', + // desc: 'lary', + // }, + // pic: { + // _id: 12345, + // name: 'picture', + // desc: 'tasty', + // }, + // }; + // + // let thumbnailCtrl = this.$componentController('thumbnail', null, mockBindings); + // thumbnailCtrl.deletePic(); + // + // this.$httpBackend.flush(); + // this.$rootScope.$apply(); + // }); + // }); +}); diff --git a/lab-claudia/gulpfile.js b/lab-claudia/gulpfile.js new file mode 100644 index 0000000..e40b453 --- /dev/null +++ b/lab-claudia/gulpfile.js @@ -0,0 +1,24 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const mocha = require('gulp-mocha'); + +gulp.task('lint', function() { + return gulp.src(['**/*.js', '!node_modules/**']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('test', function() { + gulp.src('./test/*-test.js', {read:false}) + .pipe(mocha({reporter:'landing'})); +}); + +gulp.task('default', ['test', 'lint']); + +gulp.task('dev', function() { + gulp.watch(['**/*.js', '!node_modules/**'], + ['lint', 'test']); +}); diff --git a/lab-claudia/karma.conf.js b/lab-claudia/karma.conf.js new file mode 100644 index 0000000..cbb6881 --- /dev/null +++ b/lab-claudia/karma.conf.js @@ -0,0 +1,29 @@ +// Karma configuration +// Generated on Wed Oct 19 2016 09:49:07 GMT-0700 (PDT) +const webpack= require('./webpack.config.js'); +webpack.entry = {}; + +module.exports = function(config) { + config.set({ + webpack, + port: 9876, + colors: true, + basePath: '', + autoWatch: true, + singleRun: false, + concurrency: Infinity, + frameworks: ['jasmine'], + reporters: ['progress'], + browsers: ['PhantomJS'], + logLevel: config.LOG_INFO, + preprocessors: { + 'test/**/*-test.js': ['webpack'], + 'app/entry.js': ['webpack'], + }, + files: [ + 'app/entry.js', + 'test/**/*-test.js', + 'node_modules/angular-mocks/angular-mocks.js', + ], + }); +}; diff --git a/lab-claudia/lib/basic-auth-middleware.js b/lab-claudia/lib/basic-auth-middleware.js new file mode 100644 index 0000000..b6d6a58 --- /dev/null +++ b/lab-claudia/lib/basic-auth-middleware.js @@ -0,0 +1,29 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('artc-basic-auth-middleware'); + +module.exports = function(req, res, next){ + debug(); + + var authHeader = req.headers.authorization; + if(!authHeader) + return next(createError(401, 'requires authorization header')); + + let base64String = authHeader.split('Basic ')[1]; + if(!base64String) + return next(createError(401, 'require username and password')); + + let utf8String = new Buffer(base64String, 'base64').toString(); + let authArray = utf8String.split(':'); + req.auth = { + username: authArray[0], + password: authArray[1], + }; + + if(!req.auth.username) + return next(createError(401, 'basic auth requires username')); + if(!req.auth.password) + return next(createError(401, 'basic auth requires password')); + next(); +}; diff --git a/lab-claudia/lib/bearer-auth-middleware.js b/lab-claudia/lib/bearer-auth-middleware.js new file mode 100644 index 0000000..78ca7d2 --- /dev/null +++ b/lab-claudia/lib/bearer-auth-middleware.js @@ -0,0 +1,27 @@ +'use strict'; + +const debug = require('debug')('artc:bearer-middleware'); +const jwt = require('jsonwebtoken'); +const createError = require('http-errors'); + +const User = require('../model/user.js'); + +module.exports = function(req, res, next){ + debug(); + let authHeader = req.headers.authorization; + if(!authHeader) + return next(createError(400, 'requires auth header')); + let token = authHeader.split('Bearer ')[1]; + if(!token) + return next(createError(400, 'requires token')); + + jwt.verify(token, process.env.APP_SECRET, (err, decoded) => { + if(err) return next(createError(401, 'requires valid token')); + User.findOne({findHash: decoded.token}) + .then( user => { + if (!user) next(createError(401, 'user not found or old token')); + req.user = user; + next(); + }); + }); +}; diff --git a/lab-claudia/lib/error-middleware.js b/lab-claudia/lib/error-middleware.js new file mode 100644 index 0000000..c653750 --- /dev/null +++ b/lab-claudia/lib/error-middleware.js @@ -0,0 +1,35 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('artc:error-middleware'); + +module.exports = function(err, req, res, next){ + debug('error middleware'); + console.error(err.message); + console.error('name', err.name); + + + if (err.status){ + res.status(err.status).send(err.name); + next(); + return; + } + + if (err.name === 'ValidationError'){ + err = createError(400, err.message); + res.status(err.status).send(err.name); + next(); + return; + } + + if (err.name === 'MongoError' && err.message.includes('E11000 duplicate')){ + err = createError(409, err.message); + res.status(err.status).send(err.name); + next(); + return; + } + + err = createError(500, err.message); + res.status(err.status).send(err.name); + next(); +}; diff --git a/lab-claudia/lib/page-query-middleware.js b/lab-claudia/lib/page-query-middleware.js new file mode 100644 index 0000000..3b2c1fc --- /dev/null +++ b/lab-claudia/lib/page-query-middleware.js @@ -0,0 +1,30 @@ +'use strict'; + +const debug = require('debug')('artc:query-middleware'); + +module.exports = function(req, res, next){ + debug('creating page queries'); + let page = 1 ; // page index to return within a collection + let offset = 0; + let pagesize = 50; + let sort = 1; + if (!isNaN(Number(req.query.page))) page = Number(req.query.page); + if (!isNaN(Number(req.query.offset))) offset = Number(req.query.offset); + if (!isNaN(Number(req.query.pagesize))) pagesize = Number(req.query.pagesize); + if (req.query.sort === 'dsc') sort = -1; // sort descending + if (req.query.sort === 'asc') sort = 1; // sort ascending + + if (page < 1) page = 1; + if (offset < 0) offset = 0; + if (pagesize < 1) pagesize = 1; + if (pagesize > 250) pagesize = 250; + + if (page > 1) + offset = (page - 1) * pagesize + offset; + // quersystring arguments + req.query.page = page; + req.query.offset = offset; + req.query.pagesize = pagesize; + req.query.sort = sort; + next(); +}; diff --git a/lab-claudia/model/artist.js b/lab-claudia/model/artist.js new file mode 100644 index 0000000..b180fe7 --- /dev/null +++ b/lab-claudia/model/artist.js @@ -0,0 +1,22 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const artistSchema = mongoose.Schema({ + about: {type: String}, + phone: {type: String, unique: true}, + email: {type: String, required: true, unique: true}, + // query-sortable properties + username: {type: String, required: true, unique: true}, + created: {type: Date, required: true, default: Date.now}, + firstname: {type: String, required: true, minlength: 3}, + lastname: {type: String, required: true, minlength: 3}, + city: {type: String, required: true}, + zip: {type: String, required: true}, + // path params + userID: {type: mongoose.Schema.Types.ObjectId, required: true}, + galleries: [{type: mongoose.Schema.Types.ObjectId, ref: 'gallery'}], + photoID: {type: mongoose.Schema.Types.ObjectId}, +}); + +module.exports = mongoose.model('artist', artistSchema); diff --git a/lab-claudia/model/gallery.js b/lab-claudia/model/gallery.js new file mode 100644 index 0000000..3b11916 --- /dev/null +++ b/lab-claudia/model/gallery.js @@ -0,0 +1,19 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const gallerySchema = mongoose.Schema({ + name: {type: String, required: true}, + desc: {type: String, required: true}, + // query-sortable properties + username: {type: String, required: true}, + category: {type: String, required: true}, //validated string + created: {type: Date, required: true, default: Date.now}, + // path params + userID: {type: mongoose.Schema.Types.ObjectId, required: true}, + artistID: {type: mongoose.Schema.Types.ObjectId, required: true}, + listings: [{type: mongoose.Schema.Types.ObjectId, ref: 'listing'}], + photoID: {type: mongoose.Schema.Types.ObjectId}, +}); + +module.exports = mongoose.model('gallery', gallerySchema); diff --git a/lab-claudia/model/listing.js b/lab-claudia/model/listing.js new file mode 100644 index 0000000..890addc --- /dev/null +++ b/lab-claudia/model/listing.js @@ -0,0 +1,19 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const listingSchema = mongoose.Schema({ + title: {type: String, required: true}, + desc: {type: String, required: true}, + // query-sortable properties + username: {type: String, required: true}, + category: {type: String, required: true}, // validated string + created: {type: Date, required: true, default: Date.now}, + // path params + userID: {type: mongoose.Schema.Types.ObjectId, required: true}, + artistID: {type: mongoose.Schema.Types.ObjectId, required: true}, + galleryID: {type: mongoose.Schema.Types.ObjectId, required: true}, + photoID: {type: mongoose.Schema.Types.ObjectId}, +}); + +module.exports = mongoose.model('listing', listingSchema); diff --git a/lab-claudia/model/photo.js b/lab-claudia/model/photo.js new file mode 100644 index 0000000..f2d7bef --- /dev/null +++ b/lab-claudia/model/photo.js @@ -0,0 +1,18 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const photoSchema = mongoose.Schema({ + name: {type: String, required: true}, + alt: {type: String, required: true}, + objectKey: {type: String, required: true, unique:true}, + imageURI: {type: String, required: true, unique:true}, + username: {type: String, required: true}, + // path params + userID: {type: mongoose.Schema.Types.ObjectId, required:true}, + artistID: {type: mongoose.Schema.Types.ObjectId}, + galleryID: {type: mongoose.Schema.Types.ObjectId}, + listingID: {type: mongoose.Schema.Types.ObjectId}, +}); + +module.exports = mongoose.model('photo', photoSchema); diff --git a/lab-claudia/model/user.js b/lab-claudia/model/user.js new file mode 100644 index 0000000..226cce3 --- /dev/null +++ b/lab-claudia/model/user.js @@ -0,0 +1,77 @@ +'use strict'; + +const bcrypt = require('bcrypt'); +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); +const Promise = require('bluebird'); +const mongoose = require('mongoose'); +const createError = require('http-errors'); +const debug = require('debug')('artc:user'); + +// module constants +const Schema = mongoose.Schema; + +const userSchema = Schema({ + username: {type: String, required: true, unique: true, minlength: 3}, + email: {type: String, required: true, unique: true}, + password: {type: String}, + findHash: {type: String, unique: true}, + google: { + googleID: {type: String}, + refreshToken: {type: String}, + accessToken: {type: String}, + tokenTimestamp: {type: Date}, + tokenTTL: {type: Number}, + }, +}); + +userSchema.methods.generatePasswordHash = function(password) { + debug('generatePasswordHash'); + return new Promise((resolve, reject) => { + bcrypt.hash(password, 10, (err, hash) => { + if (err) return reject(err); + this.password = hash; + resolve(this); + }); + }); +}; + +userSchema.methods.comparePasswordHash = function(password) { + debug('comparePasswordHash'); + return new Promise((resolve, reject) => { + bcrypt.compare(password, this.password, (err, valid) => { + if(err) return reject(err); + if(!valid) return reject(createError(401, 'wrong password')); + resolve(this); + }); + }); +}; + +userSchema.methods.generateFindHash = function() { + debug('generateFindHash'); + return new Promise((resolve, reject) => { + let tries = 0; + _generateFindHash.call(this); + function _generateFindHash() { + this.findHash = crypto.randomBytes(32).toString('hex'); + this.save() + .then(() => resolve(this.findHash)) + .catch( err => { + if (tries > 3) return reject(err); + tries++; + _generateFindHash.call(this); + }); + } + }); +}; + +userSchema.methods.generateToken = function() { + debug('generateToken'); + return new Promise((resolve, reject) => { + this.generateFindHash() + .then( findHash => resolve(jwt.sign({token: findHash}, process.env.APP_SECRET))) + .catch(err => reject(err)); + }); +}; + +module.exports = mongoose.model('user', userSchema); diff --git a/lab-claudia/package.json b/lab-claudia/package.json new file mode 100644 index 0000000..58c4ab1 --- /dev/null +++ b/lab-claudia/package.json @@ -0,0 +1,85 @@ +{ + "name": "art-c-fullstack", + "version": "1.0.0", + "description": "", + "main": "index.js", + "engines": { + "node": "4.4.7" + }, + "scripts": { + "build": "./node_modules/webpack/bin/webpack.js", + "build-watch": "./node_modules/webpack/bin/webpack.js -w", + "frontend-test": "./node_modules/karma/bin/karma start --single-run", + "test-watch": "./node_modules/karma/bin/karma start", + "start": "node server.js", + "start-debug": "DEBUG='slug*' npm start", + "start-watch": "DEBUG='slug*' ./node_modules/nodemon/bin/nodemon.js server.js", + "test": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha", + "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", + "lint": "./node_modules/eslint/bin/eslint.js .", + "test-debug": "DEBUG='art-c*' ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "angular": "^1.5.8", + "angular-animate": "^1.5.8", + "angular-route": "^1.5.8", + "angular-touch": "^1.5.8", + "angular-ui-bootstrap": "^2.2.0", + "angular-ui-router": "^0.3.1", + "babel-core": "^6.17.0", + "babel-loader": "^6.2.5", + "babel-preset-es2015": "^6.16.0", + "bootstrap-sass": "^3.3.7", + "camelcase": "^3.0.0", + "clean-webpack-plugin": "^0.1.13", + "css-loader": "^0.25.0", + "dotenv": "^2.0.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.9.0", + "font-awesome": "^4.6.3", + "html-loader": "^0.4.4", + "html-webpack-plugin": "^2.22.0", + "node-sass": "^3.10.1", + "pascalcase": "^0.1.1", + "resolve-url-loader": "^1.6.0", + "sass-loader": "^4.0.2", + "style-loader": "^0.13.1", + "ui-router": "^1.0.0-alpha.3", + "url-loader": "^0.5.7", + "webpack": "^1.13.2", + "aws-sdk": "^2.6.6", + "bcrypt": "^0.8.7", + "bluebird": "^3.4.6", + "body-parser": "^1.15.2", + "cors": "^2.8.1", + "debug": "^2.2.0", + "del": "^2.2.2", + "express": "^4.14.0", + "http-errors": "^1.5.0", + "jsonwebtoken": "^7.1.9", + "mongoose": "^4.6.2", + "morgan": "^1.7.0", + "multer": "^1.2.0" + }, + "devDependencies": { + "angular-mocks": "^1.5.8", + "aws-sdk-mock": "^1.5.0", + "chai": "^3.5.0", + "coveralls": "^2.11.14", + "eslint": "^3.7.1", + "istanbul": "^0.4.5", + "jasmine-core": "^2.5.2", + "karma": "^1.3.0", + "karma-jasmine": "^1.0.2", + "karma-phantomjs-launcher": "^1.0.2", + "karma-webpack": "^1.8.0", + "lorem-ipsum": "^1.0.3", + "mocha": "^3.1.0", + "nodemon": "^1.11.0", + "superagent": "^2.3.0", + "webpack-dev-server": "^1.16.2" + } +} diff --git a/lab-claudia/route/artist-router.js b/lab-claudia/route/artist-router.js new file mode 100644 index 0000000..ff0c9dd --- /dev/null +++ b/lab-claudia/route/artist-router.js @@ -0,0 +1,83 @@ +'use strict'; + +// npm modules +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const debug = require('debug')('artc:artist-route'); +const createError = require('http-errors'); +const AWS = require('aws-sdk'); + +// app modules +const Artist = require('../model/artist.js'); +const Gallery = require('../model/gallery.js'); +const Listing = require('../model/listing.js'); +const Photo = require('../model/photo.js'); +const bearerAuth = require('../lib/bearer-auth-middleware.js'); + +AWS.config.setPromisesDependency(require('bluebird')); + +// module constants +const artistRouter = module.exports = Router(); +const s3 = new AWS.S3(); + +artistRouter.post('/api/artist', bearerAuth, jsonParser, function(req, res, next) { + debug('POST /api/artist'); + req.body.userID = req.user._id; + new Artist(req.body).save() + .then( artist => res.json(artist)) + .catch(next); +}); + +artistRouter.get('/api/artist/:artistID', bearerAuth, function(req, res, next) { + debug('GET /api/artist/:artistID'); + Artist.findById(req.params.artistID) + .populate({path: 'galleries'}) + .then( artist => { + res.json(artist); + }) + .catch( err => { + if (err.name === 'ValidationError') return next(err); + next(createError(404, err.message)); + }); +}); + +artistRouter.put('/api/artist/:artistID', bearerAuth, jsonParser, function(req, res, next) { + debug('hit route PUT /api/artist/:artistID'); + Artist.findById(req.params.artistID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( artist => { + if (artist.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'invalid userid')); + return Artist.findByIdAndUpdate(req.params.artistID, req.body, {new: true, runValidators: true}); + }) + .then(artist => res.json(artist)) + .catch(next); +}); + +artistRouter.delete('/api/artist/:artistID', bearerAuth, function(req, res, next) { + debug('hit route DELETE /api/artist/:artistID'); + Artist.findById(req.params.artistID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( artist => { + if (artist.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'unauthorized')); + return Artist.findByIdAndRemove(req.params.artistID); + }) + .catch( err => Promise.reject(err, err.message)) + .then( () => Listing.remove({ artistID: req.params.artistID})) + .then( () => Gallery.remove({ artistID: req.params.artistID})) + .then( () => Photo.find({ artistID: req.params.artistID})) + .then( photos => { + let s3DeletePhotoArray = []; + for(var i=0; i Photo.remove({ artistID: req.params.artistID})) + .then( () => res.sendStatus(204)) + .catch(next); +}); diff --git a/lab-claudia/route/auth-router.js b/lab-claudia/route/auth-router.js new file mode 100644 index 0000000..15c3676 --- /dev/null +++ b/lab-claudia/route/auth-router.js @@ -0,0 +1,102 @@ +'use strict'; + +const Router = require('express').Router; +const debug = require('debug')('artc:auth-router'); +const jsonParser = require('body-parser').json(); +const createError = require('http-errors'); +const AWS = require('aws-sdk'); + +const Artist = require('../model/artist.js'); +const Gallery = require('../model/gallery.js'); +const Listing = require('../model/listing.js'); +const Photo = require('../model/photo.js'); +const User = require('../model/user.js'); + +const basicAuth = require('../lib/basic-auth-middleware.js'); +const bearerAuth = require('../lib/bearer-auth-middleware'); + +AWS.config.setPromisesDependency(require('bluebird')); + +const authRouter = module.exports = Router(); +const s3 = new AWS.S3(); + +authRouter.post('/api/signup', jsonParser, function(req, res, next){ + debug('hit route POST /api/signup'); + let password = req.body.password; + delete req.body.password; + let user = new User(req.body); + + if (!password) + return next(createError(400, 'requires password')); + + if (password.length < 7) + return next(createError(400, 'password must be at least 7 characters')); + + user.generatePasswordHash(password) + .then( user => user.save()) + .then( user => user.generateToken()) + .then( token => res.send(token)) + .catch(next); +}); + +authRouter.get('/api/login', basicAuth, function(req, res, next){ + debug('hit route GET /api/login'); + + User.findOne({username: req.auth.username}) + .then( user => user.comparePasswordHash(req.auth.password)) + .catch(err => Promise.reject(createError(401, err.message))) + .then( user => user.generateToken()) + .then( token => res.send(token)) + .catch(next); +}); + +authRouter.delete('/api/user/deleteAccount', bearerAuth, function(req, res, next) { + debug('hit route DELETE /api/user/deleteAccount'); + User.findByIdAndRemove(req.user._id) + .catch( err => Promise.reject(err, err.message)) + .then( () => Listing.remove({ userID: req.user._id})) + .then( () => Gallery.remove({ userID: req.user._id})) + .then( () => Artist.remove({ userID: req.user._id})) + .then( () => Photo.find({ userID: req.user._id})) + .then( photos => { + let s3DeletePhotoArray = []; + for(var i=0; i Photo.remove({ userID: req.user._id})) + .then( () => res.sendStatus(204)) + .catch(next); +}); + +authRouter.put('/api/user/updateEmail', bearerAuth, jsonParser, function(req, res, next) { + debug('hit route PUT /api/user/updateEmail'); + return User.findByIdAndUpdate(req.user._id, req.body, {new: true, runValidators: true}) + .then( user => { + res.json(user); + }) + .catch(next); +}); + +authRouter.put('/api/user/updateUsername', bearerAuth, jsonParser, function(req, res, next) { + debug('hit route PUT /api/user/updateUsername'); + return User.findByIdAndUpdate(req.user._id, req.body, {new: true, runValidators: true}) + .then( user => { + res.json(user); + }) + .catch(next); +}); + + +authRouter.put('/api/user/updatePassword', bearerAuth, jsonParser, function(req, res, next) { + debug('hit route PUT /api/user/updatePassword'); + return User.findByIdAndUpdate(req.user._id, req.body, {new: true, runValidators: true}) + .then( user => { + res.json(user); + }) + .catch(next); +}); diff --git a/lab-claudia/route/gallery-router.js b/lab-claudia/route/gallery-router.js new file mode 100644 index 0000000..f296625 --- /dev/null +++ b/lab-claudia/route/gallery-router.js @@ -0,0 +1,121 @@ +'use strict'; + +// npm modules +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const debug = require('debug')('artc:gallery-route'); +const createError = require('http-errors'); +const AWS = require('aws-sdk'); + +// app modules +const Artist = require('../model/artist.js'); +const Gallery = require('../model/gallery.js'); +const Photo = require('../model/photo.js'); +const Listing = require('../model/listing.js'); +const bearerAuth = require('../lib/bearer-auth-middleware.js'); + +AWS.config.setPromisesDependency(require('bluebird')); + +// module constants +const galleryRouter = module.exports = Router(); +const s3 = new AWS.S3(); + +galleryRouter.post('/api/artist/:artistID/gallery', bearerAuth, jsonParser, function(req, res, next) { + debug('POST /api/gallery'); + let tempArtist; + let tempGallery; + Artist.findById(req.params.artistID) + .catch(err => Promise.reject(createError(404, err.message))) + .then ( artist => { + if (artist.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'invalid user')); + tempArtist = artist; + req.body.artistID = artist._id; + req.body.userID = req.user._id; + req.body.username = artist.username; + return new Gallery(req.body).save(); + }) + .then( gallery => { + tempGallery = gallery; + tempArtist.galleries.push(gallery._id); + return tempArtist.save(); + }) + .then(() => res.json(tempGallery)) + .catch(next); +}); + +galleryRouter.post('/api/gallery', bearerAuth, jsonParser, function(req, res, next){ + debug('POST /api/gallery'); + req.body.userID = req.user._id; + req.body.username = req.user.username; + new Gallery(req.body).save() + .then( gallery => res.json(gallery)) + .catch(next); +}); + + +galleryRouter.get('/api/gallery/:galleryID', bearerAuth, function(req, res, next) { + debug('GET /api/gallery/:galleryID'); + Gallery.findById(req.params.galleryID) + .populate({path: 'listings'}) + .then( gallery => { + res.json(gallery); + }) + .catch( err => { + if (err.name === 'ValidationError') return next(err); + next(createError(404, err.message)); + }); +}); + +galleryRouter.put('/api/artist/:artistID/gallery/:galleryID', bearerAuth, jsonParser, function(req, res, next) { + debug('hit route PUT /api/gallery/:galleryID'); + Gallery.findById(req.params.galleryID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( gallery => { + if (gallery.userID.toString() !== req.user._id.toString()) + return next(createError(401, 'invalid userid')); + return Gallery.findByIdAndUpdate(req.params.galleryID, req.body, {new: true, runValidators: true}); + }) + .then(gallery => { + res.json(gallery); + }) + .catch(next); +}); + +galleryRouter.delete('/api/artist/:artistID/gallery/:galleryID', bearerAuth, function(req, res, next) { + debug('hit route DELETE /api/gallery/:galleryID'); + + Gallery.findById(req.params.galleryID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( gallery => { + if (gallery.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'unauthorized')); + return Gallery.findByIdAndRemove(req.params.galleryID); + }) + .catch( err => Promise.reject(err)) + .then( () => Listing.remove({ galleryID: req.params.galleryID})) + .then( () => Photo.find({galleryID: req.params.galleryID})) + .then( photos => { + let s3DeletePhotoArray = []; + for(var i=0; i Photo.remove({galleryID: req.params.galleryID})) + .then( () => { + Artist.findById(req.params.artistID) + .then( artist => { + artist.galleries.forEach( gallery => { + if(artist.galleries[gallery] === req.params.galleryID) + artist.galleries.splice(artist.galleries.indexOf(gallery), 1); + }); + return artist.save(); + }); + }) + .then( () => res.sendStatus(204)) + .catch(next); +}); diff --git a/lab-claudia/route/listing-router.js b/lab-claudia/route/listing-router.js new file mode 100644 index 0000000..5041a2d --- /dev/null +++ b/lab-claudia/route/listing-router.js @@ -0,0 +1,104 @@ +'use strict'; + +// npm modules +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const debug = require('debug')('artc:listing-route'); +const createError = require('http-errors'); +const AWS = require('aws-sdk'); + +// app modules +const Gallery = require('../model/gallery.js'); +const Photo = require('../model/photo.js'); +const Listing = require('../model/listing.js'); +const bearerAuth = require('../lib/bearer-auth-middleware.js'); + +AWS.config.setPromisesDependency(require('bluebird')); + + +// module constants +const listingRouter = module.exports = Router(); +const s3 = new AWS.S3(); + +listingRouter.post('/api/gallery/:galleryID/listing', bearerAuth, jsonParser, function(req, res, next) { + debug('POST /api/listing'); + let tempGallery, tempListing; + Gallery.findById(req.params.galleryID) + .catch(err => Promise.reject(createError(404, err.message))) + .then ((gallery) => { + if (gallery.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'invalid user')); + req.body.galleryID = gallery._id; + req.body.artistID = gallery.artistID; + req.body.userID = req.user._id; + req.body.username = req.user.username; + tempGallery = gallery; + return new Listing(req.body).save(); + }) + .then(listing => { + tempGallery.listings.push(listing._id); + tempListing = listing; + return tempGallery.save(); + }) + .then(() => res.json(tempListing)) + .catch(next); +}); + +listingRouter.get('/api/listing/:listingID', bearerAuth, function(req, res, next) { + debug('GET /api/listing/:listingID'); + Listing.findById(req.params.listingID) + .catch(err => Promise.reject(createError(404, err.message))) + .then(listing => { + res.json(listing); + }) + .catch(next); +}); + +listingRouter.put('/api/gallery/:galleryID/listing/:listingID', bearerAuth, jsonParser, function(req, res, next) { + debug('hit route PUT /api/gallery/:galleryID/listing/:listingID'); + Listing.findById(req.params.listingID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( listing => { + if(listing.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'invalid userid')); + return Listing.findByIdAndUpdate(req.params.listingID, req.body, {new: true, runValidators: true}); + }) + .then(listing => { + res.json(listing); + }) + .catch(next); +}); + +listingRouter.delete('/api/gallery/:galleryID/listing/:listingID', bearerAuth, function(req, res, next) { + debug('hit route DELETE /api/gallery/:galleryID/listing/:listingID'); + Listing.findById(req.params.listingID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( listing => { + if(listing.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'invalid userid')); + return Gallery.findById(req.params.galleryID); + }) + .catch( err => err.status? Promise.reject(err) : Promise.reject(createError(404, err.message))) + .then( gallery => { + gallery.listings.forEach( listing => { + if (gallery.listings[listing] === req.params.listingID) + gallery.listings.splice(gallery.listings.indexOf(listing), 1); + }); + return gallery.save(); + }) + .then( () => Photo.find({listingID: req.params.listingID})) + .then( photos => { + let s3DeletePhotoArray = []; + for(var i=0; i Photo.remove({listingID: req.params.listingID})) + .then( () => Listing.findByIdAndRemove(req.params.listingID)) + .then( () => res.sendStatus(204)) + .catch(next); +}); diff --git a/lab-claudia/route/page-router.js b/lab-claudia/route/page-router.js new file mode 100644 index 0000000..9f1a2af --- /dev/null +++ b/lab-claudia/route/page-router.js @@ -0,0 +1,35 @@ +'use strict'; + +// npm modules +const Router = require('express').Router; +const debug = require('debug')('artc:gallery-route'); +const AWS = require('aws-sdk'); + +// app modules +const Gallery = require('../model/gallery.js'); +const Listing = require('../model/listing.js'); +const pageMiddleware = require('../lib/page-query-middleware.js'); + +AWS.config.setPromisesDependency(require('bluebird')); + +// module constants +const pageRouter = module.exports = Router(); + +pageRouter.get('/api/gallery', pageMiddleware, function(req, res, next){ + debug('hit route GET /api/gallery'); + let offset = req.query.offset, pageSize = req.query.pagesize, page = req.query.page; + let skip = offset + pageSize * page ; + Gallery.find().skip(skip).limit(pageSize) + .then(galleries => res.json(galleries)) + .catch(next); +}); + +pageRouter.get('/api/listing', pageMiddleware, function(req, res, next){ + debug('hit route GET /api/listing'); + let offset = req.query.offset, pageSize = req.query.pagesize, page = req.query.page; + + let skip = offset + pageSize * page ; + Listing.find().skip(skip).limit(pageSize) + .then( listings => res.json(listings)) + .catch(next); +}); diff --git a/lab-claudia/route/photo-router.js b/lab-claudia/route/photo-router.js new file mode 100644 index 0000000..7bde649 --- /dev/null +++ b/lab-claudia/route/photo-router.js @@ -0,0 +1,264 @@ +'use strict'; + +// NODE MODULES +const fs = require('fs'); +const path = require('path'); + +// NPM MODULES +const del = require('del'); +const AWS = require('aws-sdk'); +const multer = require('multer'); +const debug = require('debug')('artc:photo-router'); +const createError = require('http-errors'); + +//APP MODULES +const Photo = require('../model/photo.js'); +const Artist = require('../model/artist.js'); +const Gallery = require('../model/gallery.js'); +const Listing = require('../model/listing.js'); +const bearerAuth = require('../lib/bearer-auth-middleware.js'); + +AWS.config.setPromisesDependency(require('bluebird')); + +// MODULE CONSTANTS +const s3 = new AWS.S3(); +const dataDir =`${__dirname}/../data`; +const upload = multer({dest: dataDir}); +const photoRouter = module.exports = require('express').Router(); + +function s3UploadPromise(params){ + return new Promise((resolve, reject) => { + s3.upload(params, (err, s3data) => { + if (err) return reject(err); + resolve(s3data); + }); + }); +} + +photoRouter.post('/api/artist/:artistID/photo', bearerAuth, upload.single('image'), function(req, res, next){ + debug('hit POST /api/artist/:artistID/photo'); + + if (!req.file) + return next(createError(400, 'no file found')); + if (!req.file.path) + return next(createError(500, 'file not saved')); + let ext = path.extname(req.file.originalname); + + let params = { + ACL: 'public-read', + Bucket: 'artc-staging-assets', + Key: `${req.file.filename}${ext}`, + Body: fs.createReadStream(req.file.path), + }; + + let tempArtist = null; + + Artist.findById(req.params.artistID) + .catch(err => Promise.reject(createError(404, err.message))) + .then(artist => { + tempArtist = artist; + return s3UploadPromise(params); + }) + .catch(err => err.status ? Promise.reject(err) : Promise.reject(createError(500, err.message))) + .then(s3data => { + del([`${dataDir}/*`]); + let photoData = { + name: req.body.name, + username: req.user.username, + alt: req.body.alt, + objectKey: s3data.Key, + imageURI: s3data.Location, + artistID: tempArtist._id, + userID: req.user._id, + }; + return new Photo(photoData).save(); + }) + .then(photo => res.json(photo)) + .catch(err => { + del([`${dataDir}/*`]); + next(err); + }); +}); + +photoRouter.delete('/api/artist/:artistID/photo/:photoID', bearerAuth, function(req, res, next){ + debug('hit DELETE /api/artist/:artistID/photo/:photoID'); + let tempPhoto; + Photo.findById(req.params.photoID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( photo => { + if(photo.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'User not authorized to delete this photo')); + tempPhoto = photo; + return Artist.findById(req.params.artistID); + }) + .catch( err => err.status? Promise.reject(err) : Promise.reject(createError(404, err.message))) + .then( artist => { + artist.photoID = null; + return artist.save(); + }) + .then( () => { + let params = { + Bucket: 'artc-staging-assets', + Key: tempPhoto.objectKey, + }; + return s3.deleteObject(params).promise(); + }) + .then( () => { + return Photo.findByIdAndRemove(req.params.photoID); + }) + .then(() => res.sendStatus(204)) + .catch(next); +}); + +photoRouter.post('/api/gallery/:galleryID/photo', bearerAuth, upload.single('image'), function(req, res, next){ + debug('hit POST /api/gallery/:galleryID/photo'); + + if (!req.file) + return next(createError(400, 'no file found')); + if (!req.file.path) + return next(createError(500, 'file not saved')); + let ext = path.extname(req.file.originalname); + + let params = { + ACL: 'public-read', + Bucket: 'artc-staging-assets', + Key: `${req.file.filename}${ext}`, + Body: fs.createReadStream(req.file.path), + }; + + let tempGallery = null; + + Gallery.findById(req.params.galleryID) + .catch(err => Promise.reject(createError(404, err.message))) + .then(gallery => { + tempGallery = gallery; + return s3UploadPromise(params); + }) + .catch(err => err.status ? Promise.reject(err) : Promise.reject(createError(500, err.message))) + .then(s3data => { + del([`${dataDir}/*`]); + let photoData = { + name: req.body.name, + username: req.user.username, + alt: req.body.alt, + objectKey: s3data.Key, + imageURI: s3data.Location, + galleryID: tempGallery._id, + artistID: tempGallery.artistID, + userID: req.user._id, + }; + return new Photo(photoData).save(); + }) + .then(photo => res.json(photo)) + .catch(err => { + del([`${dataDir}/*`]); + next(err); + }); +}); + +photoRouter.delete('/api/gallery/:galleryID/photo/:photoID', bearerAuth, function(req, res, next){ + debug('hit DELETE /api/gallery/:galleryID/photo/:photoID'); + let tempPhoto; + Photo.findById(req.params.photoID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( photo => { + if(photo.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'User not authorized to delete this photo')); + tempPhoto = photo; + return Gallery.findById(req.params.galleryID); + }) + .catch( err => err.status? Promise.reject(err) : Promise.reject(createError(404, err.message))) + .then( gallery => { + gallery.photoID = null; + return gallery.save(); + }) + .then( () => { + let params = { + Bucket: 'artc-staging-assets', + Key: tempPhoto.objectKey, + }; + return s3.deleteObject(params).promise(); + }) + .then( () => { + return Photo.findByIdAndRemove(req.params.photoID); + }) + .then(() => res.sendStatus(204)) + .catch(next); +}); + +photoRouter.post('/api/listing/:listingID/photo', bearerAuth, upload.single('image'), function(req, res, next){ + debug('hit POST /api/listing/:listingID/photo'); + + if (!req.file) + return next(createError(400, 'no file found')); + if (!req.file.path) + return next(createError(500, 'file not saved')); + let ext = path.extname(req.file.originalname); + + let params = { + ACL: 'public-read', + Bucket: 'artc-staging-assets', + Key: `${req.file.filename}${ext}`, + Body: fs.createReadStream(req.file.path), + }; + + let tempListing = null; + + Listing.findById(req.params.listingID) + .catch(err => Promise.reject(createError(404, err.message))) + .then(listing => { + tempListing = listing; + return s3UploadPromise(params); + }) + .catch(err => err.status ? Promise.reject(err) : Promise.reject(createError(500, err.message))) + .then(s3data => { + del([`${dataDir}/*`]); + let photoData = { + name: req.body.name, + username: req.user.username, + alt: req.body.alt, + objectKey: s3data.Key, + imageURI: s3data.Location, + listingID: tempListing._id, + artistID: tempListing.artistID, + galleryID: tempListing.galleryID, + userID: req.user._id, + }; + return new Photo(photoData).save(); + }) + .then(photo => res.json(photo)) + .catch(err => { + del([`${dataDir}/*`]); + next(err); + }); +}); + +photoRouter.delete('/api/listing/:listingID/photo/:photoID', bearerAuth, function(req, res, next){ + debug('hit DELETE /api/listing/:listingID/photo/:photoID'); + let tempPhoto; + Photo.findById(req.params.photoID) + .catch(err => Promise.reject(createError(404, err.message))) + .then( photo => { + if(photo.userID.toString() !== req.user._id.toString()) + return Promise.reject(createError(401, 'User not authorized to delete this photo')); + tempPhoto = photo; + return Listing.findById(req.params.listingID); + }) + .catch( err => err.status? Promise.reject(err) : Promise.reject(createError(404, err.message))) + .then( listing => { + listing.photoID = null; + return listing.save(); + }) + .then( () => { + let params = { + Bucket: 'artc-staging-assets', + Key: tempPhoto.objectKey, + }; + return s3.deleteObject(params).promise(); + }) + .then( () => { + return Photo.findByIdAndRemove(req.params.photoID); + }) + .then(() => res.sendStatus(204)) + .catch(next); +}); diff --git a/lab-claudia/server.js b/lab-claudia/server.js new file mode 100644 index 0000000..6326bb4 --- /dev/null +++ b/lab-claudia/server.js @@ -0,0 +1,50 @@ +'use strict'; + +// npm modules +const express = require('express'); +const morgan = require('morgan'); +const Promise = require('bluebird'); +const cors = require('cors'); +const mongoose = require('mongoose'); +const debug = require('debug')('artc:server'); +const dotenv = require('dotenv'); + +// app modules +const authRouter = require('./route/auth-router.js'); +const artistRouter = require('./route/artist-router.js'); +const galleryRouter = require('./route/gallery-router.js'); +const listingRouter = require('./route/listing-router.js'); +const photoRouter = require('./route/photo-router.js'); +const pageRouter = require('./route/page-router.js'); + +const errorMiddleware = require('./lib/error-middleware.js'); +// load env variables +dotenv.load(); + +// connect to mongo database +mongoose.Promise = Promise; +mongoose.connect(process.env.MONGODB_URI); + +// module constants +const PORT = process.env.PORT; +const app = express(); + +// app middleware +app.use(cors()); +app.use(morgan('dev')); + +// app routes +app.use(authRouter); +app.use(artistRouter); +app.use(galleryRouter); +app.use(listingRouter); +app.use(photoRouter); +app.use(pageRouter); +app.use(errorMiddleware); + +// start server +const server = module.exports = app.listen(PORT, function() { + debug('server started'); +}); + +server.isRunning = true; diff --git a/lab-claudia/test/artist-router-test.js b/lab-claudia/test/artist-router-test.js new file mode 100644 index 0000000..fc312bf --- /dev/null +++ b/lab-claudia/test/artist-router-test.js @@ -0,0 +1,1247 @@ +'use strict'; + +require('./lib/test-env.js'); +require('./lib/aws-mock.js'); + +// npm modules +const expect = require('chai').expect; +const request = require('superagent'); +const Promise = require('bluebird'); +const mongoose = require('mongoose'); + +// app modules +const serverCtrl = require('./lib/server-control'); +const cleanDB = require('./lib/clean-db'); +const mockUser = require('./lib/user-mock'); +const mockArtist = require('./lib/artist-mock'); +const mockMultipleGalleries = require('./lib/populate-artist-galleries-mock.js'); +const mockManyPhotos = require('./lib/mock-many-photos.js'); + +mongoose.Promise = Promise; + +// module constants +const server = require('../server.js'); +const url = `http://localhost:${process.env.PORT}`; + +const exampleArtist = { + firstname: 'Jimbob', + lastname: 'Jimbobberson', + username: 'Jimbobguy316', + email: 'jimbobguy14@stuff.com', + city: 'Dallas', + zip: '98114', + about: 'I\m just a simple kinda man who likes to do art stuff.', + phone: '(555)555-5555', +}; + +describe('testing artist-router', function() { + + before( done => serverCtrl.serverUp(server, done)); + after( done => serverCtrl.serverDown(server, done)); + afterEach( done => cleanDB(done)); + + describe('testing POST /api/artist', function() { + + describe('with a valid body', function() { + + before(done => mockUser.call(this, done)); + + it('should return an artist profile and a status 200', (done) => { + request.post(`${url}/api/artist`) + .send(exampleArtist) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(exampleArtist.firstname); + expect(res.body.lastname).to.equal(exampleArtist.lastname); + expect(res.body.username).to.equal(exampleArtist.username); + expect(res.body.email).to.equal(exampleArtist.email); + expect(res.body.city).to.equal(exampleArtist.city); + expect(res.body.zip).to.equal(exampleArtist.zip); + expect(res.body.about).to.equal(exampleArtist.about); + expect(res.body.phone).to.equal(exampleArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + + }); + + describe('with no firstname', function() { + + before(done => mockUser.call(this, done)); + + it('should status 400 bad request', (done) => { + request.post(`${url}/api/artist`) + .send({ + lastname: exampleArtist.lastname, + username: exampleArtist.username, + email: exampleArtist.email, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: exampleArtist.phone, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with no lastname', function() { + + before(done => mockUser.call(this, done)); + + it('should status 400 bad request', (done) => { + request.post(`${url}/api/artist`) + .send({ + firstname: exampleArtist.firstname, + username: exampleArtist.username, + email: exampleArtist.email, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: exampleArtist.phone, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with no username', function() { + + before(done => mockUser.call(this, done)); + + it('should status 400 bad request', (done) => { + + request.post(`${url}/api/artist`) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + email: exampleArtist.email, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: exampleArtist.phone, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no email', function() { + + before(done => mockUser.call(this, done)); + + it('should return an artist profile and a status 400', (done) => { + request.post(`${url}/api/artist`) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + username: exampleArtist.username, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: exampleArtist.phone, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with no city', function() { + + before(done => mockUser.call(this, done)); + + it('should return an artist profile and a status 400', (done) => { + request.post(`${url}/api/artist`) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + username: exampleArtist.username, + email: exampleArtist.email, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: exampleArtist.phone, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with no zip', function() { + + before(done => mockUser.call(this, done)); + + it('should return an artist profile and a status 400', (done) => { + request.post(`${url}/api/artist`) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + username: exampleArtist.username, + email: exampleArtist.email, + city: exampleArtist.city, + about: exampleArtist.about, + phone: exampleArtist.phone, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with invalid date--string', function() { + + before(done => mockUser.call(this, done)); + + it('should return an artist profile and a status 400', (done) => { + request.post(`${url}/api/artist`) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + username: exampleArtist.username, + email: exampleArtist.email, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: exampleArtist.phone, + created: 'striiiing', + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with an invalid body', function() { + + before(done => mockUser.call(this, done)); + + it('should a status 400 bad request', (done) => { + request.post(`${url}/api/artist`) + .send('badbody') + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with a bad authorization header', function() { + + before(done => mockUser.call(this, done)); + + it('should return status 400 bad request', (done) => { + request.post(`${url}/api/artist`) + .send(exampleArtist) + .set({ + Authorization: 'bad request', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with no authorization header', function() { + + before(done => mockUser.call(this, done)); + + it('should return status 400 bad request', (done) => { + request.post(`${url}/api/artist`) + .send(exampleArtist) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + }); + + describe('with bearer header with no token', function() { + + before(done => mockUser.call(this, done)); + + it('should return status 400 bad request', (done) => { + request.post(`${url}/api/artist`) + .send(exampleArtist) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with already-existing username', function() { + + before( done => mockArtist.call(this, done)); + + it('should return a status 409', (done) => { + request.post(`${url}/api/artist`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + username: this.tempArtist.username, + email: exampleArtist.email, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: this.tempArtist.phone, + }) + .end((err, res) => { + expect(res.status).to.equal(409); + expect(res.text).to.equal('ConflictError'); + done(); + }); + }); + }); + + describe('with already-existing email', function() { + + before( done => mockArtist.call(this, done)); + + it('should return a status 409', (done) => { + request.post(`${url}/api/artist`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + username: exampleArtist.phone, + email: this.tempArtist.email, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: exampleArtist.phone, + }) + .end((err, res) => { + expect(res.status).to.equal(409); + expect(res.text).to.equal('ConflictError'); + done(); + }); + }); + + }); + + describe('with already-existing phone number', function() { + + before( done => mockArtist.call(this, done)); + + it('should return a status 409', (done) => { + request.post(`${url}/api/artist`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .send({ + firstname: exampleArtist.firstname, + lastname: exampleArtist.lastname, + username: exampleArtist.phone, + email: exampleArtist.email, + city: exampleArtist.city, + zip: exampleArtist.zip, + about: exampleArtist.about, + phone: this.tempArtist.phone, + }) + .end((err, res) => { + expect(res.status).to.equal(409); + expect(res.text).to.equal('ConflictError'); + done(); + }); + }); + }); + + describe('with wrong token', function() { + let tempSecondUser = {}; + before(done => mockArtist.call(this, done)); + before(done => mockUser.call(tempSecondUser, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + + describe('testing GET to /api/artist/:artistID', () => { + + describe('with valid token and id', function(){ + + before(done => mockArtist.call(this, done)); + + it('should return a artist', done => { + request.get(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('testing populate artist galleries with valid id and token', function(){ + + before(done => mockMultipleGalleries.call(this, 10, done)); + + it('should return an artist with populated gallery array', done => { + request.get(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.galleries.length).to.equal(10); + done(); + }); + }); + }); + + describe('testing populate artist galleries with valid id and invalid token', function(){ + + before(done => mockMultipleGalleries.call(this, 10, done)); + + it('should return a 400 error for bad request', done => { + request.get(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('testing populate artist galleries with invalid id and valid token', function(){ + + before(done => mockMultipleGalleries.call(this, 10, done)); + + it('should return an artist with populated gallery array', done => { + request.get(`${url}/api/artist/${this.tempArtist._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with valid token and invalid id', function(){ + + before(done => mockArtist.call(this, done)); + + it('should status 404 not found', done => { + request.get(`${url}/api/artist/${this.tempArtist._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with invalid token and valid id', function(){ + + before(done => mockArtist.call(this, done)); + + it('should return status 400 for bad request', done => { + request.get(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no auth header', function(){ + + before(done => mockArtist.call(this, done)); + before(done => mockUser.call(this, done)); + + it('should status 400 bad request', done => { + request.get(`${url}/api/artist/${this.tempArtist._id}`) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong token', function() { + let tempSecondUser = {}; + before(done => mockArtist.call(this, done)); + before(done => mockUser.call(tempSecondUser, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + + describe('testing PUT to /api/artist/:artistID', () => { + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only the firstname, should return an artist', done => { + let updateData = {firstname: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(updateData.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and id', function(){ + + before(done => mockArtist.call(this, done)); + + it('updating only the lastname, should return an artist', done => { + let updateData = {lastname: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(updateData.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + + describe('with valid token and id', function(){ + + before(done => mockArtist.call(this, done)); + + it('updating only the email, should return an artist', done => { + let updateData = {email: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(updateData.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only the city, should return an artist', done => { + let updateData = {city: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(updateData.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only the zip, should return an artist', done => { + let updateData = {zip: '99999'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(updateData.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only the about, should return an artist', done => { + let updateData = {about: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(updateData.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only the phone, should return an artist', done => { + let updateData = {phone: '(555)595-5959'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(updateData.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only the phone, should return an artist', done => { + let updateData = {phone: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(updateData.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only firstname and lastname, should return an artist', done => { + let updateData = {firstname: 'bob', lastname: 'bob2'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(updateData.firstname); + expect(res.body.lastname).to.equal(updateData.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating only username and email, should return an artist', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only firstname and lastname, should return an artist', done => { + let updateData = {username: 'bob', email: 'bob2'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(updateData.username); + expect(res.body.email).to.equal(updateData.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating only city and zip, should return an artist', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only firstname and lastname, should return an artist', done => { + let updateData = {city: 'bob', zip: 'bob2'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(updateData.city); + expect(res.body.zip).to.equal(updateData.zip); + expect(res.body.about).to.equal(this.tempArtist.about); + expect(res.body.phone).to.equal(this.tempArtist.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating only about and phone, should return an artist', function() { + + before(done => mockArtist.call(this, done)); + + it('updating only firstname and lastname, should return an artist', done => { + let updateData = {about: 'bob', phone: 'bob2'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.firstname).to.equal(this.tempArtist.firstname); + expect(res.body.lastname).to.equal(this.tempArtist.lastname); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.email).to.equal(this.tempArtist.email); + expect(res.body.city).to.equal(this.tempArtist.city); + expect(res.body.zip).to.equal(this.tempArtist.zip); + expect(res.body.about).to.equal(updateData.about); + expect(res.body.phone).to.equal(updateData.phone); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and invalid id', function() { + + before(done => mockArtist.call(this, done)); + + it('should return status 404 not found', done => { + let updateData = {firstName: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}bad`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('updated with empty firstname', function(){ + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', done => { + let updateData = {firstname: ''}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('updated with empty lastname', function(){ + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', done => { + let updateData = {lastname: ''}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('updated with empty city', function(){ + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', done => { + let updateData = {city: ''}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('updated with empty zip', function(){ + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', done => { + let updateData = {zip: ''}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with bad token request and valid id', function(){ + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', done => { + let updateData = {firstName: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .send(updateData) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no auth header', function(){ + + before(done => mockArtist.call(this, done)); + + it('should status 400 bad request', done => { + request.put(`${url}/api/artist/${this.tempArtist._id}`) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong token', function() { + let tempSecondUser = {}; + before(done => mockArtist.call(this, done)); + before(done => mockUser.call(tempSecondUser, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + }); + + describe('testing DELETE to /api/artist/:artistID', () => { + + describe('with valid token and id', function() { + + before(done => mockArtist.call(this, done)); + + it('should delete a artist', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with valid token and invalid id', function() { + + before(done => mockArtist.call(this, done)); + + it('should return status 404 not found', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with bad token request and valid id', function() { + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no auth header', function() { + + before(done => mockArtist.call(this, done)); + + it('should return status 400 for bad request', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong token', function() { + let tempSecondUser = {}; + before(done => mockArtist.call(this, done)); + before(done => mockUser.call(tempSecondUser, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with wrong user and many photos', function() { + let tempSecondUser = {}; + before( done => mockManyPhotos.call(this, 5, done)); + before(done => mockUser.call(tempSecondUser, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with valid token and id', () => { + + before( done => mockManyPhotos.call(this, 5, done)); + + it('should delete an artist and all associated galleries, listings and photos', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with bad token and many photos', () => { + + before( done => mockManyPhotos.call(this, 5, done)); + + it('should delete an artist and all associated galleries, listings and photos', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}bad`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with no auth header and many photos', function() { + + before( done => mockManyPhotos.call(this, 5, done)); + + it('should return status 400 for bad request', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong token', function() { + let tempSecondUser = {}; + before(done => mockArtist.call(this, done)); + before(done => mockUser.call(tempSecondUser, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); +}); diff --git a/lab-claudia/test/auth-router-test.js b/lab-claudia/test/auth-router-test.js new file mode 100644 index 0000000..e8011e4 --- /dev/null +++ b/lab-claudia/test/auth-router-test.js @@ -0,0 +1,486 @@ +'use strict'; + +require('./lib/test-env.js'); +require('./lib/aws-mock.js'); + +// npm modules +const expect = require('chai').expect; +const request = require('superagent'); +const Promise = require('bluebird'); +const mongoose = require('mongoose'); +const validator = require('validator'); + +// app modules +const serverCtrl = require('./lib/server-control'); +const cleanDB = require('./lib/clean-db.js'); +const mockUser = require('./lib/user-mock.js'); +const mockManyPhotos = require('./lib/mock-many-photos.js'); + +mongoose.Promise = Promise; + +// module constants +const server = require('../server.js'); +const url = `http://localhost:${process.env.PORT}`; + +const exampleUser = { + username: 'fakeuser', + password: '192837465', + email: 'fake@fakestuff.fake', +}; + +describe('testing auth-router', function() { + + before( done => serverCtrl.serverUp(server, done)); + + after( done => serverCtrl.serverDown(server, done)); + + afterEach( done => cleanDB(done)); + + describe('testing POST /api/signup', function() { + + describe('with a valid body', function() { + + it('should return a token', (done) => { + + request.post(`${url}/api/signup`) + .send(exampleUser) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(!!res.text).to.equal(true); + expect(validator.isEmail(exampleUser.email)).to.equal(true); + done(); + }); + }); + }); + + describe('with no username', function() { + + it('should return a status 400, bad request', (done) => { + + request.post(`${url}/api/signup`) + .send({ + password: exampleUser.password, + email: exampleUser.email, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + done(); + }); + }); + }); + + describe('with no password', function() { + + it('should return a status 400, bad request', (done) => { + + request.post(`${url}/api/signup`) + .send({ + username: exampleUser.username, + email: exampleUser.email, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + done(); + }); + }); + }); + + describe('with no email', function() { + + it('should return a status 400, bad request', (done) => { + + request.post(`${url}/api/signup`) + .send({ + username: exampleUser.username, + password: exampleUser.password, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + done(); + }); + }); + }); + + describe('with duplicate username', function() { + + before( done => mockUser.call(this, done)); + + it('should return a status 409', (done) => { + + request.post(`${url}/api/signup`) + .send({ + username: this.tempUser.username, + password: exampleUser.password, + email: exampleUser.email, + }) + .end((err, res) => { + expect(res.status).to.equal(409); + expect(res.text).to.equal('ConflictError'); + done(); + }); + }); + }); + + describe('with duplicate email', function() { + + before( done => mockUser.call(this, done)); + + it('should return a status 409', (done) => { + + request.post(`${url}/api/signup`) + .send({ + username: exampleUser.username, + password: exampleUser.password, + email: this.tempUser.email, + }) + .end((err, res) => { + expect(res.status).to.equal(409); + expect(res.text).to.equal('ConflictError'); + done(); + }); + }); + }); + + describe('with password < 7 characters', function() { + + it('should return a status 400', (done) => { + + request.post(`${url}/api/signup`) + .send({ + username: exampleUser.username, + password: 'dog', + email: exampleUser.email, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + done(); + }); + }); + }); + }); + + describe('testing GET /api/login', function() { + + describe('with valid authorization', function() { + + before( done => mockUser.call(this, done)); + + it('should return a token', (done) => { + request.get(`${url}/api/login`) + .auth(this.tempUser.username, this.tempPassword) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(!!res.text).to.equal(true); + done(); + }); + }); + }); + + describe('with a bad username', function() { + + before( done => mockUser.call(this, done)); + + it('should return a status 401, unauthorized', (done) => { + request.get(`${url}/api/login`) + .auth('notgood', this.tempPassword) + .end((err, res) => { + expect(res.status).to.equal(401); + expect(res.text).to.equal('UnauthorizedError'); + done(); + }); + }); + }); + + describe('with a bad password', function() { + + before( done => mockUser.call(this, done)); + + it('should return a status 401, unauthorized', (done) => { + request.get(`${url}/api/login`) + .auth(this.tempUser.username, 'badpassword') + .end((err, res) => { + expect(res.status).to.equal(401); + expect(res.text).to.equal('UnauthorizedError'); + done(); + }); + }); + }); + }); + + describe('testing PUT /api/user/updateEmail', function() { + + describe('with valid token', function() { + + before( done => mockUser.call(this, done)); + + it('should return a user with a new email', done => { + let updateData = {email: 'bob@bob.bob'}; + request.put(`${url}/api/user/updateEmail`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.email).to.equal(updateData.email); + done(); + }); + }); + }); + + describe('with invalid token', function() { + + before( done => mockUser.call(this, done)); + + it('should return a status 400', done => { + let updateData = {email: 'bob@bob.bob'}; + request.put(`${url}/api/user/updateEmail`) + .send(updateData) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong token', function() { + let tempSecondUser = {}; + before( done => mockUser.call(this, done)); + before( done => mockUser.call(tempSecondUser, done)); + + it('should return a user with a new email', done => { + let updateData = {email: 'bob@bob.bob'}; + request.put(`${url}/api/user/updateEmail`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + + describe('testing PUT /api/user/updateUsername', function() { + + describe('with valid token and id', function() { + + before( done => mockUser.call(this, done)); + + it('should return a user with a new username', done => { + let updateData = {username: 'bobbob'}; + request.put(`${url}/api/user/updateUsername`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(updateData.username); + done(); + }); + }); + }); + + describe('with wrong token', function() { + + let tempSecondUser = {}; + before( done => mockUser.call(this, done)); + before( done => mockUser.call(tempSecondUser, done)); + + it('should return a status 404', done => { + let updateData = {username: 'bob'}; + request.put(`${url}/api/user/updateUsername`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with invalid token', function() { + + before( done => mockUser.call(this, done)); + + it('should return a status 400', done => { + let updateData = {username: 'bob'}; + request.put(`${url}/api/user/updateUsername`) + .send(updateData) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('testing PUT /api/user/updatePassword', function() { + + describe('with valid token', function() { + + before( done => mockUser.call(this, done)); + + it('should return a user with a new email', done => { + let updateData = {password: 'password'}; + request.put(`${url}/api/user/updatePassword`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.password).to.equal(updateData.password); + done(); + }); + }); + }); + + describe('with invalid token', function() { + + before( done => mockUser.call(this, done)); + + it('should return a status 400', done => { + let updateData = {password: 'password'}; + request.put(`${url}/api/user/updatePassword`) + .send(updateData) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong token', function() { + let tempSecondUser = {}; + before( done => mockUser.call(this, done)); + before( done => mockUser.call(tempSecondUser, done)); + + it('should return a user with a new password', done => { + let updateData = {password: 'password'}; + request.put(`${url}/api/user/updatePassword`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('testing DELETE to /api/artist/:artistID', () => { + + describe('with valid token', () => { + + before( done => mockManyPhotos.call(this, 5, done)); + + it('should delete an artist and all associated galleries, listings and photos', done => { + request.delete(`${url}/api/user/deleteAccount`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with valid token only ONLY user account w/ no gall, list, etc.', () => { + + before(done => mockUser.call(this, done)); + + it('should delete an artist and all associated galleries, listings and photos', done => { + request.delete(`${url}/api/user/deleteAccount`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with bad token request.', () => { + + before(done => mockUser.call(this, done)); + + it('should delete an artist and all associated galleries, listings and photos', done => { + request.delete(`${url}/api/user/deleteAccount`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong token and many photos', function() { + let tempSecondUser = {}; + before( done => mockManyPhotos.call(this, 5, done)); + before(done => mockUser.call(tempSecondUser, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/user/deleteAccount`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with bad token and many photos', function() { + before( done => mockManyPhotos.call(this, 5, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/user/deleteAccount`) + .set({ + Authorization: `Bearer ${this.tempUser.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + }); +}); diff --git a/lab-claudia/test/data/dog.jpg b/lab-claudia/test/data/dog.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72c98893e1f70cfa87da870d340b97cc0c500128 GIT binary patch literal 41326 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 literal 0 HcmV?d00001 diff --git a/lab-claudia/test/data/doge.png b/lab-claudia/test/data/doge.png new file mode 100644 index 0000000000000000000000000000000000000000..b86188630d79fabb0fa7a8bcc0b2bfc34fb7d628 GIT binary patch literal 212428 zcmX`Sb9kK3_dnbwjcwbu)mR%_jcq$=Y&N!SJB{7gwr&45@9*b%{@81G=eqBiGe@t3 znJ{@-aX4sf=x^V?!AVMpD1Q3}q7MA1K!N~AqSA0xfL}jM1!V-keXAbk01H0`{>O3> zRd-UdHF0v)cQF1YXk=?(OekrsZ)&V)tZ(FQKVr=L4V3Sfq==xh+sav|X|ncWBAMH| zr~XWjr?6Wu3L)flXn{l=s!DvB#r$|$7DK&9*S&7}3;Q~orA%U(%DUo+0&C89s9sNm zf|$_94QI!P*0`J!)kIP$Bl1{RmRNw1mr2f8Q?262O}a;a(nuxHZoLR1gYRkkri88;;k5j9)Vow z_=m!l+Q?bQ5FH7L;c-A{tbytu-O$MW@m!B>vAyJ-en=nGtnMV;yvWG?-+(}@F^q%@ zG@%49ED0n~U#6`h1CyS}NZ>fu7!e*VF^Ya9wLc^m1}X74ZAx6NHlqLVgf;M;wAmhX zela*?*aY<86b(ia!&XAEX;v_1XH*hjt#N1HwTD^aeEX+wL#6w-rb(87~So-OmD;7!99=5LkNEiBVu%>C@tX10;rACV{1_Wg`C|SeTiSOpIt*3Rrp{@>zN#|F;D? z62JtrasQJ@7Qy)Nun2IXj;npY7edkDYDqCMAYqaFl7T{pD8L{8ZQ1S~=g3BhJEbZR z5R8SD`X50Bn40hw7uXY~Upzr+RiigtHVr&V*!E(i0FM0k3|d3~u^v|ktdT12|1EQk zxicxT3q1mQk1UxR%KZO7h(U^!+mEX+m_3-{Y0S)=MBzY3X{aGgf|&rW=8wun@joJA z8NPK%BI?UR{cl?wBKUbzCDHULbEZZ{M#SU8>in_7#0f$29>bAX|D!>Mzr+AJ8Hr4c z>PEs_Y|udjNV56`R_;#KbdtQN4;M4DgBWoCz^VI=^&(p24!NKo{FPqu#}E;`my z@Q2J-PKEy8iX=59Xw%pf0QyyKq!fswOEhWyflphh54Tf zvU%|QL%8ZNZM@SR~Wf3TFYa)*c2>4)hYu*Nce*v}epikk(8>1hu`-qMQx z-!c^GkW4=b#wgo}T|_BG5**Z#RG3VI{*R+scK8Wz-&?mNLUpiUkcdO&%SsYrzr_8Y zrUewlIyY-Mp$G+lg~9arQy;XMi4E3;hB(#(#7u;eaPZj23P5`?MT>yeWZdJwE(_G| zn?%MA$pMZLE`#wt6f7*cj{yQ19@Kmy6#YSFF$a0%McHmSFZo^na?Kkjnl<1VqB*3SjJw>gZ=JBg} zkw>AdTXA%ni4bm>ykB43H?8Dz z$Y$*Xw4@9qpPnG{FC&q|MFr2ZqP1u>Q9N&hU?>@|>i@WZqD(p+RoAOj39Ba20tK zk?N7kV*>QRpO|W*8lj8^`$ZuK$72K6F>~8WHjjlK=b!>JFeV!!SOpUzDYfGoh#VwF zRPnDO+L&ZNAWW!%e2a7xZ2QeaUzNqNdWkBh%NQaMAF}z#^KYfm)w-R4un1FoXY9?u znAVoKf2{-AVu=x&fY-5(Y+{Np-P3y^Eu@%!* zUpuVa&oAZJ(tNu4`HuLXn9gEXHiZ@kWoA7-6K|GEclNrE11?`=)O0koh}PsyUn3%T zzQkYrHy6T|$e2h$I_{WejhUnnxz!xVaeJvB`MCl#)V245t)C9=XS+AUuNI#}HoNyl z6*x77I%`tH3_6QnN{J|pO%*Ic*)4(MsLx8W%M@-|SW<}Jq9U`#RfwQV7vnp}|B~f# z!qWU8rlUOWFRAzGn!oxbi@*7KW>3fh2PZ)NHS{A?##^oTIWJh^Cqr15pUjvS7fA5r zhoZ4nSD5b$Ke|M)VL>vXeWYaeRwh0Xzd&&?1AO=WmHp+5C57t}MKG=Vlgw)m(|GL^ z@IJIJ_zs(XMH)8e|D?zK@J1g8+ALE6SEAQRkQKu1-|a`EFAtZ*7~0p6fYolb#Fk$vjPH~|$x34xe~prj`YR?_ zGpMTijPW&I#&f@fad}Cdf3VuL;I=7!2g!FJGDrPl(8s#hRor*|5Ov0~dxh$_+N8V% zCNq4par90gvLYyovN2uZ6orBHU5vdPv+U=_gzIF7IdfQ>E_@(`>3uE4VCqp45e}8B z3dKg$E4(AL7lx$6O0Omk+yQ1nC<hvzulK)QJY{%)_z_$}F!-mUmznmj?A!$JsL{;tKdAX=KCj~Cy@}^4 zpb}f&#BG`3e>p$ySV2@-z>ivY&9in|gzSY^xzx(!@cGC4r3%|g4(7R|`;XaJpq)eI zhx+(-d+e{Qg}HjH@;e7SA9YNF8QTy+dmH4vR(kK6$~t|Rx3|r_L}(kmQ8a1{iHlG= zwgclDMMA+gH2#XTWVv+(Pli(A;`ttg`3Fand7b&pN8?%4h0$uo?h&9ZvPu)=xw|;r z%s;+e;Mi|kTNY2~{o9u4KY+Ppsy zA8PM%0(qXVHa_2D5ZJpGsf856)aTDB^B)z$$$vngLG6?x8cYrxzw(%_eX=#yes(8b zRz2VP(5l;+O~Rab8UaEx7%6~Y5P^*=UAs`$>3SD8d4H{2CM=+`ND*~urbdiQI@$oem6Qz2C_=?y-39>9}9@wVb!%G|Nhb z81@NZd_jRK{gLaV$LElEeTeU_=c5s(+hL+P6`r*jZ7l&Lqu74pT{yF_oNhhylKgRN zWA?odr{h+Sulc)GU$(rh`y3hy3Q~BIvT;Hh#PM#gWNtk1e<1~Gq+9K+AGe1r!D|lZ zP1cI@igk+t4=4=u+Zg2TSD-3HYlZ+2c#;!1#(R!Rn4MAKZ#mliJHLWC3IeK-vvvI- z(T}>!L~Lgm5!nRQ6`mOqkeRG`S%m@moan<3OQ~shv>%B$u$X|EXOYGOG@CB_AIn(% zc#i|u5jT4fH_N&ae8q2NT2VFV!d<>hK$_HAecNP%%OgvGi$0=a>kg zw9V2+f&o}qn~a3Eoa{7@I^je77!R-~8YW z<~k1h5Gu!2C_~&xQ)R*&x2(*gAnu1Ea*Ls4LZ*=#T_z6PKc4*CexQFU>}qs&jPM!J zrt@a6wu(W(w(pM&dfy*pqRvoQAYV|ySPXQxn}l#;OdH7|lt^B3xSv09`SF@{9BGRD zt-7Ku74|WB4XFsDLo7b&ii!VU>?2`0so6V6q3}1a=hTcA5tu(J(|q3a2++azax6pn^Z@p6xL=c_af00ZJ%{kL8D|M^|+-{Rm$SO#}9yo z#@*KrjZKBR6Qx5l+43&%BowACvh99GsGF^LBM_fEe;{bF*b|a!nW7bz)aNEipdtbI z-ryv}T}K7>=NS(qh3vG#>@3Ue=Em&(liPhsx38cuiq+N9AqPU=%+o3Vc2mLW&W-=V%kTWFqK#}+!)am* zp!*p{-k}#6-94m_r59(5tPn+e~vVH7-fr z6o@sfiy+@IK;_u{mnAVb^qb^Rcp(8n!3AMi96Sw?=v<-_ZJa`q{tv<;#7DYg{&0T+R-J3qXm0i=% zppx5D-XKBp6NV@-+M$!evJfmj0x;op>Fd{~MUUsX{bXw`^Q|6@j;FH4iySI+uio>l zgaeVEkk$!5jLn*!?)$?#L5I8dsoxYFaBwSnu~r1H5T%~hlz7+ssC=a)FO25d2*3MeF!lyK!bAW+i}yof6enaSY*1|nRR^cWN2KK z2Crc^(ZFpdh|h(B;EVm}gWvQk{Pbeb24%*Ws=IaSD??HIjsPPib>wxWn3k906w?F? zk;)=|wnS`m^RIP~>s0%vGpj>!B!$IY(pd5lHJQ|0p-nT7=J67%>#G`ti?(HFx55X5 z$S?`AB0Z`Y83K|^{t=``!5U*tv9$gr3g$G%KfiY~!sn*OtTG3K4T zKaDUj{kVHkZ$CT1$xTqW4{rTDA&qVFwWCl;wuDYQGFvic?np_(YnZIC*e2{Z^9r~P zsLMtPcu5eFd;C+`>h9J`232g}hx5%T| zu-M6Cvao7Voo$5OjhNjPoZ1SE9sO3y-md?^*C~@5`R}fOY2=7E zbHY%ri)5+(Uw4DQ&pC)_;BZ8}aB-=65lPooK*iYLeW=cq#u@avCu6$M-}`5Zw1yb4 zRt+XCsk^cQEYW8#y(3nt|13-G#z){F@MSKe|EE>{8 z_2Vn33@@#dSC~_gX!fA3z-GcxjkemXG3nq@N)f*FYPs*}Tlll7y;3?5>}l`_c7I3a zcvZLJIdaK+skRAY+{s8(YZjmV-+s)*A?F6=&sdQN#Am-r=Bm=F6LRJ?Moy~i>lk*>P_ZQL@yLjKx zaYGq)CI;4gn3>VN(~utz`~9!ucBNfix)DYOTw z>EER^G_M8ZXi$RFqd(Cq6K(jmoW5Vsq%V$V$Rk9Edyx-Z`XD1O4*)!|VCJFe28Y)C zy#LsCc^oJH$*#t2FE^N+S1m*_q7^`0Ceh5oOMcAtly;I}!n7xP>z%p)xIRRi)s>Qv zI+&BGSsOoEM?Zk^37GkMxSV@cyT#uK(dFfC{R9)Z7)au(eGKOG(7snj)KOc8n5a4m~`iK8{ef?bJTgpQM??5&RJ$ zMNaq2$k#6aCnzkd-VYbl;v8K~Oi{n*oQt&%-=og37?qm^T-C2ru||TfAa1LOIAAfC_C9Okr14P7QZB2trfQQz?&mK`LAqSj4BD6lhtmRn z2NnEILU>2X( z=rn4$O-k-VL?9KBcQfM((VQ6KMTQ{?RN|ck6TlCXZ_E97+Qf$Nf}O@xs?o#kyinE} zciT(Qg|~=djB;$-8=X{K6n#Q7+N^nJW1Ype+vQ^~&XI<@IX~SsDM{nEQ^4I;!`2cx z3)T;?2bfz4=;J}%MsCY43F41oE6JT7&Gb528jCT_JowUR z++;7R+yeRoO;nC|dF~e|?t5Nq?z7cCh}_-~^_Xoo-9+0dz>q1X#~Ec%N1fyEI1e>o z!_X^>9ZDX*L7WMJGJrB=YHC<=HS#{GQ9SXtN~f4ETpJoKbs7Ip2i6_zh+i5WWoBz)jf{&+VNcY***`(4zqc@ zO!B-bT3YpJZ23@#h?!%$XyI4q7;XF^s}A`Y5Od{Jpsz^OWP>8YpQa26*>lP+*=mC` zuq;RN+c#5Fk0I@!2G7IFu;H=!vswvt=$o*PhZ0#FUUhEo&>YVk9q%fZ?8mAzqm0i7 z2nm^Wgp!qkv4W^>08>RpZ*Ui7r-L4&6%HmGte18PX(-ZjZm${9%ZviTe!D`M#A;FCqs}iAN!o>_8e*Mp|v%gIKdkC)8@q6@XP0QN875bXoY@SvVoFI3nBEG z#Qb)(=s(DiKaa%8;>qGxgSKqcc_GEWytmgPhGb)!Y}!5O+qm7v9>}8)EvItEi{})~ zt2TC95JfP8>#lTZ3Hasj*wvHx{mtmf!k@r{oF;w-Fo%R zG0Mm2Ac6SbYqnhzHjR(1W8CE;-1|-5ZInBV^>H#0qQmwEt3#P5FOVl-cn{|3a@&s& zHs03#EhUi3V7AB)_yc#&S;g)pg!!ihVCI#uQj$k&t#qrND~^Egrl@!l%8ykzWfWe7 z)n*#UPSloO_iu4t@9y9njmF18dDFI-C0rTPI{^=UDmjiss41@mKZ@O0C)K#m zUo&D_q1kuyZ1eqUb{FDb!~MINX=P=eYXc!w9tcw13IRgu4RZYmK3D*spIn8)+a0kA znpau%Z2_%n1&jm@=t8S?C~oJ1YNF3Xis$pwc$le8*2~#bjBnHDPU7{)PtVV#xjlO@ zw2i8Y-G>l^Pc-K{68F1ghsD$u7wV?Mn88^zI*Z(O#r7m%-H?_WX^?Y4WZo{>ET=<` zI!ga1EL0v7s}yu?z`_yN742%=oZ*jXGeTx+u9_kUWC%s4s>aA8r?9h$y`x;oCVA-? zB=5r)l;+E>Hy9QU{)bsUi+`aEF(c9s2j?Qw0{|kV?c}CmFF*^fLyyfO!HU3r0LMQ( z9yjv}p5t=Gzx?e6jkxe&a`g-%#*{w37c`iZ& zF#4zE`nQ$r9|vjovZ| z`J6sfZq^2F1yeGYN*Zf`m5FNfn%gs1r$O&5@+E@m@dU2 z2+-o)q*MI?s|2xdW@u;^VeK+CIiQ^nh**I&n|5)za5Pwi=b@oC)K=!CrGn$w-HR6l z@UDb3AdnRf-y)T|isfcNgBS8(xhc8ndP)ZtX$6Z@1t0$I&#rlMR%s~w!{R8XbJWH6 zYbn~t*0@0zcDXpR28LeQ8dhjKlG!k|W@|W9;OcJfvD@?Ec^5-1nm^WtP+M3-E6@We zO}B*sReRWx-)aRd_$Eag4s4(YN6FkK<60Xn(d%qNdGROPFGnagJ2f~O%KI?bXfMQOo#=DHey)IC z0UJqj|No+*L4Q78mCN=+XI&&u$qU-Ue;|h%2wHz_)arO$6ictxq{Y)AqTdTBBNgPG zjcjpJ+ce!^5u=KjR22TW67hU;Zz?OM=Y&sXo}3G8W@&NxT&A@A65-)(`dJ?ti^oJ8 zb+T!2o&m?dOuKX(*x7E{{kaaqkj>j};iV|L=U+19$WZt9EN4*)Q>MLZt4p3g%Oos6 z1zdxPp{I;FTm?&=XOZPc#DgCq$qtttrI5fybM3T8afA?BCZum-nc2NaiU!;DRezio zMs+2Rqf5o2Rxd*)l*4I&+fxSbLoKh1)Y&`0ssnxOSDI^lFa5h7GcboD{`HeMTC3TZ zyupJAR-HXv5+2SaG_G-LmjCj{*w9wLaS2=5(WzMa+_sRro|15!R?E-Nd`>O$dwJ^&?3H*|b9g+hM`v3vPCMGMaOap+iR9p;p~UJh@=!m`G> zs+96q^60?>uGSsd>C_dZL+7%7bfE_OKGxh9ArWD24~353z1ao@zUa-jNVe`Y0M%8Kj#_E!>2&1hGT3Y`Q;8JlrEN_`R+L{zJv zBunNuQ>p*X<{g*UU9wdeMyjytvWu;(5KwMZF(02koJ(uy?U7tYq62?%2~va{;q?+ogbMb|~m}nbG*& zKwOa4jzClR*$z?o+CaVhkb&6NP!W(o$z`D=hIZs{@-Be-#szl=e_&9eKE;sBDZa-| zy&qV0-DKm%2iZftj2fit;K_zN=CQLSYJbM!{^aO>73p=-q3+4oe`LSsQsh`)j9;ybvpQaZ}qNy7Az z(z+~GnP+jmZAGau;I8}XdpR)&y6fWv9f$mNfc}o3@~_t{mPPni&o4X;g&cZdGB*=}*0;kE0t#Xjv*>Ln@ zG%PhPI*}1Vi&;AM*YB_{UN^yxD<9sM+BH`(n@&Q>W=|nny0+b zPd@W@Z5JNpHBVlD3b2;JO*B@f@(&N)RWktbn8LGVuanUGf z6Qe$dSMIl!k7u-tc1_vV@Nh#cTP9o`&T?%M z6*SfLYyo>4BMawOH9R)S+Ris(62L}h>W}OM5&54|JZBqhZs)6u-M2gQnX5La9xDrc zi>&?bDPcp`EJZ3*>=E&yG@L^RV%5}KF>~5rzT=C z8Cv!?e9ljIXMVF2O%Xm$%sZS{*V+hXV3c2(x3XfL>_=ZM`e^r(FYB-frAFKnUP zw$deOAG7a0c&v70d&*HI)AooA_!^MRO!Q`&BTj%6=<1YqQ+wK#QDW39^JGW%;V$ZX zy}tX>-SWjX?Q+w_25f-5m%A6SbOj#~C7?k2sxjHt>vG;vi({SYKC&93%ZBo4hq*m? zqp?|Gs`qCzpnoaKl9LEPlqA1`N~c)`GT{7zy`llF-zy)F&T57yZQI8;pnjQNrf*43 zUkE+yeA~jORk9@s=b8aN%%Afh~YC__GOKqk*n?- z*4;beMFWOo{(3ICT6te51Qh&I4JI=|zrX*m(v$N1#tv55A>8w)%feczET|}OE^&xW z){v*)CJi?=wRy zN~@5#C<$S$KIGYaau*`}R_QA*!beil!G3MW8krzL6J7MWIn43P=dewi#eI!adpX-vRDQcV z8&xaF|Bx)geG5ERRa*i@WG&>Z`{eh#3-uBPj2l6jmeR+B*|Ev5D!&n}2$m|YuSSpY zE=(2f&Uk4V?q-P7#Lo|f`^rpB!M=pH5A?pCJA-C5U328N`&j0l{MzVjyp#I9J;MOKm>^UHfr<)Zj$fZ*oQ+SAcHD9>fbIr0CFoD<8OJSAk1-c+*(zV zs?539k@CdI1>NQi39+!PAWQftCk!4^+0pQStC_>&qo7KKF-XZ8a4KOM#}n>*P&F5= zlvpY$bC0W#G>=**&j&Y}Fp6Qk)(mnd*o?W+YRFqM@gKNaW&3_CdopBqG+C~mH>EuU zEX$*8yN}3Tr`*DXhNsM8vy$Fo5lEvD_2wC$C~0Iyq$yYs1>kb${`H^QCdv~3eRW@C zk54!0UngCdC>X<0JWGtF-0atMelXC)K-G{i{`zCp5&VSw2$;|6?UeZ!Q=e zbV5?6U?PArzDqeNsGeeG$wleyxwqDRXH|;IP8^QDECj)A)I@kYGbt7Z(OqxSOvy~%5F5=wvV5XXGmTH>XWCKfgAAKlSytTpb#2-Q z(Iz9z=(*zykLma=!BIZJ@Hqho%|%NdG&iq%1SyaKJGr5?hAM(Mh(NX@N=g&Rxm3CuvetPIo(~mjl=yKK@&5wNfezDcDPxNRMaYbdxFIp{8lQ#1?tp( zuv}j7`*hA&ec5X7DQ>TNXXpG6FXg2*l3Tap0K@!`sgKmlZs+~N!>S7Wc7pYetHw}v z2e~>4EW(sYcl5aH4d`DcBT7?_#zejM8o9ZsbZS*vqW_;+zzB@{7NDK?`Ueg73t=9^c=h%Az zQf0yU$HUMoN&c3V(1v;!>zqv>nAkFUv>QiTaF0R3PWXW=LaB4@t=&$;p52yz#|V}| zzpae&tB<|2%ADOzT#VEfo3=HJ%@vHk=f$wW;I9e1F8Vs(7yeOGivoc2SmF$~8){}n zwJI|8gkaA*;~wSuagkd-$95c0;V8E;Vk`RcZq?X5;2LjTRFFDESj9|G5I00!!OHvp z#`Ow-MOtUi(sF3&C=+z-4TqeC%GcV`80lRLzm=wg3o=JmKEly~M@I4y&JPS$x6|yp z_H%m%wt0D6Ru5g?qLbPATUFw|+<|alE#CVeSF?|115W?0*+TyL_kQ#(+DYtBiK7IC zm0lEjQXzwPLo;=St;mp{K z5qOhna878=0S|TWPpXT7`%3%65m$;az2Pvx`=x!4Zk$yYp3C@V(4VDBjkN5qOHPh3 zO>WqF0WGO;B#Ia#h8d!u9CTu7s1ti9og`YSG)C3XRJetY3YZcyT|EAKc8ra5AU&I` z>-&>@RTZxzZ_`Vh|MS<wLgs6fH|cQwu+QD)~8+}Y(bDm41IWGOjz&reRzKRS=>A$4_U&iuzD+O*a_f$Y&j2b=VLZvf<_h}DK^#;Go(Lw(( z=(dT@tT;C&Zm{Y!g7Ej`TPtcgETSQGbiqU~Wx-@^=br<9WUuj{f8?;wv^&K?iw}^5 zhquE?DZ(wxRc2?RL^bBv_-yl`*A=lZX7EAQ?*69De{9(iEUyb)eFLA=rBs&JQ3h`c zlIqFMD|IfmWwqBQ$$Ne&yK^X*-h3JNy40O_?2h&nfGM{fe0XElc=_@VP=)0afH_dV zSgg_I71S9B9ZK|w>WF0qEgyX)S*fu|-;2W5h2*LV_@x zk|oce$rLJ|S>pr{O4C^zSAMpo`^3UGsjBFfE-&i4ub1PS=u zZO3eMIqYq9#w{q99ClQO%95Uj)_Fn&@xbj>68$p5(Fy{`w$S4SD-z=Ro^9T<56ae% zt9C>)aGhyn3U$|ks35q9fREC`F{#H%T%SidVAn}SDR`BVBg;N5)hvS%#qlJVh36p} zkX9B#AT9B7jr0*X%(*?Qp>~VejHavAPX1nz*ts*n^3wjPl=0-g^cbh>y-L&VmE`qR z6Z{?B#aH9=Z!w@r(0*jbBrlH&V3*V~sJ{fGX!3LU#Qlib$Q!}Io?bo8gy7YG< zb)i;|+@NIGpt%fn2Gf8qAYGm+R#B)nSQ8-S!;FQ<%^N3cJzT?H=MzR4tlp>Y_66b~ zQTNrb+rq`$N^K3n!oBk8e|~gcLILGjqKIY+|Hq;me#9;J3W**gW^OfkOuvn@Z(kFjkW3&BbPo!@5<5&0OG%khX4OuPU;?!-J&<*689od|X+gXtqm zzZCTS36&5zyz~pNyIIgh3h~Yf+tnE`CV7qUD4esA-N)|7l}5vm>X@b?uXWoppN1%Z zC6>uDZb~=JafzRS--7~!{wZkwOw%$T=J<7NRLqJCkf9j&aFe2NpJFR@G*?{{d*-XL zBEZR~d_zO?LE_ON(Px_!4<4ybep10e%)YLWO7o(h!6cTOz18QUzQZJO=$BBr^3HRl z>H@d6MAk@LQd>Qvbo!BpcC7(6$&m z-ex9Wbid3bg0|a6Iv44%n4X6BQjKho^ZH7K>B0$UzYJ41YGeHEVU|P9Ym=0QrAUXDW`xMr{jGb@ZA&`7g*-N zg<2uPU>mR=ohg3*5q>OI;zMGRAEf!j30b9BT6)LeiZz;q)_z&KC=kB+QONgV*?jM` zo(ea114{^uy*S{js0U_tX%XP16iz`dikTa<3JH51d&tu{Dyia4csR_g5gLV`Np|c= zm_mXm0$p}%7FxHYdd~CLc5~1Np-DV&VS|zAcvYuIA*A&h7>|gtejDBzCgWQ9b`k7! zHd^+tRtJX7ce?U#cy7=1uW5G~CE>%M+q2YeHD7jL-`scd*fx#_6bW3O{jTFnKzEn_ zzAp#6{oCD6$Sjw(Oe76z4+`r=kF!tuPw|=7PRkxVQ3~=dY~KhjM$33~A-nSBAKJHn z&cT1trzoB3e!())QsEwbP(g$Dq9Lf2toq0}HeX=HXwrI=s-l}BQa!Te>6KAxLOiI>hqQPHj z3-gHQN)wBQuWFbzShqsRVPy`ch^F;O8Quus@=VITWCgLJ&`ATG60|IluPHpsIl zER28+VEu#7w1%D3C5?d^PMiUy!cCSbc<&9&On(oKa!h9jy!Q!oJuNSzaZzb2(n?@0 zoTywpn-X-~$xM@*l}!ztP7|{_hd@Fh zWy^t5Jq`hsq`n5kZ-#6ORc@yqRb(qXm}Rs&4bw`S$9LPJ;^PgY(rzFLr}9uXESbM7 z`*Z1>hiFXgTR>|{M+;GGIJq~J<%6g0=Ku76QGkv|Ge z!d=ns36fegh#^G;{hTTKAxFVm>kvQbW7Ok_LJfzJ_hZ!jdxR#`>4^}A)Jc%L*vL&x zG|;>uxMgJhakxiZ|Do@I6D0%Lnxd>6iSm$*9s+2U|2Ig!Dn;TMo2o>8aB=iT?s7uM- zTb+#4NZH{;Vm|znh`M&qYNN0)r27f|)F2&DyCy;6<`HMv$Xm{!_%K2`cJIR&e~6z1 zVvN*RS&(13d~a#ehPQcodUeQJ=Va-;<>h{pxpVaTupHC-F=_0|-YoJ}@6I@(M5x|< zBcV*9et@$^66-l(&D@layh#69>)Eb@NmTCfmsR42*>Wyp5l+Sy>0Gn-P5z;haF$yV z5fXhi&5b<_De?3o{62j0xbmNZB=+$H=&BG9du|I2H{=*l?Cp~hQ`II-+l;4yHNewy zyC+4l)`ls)HUJvXx`jOoCdccxn^-QM)ipznzc3$p#|Fle##4dX{f;RQ$)1RmaW6B# zo`Al0#3V3!b3scyDwPcYjEqYc?*3(U#&$d4uR(@;G!b0%ODZYct79ypZD{OS{%ZhF zSKf*VcQtS z*;PR$x!`GGsvuC0;UG0t{pE>sj`zIES@fHz$~C1$z2j^Ej1rg|5VLhiU&z>%`bcTT z%MLMd!YC6R*C>FS^EG$VR8?}D`QC1U(T+?v*{Cqp8?NsYbgaPV-PdoPrKHh!PTKC} z)#_;sR;HgO%Wk~>lU|JB^{%ugH%@VElCuQI!Y|M!y{oJ>Ny(Yg6SZY6RN@w6NuULDmN2h|Jy8wN!NDlw-O+QbQ3eYu*dnr)=U zt4cJ~ez>2+ z`eIIw;{G#h$b8hhJg77oi?HhZBGsrE^$|Ml@v>8_z)f@rX)SH|c60DSAX6(kb<(ur zneL>DoQs9%h_pTAWn+q3z9L5s_YM_Q7`>d#~6pSO6*RN10$XxZ-K{Be8-NX_% za7ZQ{=3;R=k)M_u^#sJ=Zj2g4{to_?9Qyd@9YW*#Vqz~MfnvUi7&1`^ql+7})$iQ6 zFa4J<1P)`Dl5k5B0}K!?EYh*`~=dwjP5|jtYlH z-)|V0HBMwr9DZKV@v+=y>UOl-Fm4U@0YWwjfhPr*d7sLgZ93jx8jIOwu)ohI9$CtX zVDdg1B3A>ekpNUi<|D$=b9??!bG{_zfMYUntevTNj9u=qJ4O3HW-#A1`k2@Jy*p9q zL<1E^W(pX=Mze9P&_s!HKN(TC>Bo@$#fMkDd>N;}XcCwo-7~C}UQ9J0{J~-anaNAX zw~8%Ug-Mp`DeIZzP>}q?E2lwswE5>gP=rIK%e?kGZz>5Q42yfC9ewUAa~wrkzuHxY z0VA(OWy9;R@vDfq90j)uu$M?M?+2WDf()=6i-d#8B^o0+QT-Z-`{74@xALcp*f{85 z`*(q7Fo&~l=u$eRQu}}S;gWNnT=fKL-f&kr@BZb($&<})h{Rt}_aCvCLfrLHrc;Jz zo>q5$yqPv%x=j9h3wJJytRD3@*q3^Ib9~(Nc)wYQdJM|0?>fY3S!^rHLo;A33?an_ z{e9G6@f3=fmYKvNG&-|X*pF1K&x{sDGki~4Nwz?*$Y~32-VnPg1M1qJlKVYK@lGUo zK~rmn#d-CdHAg~HUnAkXj^}7{);D|}+l^nG{VI{b#D7a~%&=x5S5$*60Fho=wZfy`jd?P4Rg^@P6(3_RrD90DAx9>xQg!5MsU)d?0w6 zQ2VzRrI+`vfk03-274F(Kj=(*#34fHDEcRnSW+fnLz-)59|fk2mFRO)-qiW_+4v6_ zbL~?eqlbTZD{5nLca(#*&&4qi(hkS=1iG4D$gKAxJvv{ta2P{u7_?KKi z1>-hHZHtIksCQQyC!{AgKYP-XIjY(*-uudk(9fWxFTF&|!CJWtQim@t==BUTF(JZq zl;*AKNU7Kgnia^do%_1RV^;tyyqICXA7X{6*o=Si8ri1AFmlFp$m?f{>}wSnm5G4W zPk{vzC!9Eml?i5oyG<>Y!JRZ#od0I_#7vWRB=c9nkEcz?^-v>J?-Te9D#%(RbP-D2 z*oMDkP&VaV`H`SzeV^fVzPMxineO?JY;a1pnHj-lH3lqs`Su%J$Yj$J#wM=Z!c`Dq{M-gS*3-ArZn3c zI3I_=HYfbA0AU*%W9nIoMJZyow)$O1Go^%}Y2M?17kd6 zLF`p}Jz7=6r@e@;ns;8J7{Tkg@xFo;o=}NcetiN}RnRub5&F8?y;Mp=Ug_-rjljOFj|afEPAD-` zZu(QnLZS-Z#sMVammMS|Tdo1``6hC<@=A%?zr)XwFRYt-9>l%&m&jBKd=yO7*l~ws z>lusDmUB4zfyKr)S3@S6O2(o}8FO{YAO8=kKvln0Dd&Aj+Zs=4tbHQR*AM{ygGrWO+YI0b%h@dr)nwHHra6@UBp;>{n616RcQ8Ciav7EYN6 zGcvy79`~q5$}ZoE+XTv@tX(DHy9>L3vM{#6;Ji#p8PY$`e3o{|z!Lu+sxkNsUn`7h zTw|nB>`|{!P(`BzM)gfeo1q%+uo5-2q$|S1_%co2W0YR7&rc+YaM9e@#&Nf0^gWb1z*!Y0qtNAbFj&uBJfZTUV)*UDV91GwXf0B^B-_w;l$xru{5*6BpxHq?mW+dbzc9sUXQDr-6t_2|RDk>Nu#cf321dQ!y1LY*7+N(s zx@TFIrch{G#|dO;chC@MttCok6=jFwRbUY;+VyC2RFS<~p)Yx0yPf#aV9&M(pIq4X zU~2U2#j)ubo$2t;NeAhr9Ut>Mcg#~R`1WgOHmtsHn^{-awjN+!qJiv?7M>+K1Gh{G z9N_}BM{g*&qp4lpSgFv^M69OFP&bs-%eWlQ+A%ywh^jX_n3aw&f1veP5Be2FV~vG= zOkhFU(v*-dg{8045cZ3(U`i9K3Lk21@o|9Lu>q~7ktt=S* zvIhoz*+IQQvQm61*YiO-r=@Tuvjh5-K%D;`+O;_TiTJz!Dqj01vGal)8}NxjUV@nj zGnQ;VXcgKIJR86Hz)Jt@M94Lqz@6qS1DU+waN4ra<{6U71e85kAsg;E+)Fw=JJ)G< zX@~0#=G(2Z94}|5XCXoZaS&ISQY8H>)-uLk#Et1vvd&~kBWA-sQiyRkO*+r9Xu6M( zoz%MO^-PK&1eBs2!b*t{%dcGda!%VQLLW8zUt%T;*sQ~RX%F`_d{}|jcN?#P! zpvLH*mUI09g*n}_%Y}EgL}F!asRdCTEeSPLUI6QGb16k-)>aLzN_t}HlSwaD2J3e7 zS+TWnNYRk7O&akO7!eO+?g6DuooWsqGTc7RII2{0VCxgEgGgxH!hNt|6Z{!(nYE;|Dua))Rp!i=Sx4)P7UQNp<} ztlUIS@&xTIsYM0!m6*@5AslAnIe8Oo_{LH(Xr^O@e&txFu~oF2+L{)696GJmTyKE! z3p6E&q>jugqmjp;A^a#)JuI0G5aGd`*CQw3J_$Y|3Z_UpBOKEsWo>wy8t%Zfe<74F zBBdsc@3o5l?oBgW*FV^}_Re!hx4i%9u2ZMiU%J|D7K{vk`=gY3694yua;$bM9yu`k zzyI*;ODC_6b}1K}Z~AixTqbHH~GrcUneba&m@3oU&*S1{J-X z-Lj<%%H15dh@xK3wMT7)u`C?}pD%Wv2R8;-_*VT&Aj&cHgp^(;Gu~7rK$I3GF(##S zMN4!0mBI3Lq1Ecpy@*DKY7Hz32yI&tarT_N=TYY162C~EPi##gLX-} zjh&*ejjp;~%+&keHe=FG?>Mi&_y5JW|HU8wNUnP*OzzssM3}Mk5r&sP*Fc>Ts9VaR zD;!$+Xd?>_9q?(QJ&iyhw#MW{%G2--)U$9lBb{xM#)sk)pk}_;BaV%7WOTIZ*YqIE zjWDV81{BD}so;n670{rB&A?D$4F`zuO0=}s(p}Jf$VD?A#6~KLW_QivkYH>VEI8ey zsWMxuVz6_=)V2-x4(zzLZ^wb3{btvV+gRFd@=JPS4895 zHSU?iQ$Kj)@{1=fj(4Vf)Aw~%S5;Hjw!!Q|!6c$0j+4@zJ=J=WBw2ZES~xnRUNJ|7p#IJCIno~`5@#v_lZw;9WTWs-3Asgl(g z`}*SFH#R!Gc6IOCtyN$2CwD4!O!Owwl%BPf z(b7U722FJu+2AnZW$PN0PHA&uoh!i!0K0#)Mz)g<+Wm~$HLapm*OijG*PmaxVpZc^ zyVI%q{jz18^+{{HC`_`H(Y-?!stx0tL4bFLoL z7=fs4jbbIO1&K7>!ZZtZOM?BlSbf35W0F|VStzm0&Gq~J0nyl?s)!i7qismr!sG;^ zXcWuDzz2l%2}`2!01b}gTw{p{Tdi)YYC_vcX?ph?%hK)1$d?wX!QmpD$5P?ipQ_tZEV+1wjK+jM8mz2wkLPN@V1$L)4qJIUMG==kWM>Xk){GCEbrcG0a= zgV_lL;e4!!acEkV283rak3wxjtL zAl-EMa|-U0*R2z%IAiXTaQM(Z+} z`bLpRn3jn0&~QMf45hC=IYe_dIh3}Q!^kgkw^yorLWb0XGqo}3MkUH~Y>LQ_9rnwx zfOmvtMqD*Mw`uKzZg*GT3bE;-c=4n7_D|yRU;3xc#!gSj%z~K+GafH0q*ShfQ7XFJ zQIf*$qu4lr!hX7?CK!wrilFt-XxFj&C?+Fhta~-Z^%SM+qAXh_#vYo6=&5dMBCS@t zpd_L+ty1YucMRQy3Vu5D)b!w@Xd%c)sfrk?aOgoOQ0j!1Bz>Kpr$}y2NuaGL3v@~d zShJ$!N{=cu?W`JZuA(qFS1chAfZvQ~^@rQ0YbYsb0^~r9$Fu7${m5>K4vQ59*p8oe zuGgBGYxV1qf$h{y;ZrTmDC5K^{e z8;VAl5j~f+DW7xPZUsf_k&zL>R+UMy9p!11E;X_&>D|x@5ClX_*zZ@A`WMnf7mLwW z$Tf&f9LGh4YyU8vb}lRP!>2qc4g}rniy+jfgEG_n2vb4q!>`@#AnEwA*JA19u`yc zMtWP7?N+zjX2%FFNg~{$RJprxZ^%>01COzO9i3kG3a55)~!hap0qsL(`>{PxC_ zH7ZUYaq!LRy2uN4tXHkQyXlqFmsVbEj{GJbdN*#kNaUM$luU#fk6}7UTS5`oo>V4# z2tIScd=^nokQ6c-OR1NGF%tr1PYGghH5yJRW~a>&ZF4NppR_yzG(-m7c9%9e_I46V zC6!27w%Yx{04ZGAA%ZE|En-&BZFv~LKtfp3{jBw@zl`h@l~Vdh`xAy5!Nvx&We((| zpx%aG6s)L|yLe3JIcx|Oc_xzgGTI<9k;c|P5?%I1i+*?FbV@9kr&LjtX;y=GXd@{b zsSVGvX$W1$w!)Yk{gbgvjh9c}sq3QVBSuPSJ!n8Vr@3gX zW~N*BACArS3T()%%{4o3?%pdWr^`Ech(61P+jf##^^o?H>=*x)zm<-vzQ)XwUd27T z>;L?pF1~#7`kIyZT|H9`=9 z#!;w**VeJ42&ZMj7)O*Nd5oRjL&OAJQsvY#IhM3c%MJ$I7$vwPh8u=i&yRbC@y64$ zb=nZ7alT2m5Inp?L5!k^hu=C#p5kltaHc+%=8MT1lMlreCwj@|0y!o7H=)#z3a`;z zamCzxVB03i67VR%qm#(B$M279tK*jU?B4gon(I;9yq{zu%y>K#LUwmc(KAfhIPi4= z{d=V@T}0cu;>5_&?pM=3;T0s=68<8H2fwv#p{{n_a6lien_} zDVbEIDPsqLJ4~8XDcOK7vX%$iDix+cST5^@GqnJtRESApusJ45p=S$*DO($EW8p0r zP4>)rGEE<{!8qooQEFoaRL}0ir9smT%m|0;u>ttSz3UiSF0r*J*S3GxiZ$2 z-l6ZG_Dw#cLSGK0so2CZ#g74xv37q&Tkt{Gkc?Sz009F38!16D{i<->?N2;R|Ju4y z)j;+7>fZhG(zR7{v#t4_8Pw2HL3SI}++1OuCI+DtvuTh`_6PK8;CQk>puPTgJ6-?G zT##Pm?OU54ym{sIm#?fCnR4~49rUZ-d{fN@R&?Qc71sOYV>7wnjm4G`X~qs*7lvnQ z$jRv~b$IpYRW+@4yVGrZ&aHS-KC;av-ZRTW+U)RvK&+q`%lo2Tw1uuKHu%+IXv>Sd zKd`4E()@N=)^$biCgu0EA_s#$>IX`r_8F|vI3Ok(F4`qqNn=$D6y`%JqkpxdT08Qx0Rsk1gLD^>(T|Vy?vNQ75q0)i zd#?e|5D1Wiat1|{yrn$`B#9YcvtSx7XuL{gV8kBspI7A!eH^*s&wy)&wt&~8k-c%OLgREP! zAP&(s~|cGWdwo?08dH&4OvUg%G*Ri%#1YYYPJqI4xe|q z-s>vAI1UH78(>jY&ZCvI!sM^2_Clqtt!gTCLA1q&9{wg5!?V%dU{E8ws+K#Q-jRbk=^30!4yn`RCN@5e-@CoqQ>BkQ4Fw^mz?hnjrM--4J$(AkzWw4>1#gA$c6`*0 zQOr{6Va;Y$`hZnq?M9hIu7E*Ypy-g?*Cm6jCu??K*+Q;ptsZGjm-Br0aDTYF1H05& zH8Mt1lpMd9hH)5IcD=X^DHLp^w1mRQC-uszO>-?YH;#5Q8g)TsMI~dx8hdS|8N1W- zy2>cn6m?y5?n!STBBKsWe%;obXV4>1dS&sHbN6(3?lkx69=})mhU5#7b9TOsMW`Z5 zBUe=G*%S>FlA|2b71~v`e^W|6$fbCFa_ucYDNY;s6rRxrDOMW$%DAGD?|m0tSBhgmN7w|>E0=8((yqtA~2`rm%-ho8M!H;lfPMc|{OAix3lHb2jP z%>p0&^GkUDAug-iRwKyrCvl`)rB6Pof9Kae{LXLx*>C^W_XP{~ZLaxj@@(qrdUoJ{ zR;k*}JDQeRO_ihtnq#r3v{Y!2x5Hr@U1<62r_(GI15;p(S4}|VAn#E%QYH~PLDq~R zDGK9fULpcYBbVUx0s-nUj2=nOYC&UE&8JOIJ@^^iIxl#D_d5&8i^t`%Qo7|#LhS=N zTvcf=bluiaiGXRbm#35YD$S~3mbn#7ZySjo+QTfzxZIGWFdOd_Y(1SuD~BZxDrsu-80%*0hDcsn{bYp-ARG=ITFC5?kgtri9~75T0yF_5k)|FP9f_GGZY%U0!M z)$infGAU#=_tg=<3Q+YCPSP|x?NSeNBcLwKIC$MuM2)LFeVajg?tA2PdR@e%i+7< zd-&0p;}`G750CNx{FCGR$Km-jNI8^Kc(~tw^_L%i`#=Aa|M)k)`*1khBH#~LFf&}K zb*B=gY(0{o$C7cXu0p z9qmdfQIy0~`{%|!N(d#6ZEE2MccDT{(2x>qkRcZA?A2ofWAQXl^9LUBut+W3IDmyj zv8y6gyX4R{Hr;lB7fp*k&I7!HK?mEvh|U9A_82{*Rm)6@zDIUi%_gLT0{{^iG@mZ= zH^SZmc5j97c6`)*QT!DTz1=M^x5CCByLO!%YL(aoQ&_1Bd0Q?yuj;=@IeaTdU=q~O z&S9_z&gU@L7)c&%Ll^@PlA#cSOI+;zYDiW>R@^kE&IS~;Xlo z=qOrRjpG+m;CDTIH(d;AMBQs4vJC?>$ZZBBGoBp z4)$p9tpyd_QTU6p-wCV`&B- zbbMH0WCK)1zDtWf(bug6HWTd zTIjxkJe;yX>PAPCzzT==Z7^-kt|r)6gRBCYt{(<|kLiq55x4}Yz`u2_YUlnjUq61D(GhC7^rA|%(Uee`qE@ka?v%N4Yuw#dkS8fkn zmMu#wlt45}?{r=V9;t;GJ6EzWN$-`$>eu&m-0vx9_I*NHRN79o# zLh|BifDP)pZDyZDYVcR%C6rH{QH)nXgg9M1A>OLV5$=cgyVVg|1WoP?b~hHXq3*#O zuG|jX){SM_H%qs3UOrrUv0kg0XUb3QsLCS_9P*n2-5wy*$>jt1{ znX=I}uMaCZI*tx-Fd@FdWpI&pm_@t0wdI*rPiL~Wtc9GM&Im_Y%k7-GMmx7|vyv() z*2Dx1BwVI6D5};nDW4XE%sAjB4SmQ$sC!2ecoO}w_6Hg{0obMp9KNy=?7#I?M6Nn@`8Xo%)#Jjd~l0vE)te@05~kH)5{?+C9;}NYptdyvR)?;DFOy5Pva+C7l1M=VUx*@?kxx#sxgQ<&; zDM$ev4~LXwfgNP|?dlfEdee5jLD0-^=FcH_JR{q}zLq!Z_NY}X_o!A>Gcp;Di#gbq za5-(1P}u!ov8>$M+ihDhe40{0_p(xy4@f3uh+`wPSuH4nL1%X;$|)F6B8Fx%a@GY* zi_*x`Lh)=2K!_F-mTxNVucDhURQXAx4 zn^8ps7WY}MbNB1Ao=*br1!$G8fBox+={O8g9tlJvd3(G&HY!NuRF_psARN~q97M_m zxJlTQKmy8Ij*Si|t9btv{;-wXwDV<#5W*w0vceTt@ZY$tOM)y%_A47MwV^#kDU4(C z6+52f_nas^7dorXt!M(66k6xBNumu?J`CKRsmt~?90iw=M!cPq#OyJNb+?t|RtRs$ zM?52JF*pCBi4$12b4y+puGnnfnkwx8UP5%T`_D+_mcF( zFtVn4beAIIn+z!qJKv-$c)_A)eN|<@Dy69AB zQK0*Vh@~j{19fPv0j*XznK5cHpu3A~TZ%k+4;{RIXv+#cC(`&fxr?aNCr=JvTCU8xp77Ca<9uM-goL?TJgGrw3IRMZg1eBYO zQDO>H=OvPdLCR|5_*VXFE(;zG0Dc312L$#)#wr(M1xw3fE{Mb>C*PATOD-#K6ZQDi ztax+-T!beLr-zR8XlprLWmkM}PI$fPBuJwX3cof&M^&n7698$z-3@CVO1LfFw?cS3 zKITGr+@|c^f2Luk4WIw7&%FB*QN_-8nKpyAd4NV;3&%>T^Z3o zx(??+C!x~MRRu{LR7oYGRtrvPT$I4N+m;o=d{|Q&dChfOfy^AtxmUQ3K+U0Lm3j$n zj~b#qui+I+f;hdLWU)FN4lC4exvSBd-#IE&An?_8rV!(wKtqIAH&%ZjPXLDw1zTL< zv|7fL4Al#ut~kx=;EYV^f*2^Or`ui$*;Ql4p0_HPK(kbEq}F6kCo8iv_*UA2gF1jX z6LUR`Jaggp0IwmykoI%F$W;;`8!g72poDs=yU}#T#jh}sQ{HRmjezrwr57kP4%H5n z(go(O%Z3IrJP;`{1CxIoyMLI)VoK>D8d1X_a9BxxMZ+^QSGjOzDHr6!`wxP{@9#g| z)}|!SWnE=Ub-X{sK-Mt_iNKUyQFqJ1h2uHhL&&}29|Qc z4ZYe)P^B8xA%4Yxt}9n$qXtpsBJ$!D1A}4l_hkx3-ti_SycNRR@ev-N)~D)1<6KJz zPfZqt4ZKh=G!0kgrgmqf31ail?XqPRl+CpOK?ifPUFwHeB|U{Yqq>8TgSYzL(Mfs;3vi5=$vA3B`1GK z6Wt}LZ!=1x^qNm#RX7M91iOF~z>%mzJIa0BgI3t-aym?hOrnUKXW0s~p{NPb_H46C zBI0kOWYDz|Qpd(Jh@K=F9qUr;zUTrB%g#YCYlrCx&XyjT4zx@|iVvJm2r7->Bw`Wl zB+5cTLX>hSzY921RMQfNq1mRs0)l*GYatLzek%fF%H`h(iBMu?QxU^xQJt&6_;mw4 z1sIQ^R*G`zo`c=PhC!eXSLL0m6ds5U($I-PrTidV!u0#QH>dM@xh(JBy$H6Gd$+CS zbbgUf4XUTHclQqs*6jXp80CT5HZ%;jbsnf;Sk7k`l3W}RVuQad$a31)T9u856r2j7lsk8AlrJlo8lN>o?isjR~r8r_1?roDTCMXs~#OK*}pfxjLQC$K$ie#;pLWs<5DySWEUeB@@+Z%%=UKZakDqO?OP$d9Uoy|v`_8nH~+YP?FG_n zOMHZ(h+W}`Qn{{!uz((^EF&=mthmZm2^yJXc2gLG1b`0Wy^0c0es*Mq`3w1mos5vk zq5=SPnJUn!R}-UI3-pVi zNA5XyiVWpF3WQ=P?f7X|gH4fE-Maz(}H~ zop>re6LTzxz3F=*FM6D&(sZy|s8~QNHPx7DQx_viAH9#m2;&oE(W1wh8H(lUyA2Ei zYn)zo3M^WVwdP@n%Ig=Dr=~J;=K_~#%hYB_K^T(2wR)D%lMTEJ&^xurYJ@#N zL9f;O5f`j9fn@8lr2+P-QZ|3~XTJXU_~G>O{O0Xjxvtxy^I{`a(Asc%I;Ai!%kwlH zWe4--;r{+`v^;M*4p?6H00ayOcuGtnSrBvI7#sr2y{(c5+qyu{N=F{*Q!s&~(VZaF z8fgUsu_?|fwyQ56D;&_8r~6vnT32_1qU0DO9}c@5m<0mq}!Eyss`JefZMz;R|&Xf7!lN30Y+IKe=gXnLwhICB%}CwU->IO0vSc_qe~|FP%u!4 ztP1kQw&WG5;54n_*bzR;V5wp;qWYotZ7Wos4NT2xY~{^!-ve&N^vv(BF^yKKY|^5~3pxsEGT!OhOFg5W zhjK^zUICjLyg|$dIVJ+r4LwOT77Fd=MlLQkAHhm7rbV48R;rg&zKdMmveiI;7-rqp zkdjfH4!OP9&ZG~_{KskD!c<}_YQ&nIR+ExT8(`~ioc`kX$Qbqh=$Fb4xj#s{W zTBbqOV2}+7ER>k?So=DoZ|Z=P5bP)4m1X}prCzhV!gY#_DN-W#zcz3k(~jOIR>uJju6I%ahA8ZviV_;fi2*{=B8nsO_Ix8tK55GFJKz0b=h zr@(b4c)GXjzMg<#;CYaok|OlT0tjX8VcHQm?x5jWZ09lDs!|1E@dW`mzk+2qrZi6Z zbhh5_ejMZ{}rkc6jF&utssb% zInx0IJRWVSA*q$27Sw~d<+3Q7hS7yx7EL)ZS{Zje# zflb+Ji(IM{)0Q_uBI7ua5sFQTCNt@%6e38v!Xhkd$+FhZ^PB}?Een+-RVt8tg2Uk; z>;E#Zh6;sY8l_02@gR2q;=g54+w~M7-II&&p}+&}M8Nbi!#)&IOr@f?-Vy11K3~wV zT+4Jg%1%Pc*xm7NoQCM*kcNW%Ub8MUJj3x@Foltm8HPbFvJ}?J1SPpaat#p9Sm4Ed zS+Z1~EO%<%hT$OB0Zq?s@7-4hF$Gw@hCIs^v%qY%U6#$b6{b8c%08;2gvtJ*a{tlG zx*2Xkyc+LlkJ#fD1Qi_8P07La+>kX477y_ybd)+`R(B*i{J~AW%C05bxK9{wi11bj zZ^y@m2;19ihn8BD)yV5uMfn0&194@Q1!BMej>e9O$+SK~<}B-ltjp?U<)J7*+fUX( zssK?v1Sqs;(lW8+7mz;SFe-2m@&H*_E|+S|i+~332_d;7 zC6Ecds7zR1QN!Rx<~+j`+D~}4>E)vO42?WA-=u)CjY;ttg&7T!43vI3y=~1=U)=GH z*jU~+hFGD9B4k62djz(XtxL_5S5E_E<`s+}SLXTRd?MMlOL=a&Urra( zoNSm*m-DlfZ6AiiVcE96LVtHmmgDhG zzWnrjqBA3qnKqpu6JsOJatKt{Oh*;B<*Z^gyal9(UeKpz`R>(NBZI7J{o$HI5QAxu zl1kuP-B7Ktv$vtK5@_``tXO#i|ANQ~eNTL7*r04!1|wLn^2-aJ!eQOT-pxuLMiJuKDI87Qs@*QI9;68|!0d|TJn zuUxNbnpEd0l@N+em@RN!u6E%@IS!-TC!_DeM};?lUKlRnFP44mP=896e)jP0#MG93H`}vWW_?`3`ExKCbatC(~~ZGq*x` zJ3hKYR#(6EJh!r9qf8+Hy^qz9OkKl3kdsy0F`f|gPtpxkfXrzbBu6)ZX@pCA$ag(tu2D zKU$S{OVPu7WSv0`6i5Y(#Y^hXyt-w0)7kb?^y`P2FHP2cqNy zOCid(1K+G&xWA^9}MA~m%Ec6$x)5>O6(CYlO)McAZZtt=0N zWjII>1U$SvK1+qGcvI}Zs?=R@ot)I(jp6xqEC&dF2Ku74+(ANyU1PX^=uG z7;#0OYL!JFleEzM5SC?O*{~!5!uE=0FA!p<6bV5JD~i94$~1Zc+wiXC2!_f)mUa~0aFw)KO1>=Rv=~kDlK`Wcrd)+O${m1` zjzdW`1B#h6->>21jvq%Gw(5A=h{N#R%e=U4<8}g`1F}+Eb_*L)9)|n7!_(8_`7)!V z2d`Ffj$%wj&qilgo)arNj!8|RL4j9Ery%Je;fDqB(clJhK;m<}JLxB#DME~4K!;Qy zqsiDS$epU|mgmb#Up(v{Dki&9Wh)dYf)}M=R39>-~KaY$#t}R(x2LMkgDcpt6}p$&LrLIjP;B z=KpF!v%o#%S|n;`XvimbFlclY`N(wi_L>E(Ltk*AY57BIAX2P%LtcQ#TN2Z)5Z;cD z_1K1|TF$m@f(+>W;Z#^}n0hA*GJJ}?&+JO2G!&JmBisi7hC;E3Sy;hxOrc=3@XOxl z-lB7`B zcMG zu^HU6d1!wRSIc)$@m09p`CHa4Tj6SR!EXlY5$rcSVnST#DFOEqLDWQPrgtu~iVu~s@znm9=QA}Ef zb3vdRH$)4v3*iqFh5^pf;}kABa^D1&fSjYlN(P6FtUF3X*&m9?vLakuyc9F7v$6i-WOzx>j)! zvCz2Z%XvnNl^K5W-3(eh@RoKdm5fR36gW47_X{c$Dy!96g2t>A4Oz9Ruvjjad8Wna z{(_sb9t1Xy<49jNH5-vq8di9f0rH`n${w%0uRta#T967@v>#ogAOx~A&oGUahEe|f z8tuM2rpek3ivPay0wmF{IR?0($Y}t#6?E)CyH$l{#b8zax%b5NKuc?;9$JmO<+V~` zClvI8ScU^6I3{h$GW?Z`&koa15Oy<$K;Go&#WbnHw+JG1L|7#yNcIh2Bc zt&{`p8wKnfXu|*|6dMB&)W}~Do8<3G_vS_!y%oaS@o`ND{wzVI-4G$>3YHBsEkWU^W)wz-bw__S9MT!fqA4n*j$6fg!)Ek~~?2 zB1n{sH z7|^hH)1Z6^otYW(V_W9*5lTT4k=Px^7NJx@Jd|w&m0)1-h8+VIR@j#?I&U9AGS8}R zfPfr*k$Vcl3uaaHsbOCl$_+S`!<8n+(rX$L?8Hl#eZys&F~&{au3~Rg zN%%2Ju0`t@&4X=220dNTRGQ*Zg?RB}LVLD*Dajyp8W+cIPv`TR;h-&>6=?9~OhW1) zlY#&h4A@UFgt1ly@tA}236hV<^a%zH8r+(M7JrX`gU;>FvAnQwQ1{%9fv8IY4#wmj z8y8ThKs>Iz$yAaKhm;PKCEgI>tq|Uhk83)({>tNL|M-8&pMD;()tA*Z65fTKlVVlE zt`Q2Hniz=uYTsm^T_H2~2yS4EtgJmGbja#cR9N1XW}~S~Xt+hP+nu`lYe!L$pWUP6 z)orxE?E|hf*YrWg$+2IG$`Tk`0b&JBY5;veTwJ9JvRJZrTGeKf>2bznWEZrtw0*nIg4C1TpZPL#aErD!u8oJ6kWsq@$}0`zH;q%eA~)^;VrOTUxMzNa zqh*8sB5Y=)n6-kPMpnxmOWJUE=I;I>3}vCwX|4j)5?j4sz0T_bs;!*EvW9WMP9?8A z1K1%L&hDT%Y#m_}m{vAy62M$WzeCLf>7t9r9&L5p3Y{H-PK-Sd=|>#>QHufx2=or* z{wiO&us48FVPK+In)0|@!rE=SEiJb~cso9(eEAwjE$W+)h< zprwW;r+Q}YNexIP2bjY!ggr&1+BI!;ViB~S^1i}Z5PO@Bqegb#8uQ$>2S|D(*S#1u z)?ELSX}z6D30)~_!<%aAX{SYTgSKdSTwS+;P^CuXZIl1Q{0W==Xu*MKLmC+SC*=WX zMv5FKXG!fM(ZOvF9?;f}!vy99%Qd70C=3|{(Iyycf|g**<8X*Rp=P(-Im}Xz)6Nh- zmLID=ZOpsJ>WaK4glrH8(?|?qx@fnm!|HBsig}Y~9c@(!8s^WPO7%~mLT02Jni!J~ z+&ly7<>i}pciDBNkExsv(jlfa?yld?-Q6AK_2;@yhw*Z0>$aXRmrp)(Yy)6wOj~w}5)Ju8m`QJ_(^Bc-F!Ov_?%L>7z)7E(*AL{Dm-kD&` zaATBH+#FACYO9tJP4v5VRWomf zEJ5OJHQX_3V~>Hec~g6H*XTk(Lm4U}U=xP%R%VDJ0_;8V96S$;YbY!H^V3;QAq{Dn zIR!&V0|+*GO^z!(88jc6}^|`RD+w;qdY-Lv1#Q1SMg12hQG{CL9>G;+| zkmI^~hq47N52}xxi&kx{!+{MFM_Aj`T=@fmgI-`qd6*j07M1z}%?w?Zs0Br=WYa;j zqw|sj!RWK&<&WR|_|d;Kb3=r;LU=nqlB0a<>HY8ikLG9p^a0S%LLgxW(#J6hEHI#I z^j&0WL)e0>AX;WpF&NKqj%YP@&l1K_R;)pKzH2xyv)P~#M6J>I2&WZ_E@Pl)uvKVx z1v-d+yFU!)QI#kO=|8gICa?K=s+4c+;(8@;va^awU;~9@mwDx$p-?wQ2vmS}hqtg-CEc*I z=LetC&>0%9VtY$DT4fitsgI0R6BH`w*@?zk?ej`}GHN!|u!ii$UU$ZB-b5Z4`2iRdFr+rntpGp^qQ<-XNdOr3On6voQr8wFA*0Pe z_LV#Vl|jctE$pJpGq#}7y}`{Jnfw`kDRO z;VG+D=fBp|Eoo2h!X775=g)R3lQW&_BS8M*$QGXm9u4$0VCG}giGeqeFKWAAwuZhl z-J}2T`1GCo&z4*C#H|qCj*kNivU79#PyY1%-}(J9UPfdU$oe={!6*#h#_B7J9R@vJ z4UsXOLf#GFq0?8eq|bqtvdamy1S&z1R{osMbhV{GolCYh1&w+(Hf?oE1*z3; z+op^RqgnL&%BITK$vPxY0nR7&CmW>l-KOC<>?Kk!0plxX>@@gO(?kuFK)^K<833q2 zSHCTExy_p%%r)PQLd0tv-zP7tG&6mZ1sw42xlM5rK zGQbeLtOmkJKW|G9R-TWoZQ~fpAEk)W6PbE99Y%S_bzOilDGZ&ag9+GRK>|^-+kx^< zSdB>vp};&rsSv!soaJ}352O6R{loEK9xe-#stUs^1j&tckf)#`?4E?ER)aQ1Ne?yv z!xZxvP+kGqobD5}%dGCUyzK5u(5273BM6uCVaU|(p3Zv!DGI8iw~bX$_Dw{GXaRUA zH~jl_`E2-*ZMm&6w?cS3K8~aP(&gn_f7r}fcDL-a$?hJ_yL6b~DoGblG{UG03|OQp z9699COmU-433^K*Rvm9)-F2)$Qa~bH`pKfaa!MMRq?ld7?aY`f7lhKX&iNwNPs{mT z)1Y70HrSmYcbwua`Dc-J9MB5|hlGo=EDMN^F+qyGt-CLp5_P~UsRS^UsfS(l^20E& zs=0ZYnL=Rc#90wQ#S?)MXkMpjn&(3B0soY{!!gro6MTBil$1QFl0Odhtad3u2X$q2 zdtj?<5%||x)AclvkeEi3hCEDIrR!LmV3- z1T!b7RxRA*7Gu1(E^=)ITS^trwIA<}!)V|WUNYcshg;jaZin%ZH)K@Fb3`K>BumjA z54(>B_c(`ih*ESv6KNAK9|lullT&b0ipsMsOrC;0j|u^IpF8D&^3v-LPj(x&!P|h7 zYs?BbS7rzcNImkX$6<`eX+F-6;e4~(z7@jT@l%fl<%qrh2OnPkyMNJs;X4N#ow{RU z84H!5L;g~$-L@~=yg)@DYh>pvFr?&DY6upm2mz(2F^vFX3E1+hvvTJ$%pEnOY#M{- zp$>%n8U3$Li^?t6#b`?q3ljprDLsz0Df`*ue*E5(c!UDkd{uPvio#jV9nVA#29AXRu3O8<@QA80tQ?e zhZI(lTeG8$fV#)oUEM){w$>K`YUE-v0hmb&j4bF??FB5aHea5whyp}Moxgk{6eLfu zs238#4>aqj&{rbuwM8sj6F3PV^vbf1md=@=`hJpI1Q`W<$f(qJ<0u6r*+G!nwqn}H zz^OoTN@gTOCKz>~brOHCYXM%&lr@C7LNzF>KMZ1=N)@S8pT@+25P8HqrA<4ryy>VnWX-m;fe<)llOz{9O`&@ zo;RtIsBUm_L&IKPTRwrOu&2YZ4<8GIV^mxzsH-E*HV&aNsxZQEC$OaPgKBgnJJS$B zHZ`}!5r?SFeC^)L15ep2M3|_%7;lcJPwf|PE}O=*+>Tozyd6KaRj}XH{7aAPKm7Lc zYk#=@d*2)EYI-0uzB5-XV91s;{khPBBOhQd8wUQ))it-)h17hkVrUD(pwX&#XlE>H zFWSy@1Stdvdkp|Vq&ua`kevulyiO3UCNo%$B>V0mgZ^S?!YamZFb$=>8wCq$-l0lI zQZWnl(^!9#9jj%!A)C5|S$P=TIR*&xGXVl-n1NB?SU9n>^W{uQwdHo7a(kjfl&>%# zv6zFUrf$@RYD=dx(LIcb5^Gr@rA+b!u+Spc0OAZXXmv8`)wCNK5Cq3Or2?%zlNjM& zf~scNadg@cWjuOqURTm)~B#vlc0 z9FOz)(sG;UCtexk`N_3{s4n=eRQWCmoe&pbXxxzWYm~vGb6{+tNCfiWoQyEB<}fnJ z15n2)B0t#5J4pSDvYk;&yj^O@&5G;|e2ntp+qfv_r z7a1cAeA(IeFx0EaKZATm+-TrYyk-JlbiT7M<63RKtu(hncsqVtkJR%oo}PZ^j~>7E zFZ)+Ni{p&Hysq!lyMm^WXV5#g)f+h+t|=W-z8bNfdw3($3UZjh$`g19nE{KWX3+wp z5ajGg4wjFe7MBY0z$=HQG?GCfQ!O(r-mY{|1wgNZ zA44U(VF7bmI(|ad441E%tTqoRwJ0!phN;L#*+xncHf9Krn%M5-Pa>lisSE%Y3WAkF z2wxajgI<&9y`BPg;u65IsT-otiooMo>w@re@pCFnK-I=S-)b z=-JXohQe^{TzY?vquPH4#=ALf*n^WC~fZOH3m>-O^eg7`ndux%fdI@O`J>ZxNP zftBtGq{u=x+UhmM8JiaRbYtR}z_j6$DW8I!kYPx&OQPoibSW6~a^OlGJ;%%P=nHn& zKxve8-0_lZ3Fao8xD~?N@l$&AujSJ}`Syq3`j;=?_!HyLzAwQxuW>V0MQ|i?I`YFU zlIV5{0Vg?9r7#Rcl)0&8Lo?9&Aq|Wwbxl+F>@|=`M_TF_vk%XV{@DT58P&{zxJz2FE1TUqJxT^Sg}a@=)H#1UQh!x(^9n(ME2DQx{JB(b!GtNJSlH3vpSRlstu z;%`ul6j?Lgm%4LW^K=k&jb-u=B@@^mD8rPmN@~8--Aae;3YxL%A&{I#Q2N--5q?b60Bq^piJRhGY<@EdPKaE4zm=7pfBx zBnj#)j9W;{q%?x~3-P^2@83OL=JWdxFVCk%!1gpvf(r|eM9+p(Vhn}7BB@BL46e6(#FWQ}eG9LGTVO0Ot=@Xs{V0n#PY zXh)L^E!dEN5fFP-8p43dG9Z8rs`17l87d(sVOCjm^lYY%Y!A^z8xaH#RfUsVh`4ha zO9B;MT{7uM#v;R=zLF`ga)Q_YzsIm^18|q%lZ#*Ss{B>?C#(X8dw+hcty)FPEjNJxx_3ZWMP5hjb96q;iau z9Fhstw{?vv23<)F5RMnqIzl!X`Tn2tFzek8D8J6@YTHmc8?7#^Cwybi) z(6gPT@IqgBnjb$rKc6r1j388m(MuQyMzgF9j}S+N+^N*2&oI9NEVyQGtR_MmDpwh# z#bQo5eWR{CcOrqS=s`vC|%x^3Dupy7(M-MOG9e#ZCl(`kc+ws$M z99#Y7%leD&FTeiB^WXd5dwProJK{vU7?L>P4}!=Ac&a&~FU`&)h@h-kyKZWPCRm}X z!6jZHa1lnUO#QH#QG{w}Dt%;jw=HvZP(zVLAfM6hn$_G?otZgm70C*LS`ZBOy_xA= zEgKsV2HUDkorSQvsSsWO2i%gSbiz$Ww)RNOk-fYC7YGMQ^~0*|_E%8Ygx;64QP2r> ziVf?EBsP39oWcS;d)A|VTDqm%;ZX_&p}_5xKn zRDEc!UfZ)?i&=%8n4I{VHxF8tK74pWXVJ8E&3SDZez2g@sCl-)Af@4uI|*c@+#NO0 z%LR>A_LFf28i}mp7Wte#$SpUo)u!)uMf*M0hizV{S1fpUpgW{DjtHgS%IDwv{`0)n z?qOCp4lM8RM9Yk-kkT6mJeIO!ldS?Cr79af)%7DYLSE=FjvbmGTy`zkmZ8TQy@aiT z7X<9=-fuQ)YO5acc*0bPqNd$);nWt@MJcd9xO@4-Prm;gF7<{8Z-wx7{Iv8!zfzXJ z|EHJV`9IE|{^=5b972l_I#cOv7hh=&Jo| zyFtr!eOK8yD+H~N!41W$VX-P|;?=23kYAUj6hLIyB_MY)6^&c@<#nXAJ% zHX^@-?TAX7Z6+5Zs>4%c-!(YW)T~jO@Q;;BSVDMzSbs1*&UU*|MsJ1icKlSdL+j6< zPrvs^&wu}aZ4Y1e`s|i9Ipo4|{-mMe7E>E6kONGtt5N9zOCvB-w2=&QtWxR}QwU15 z&sONfNLGcCRP4@I(e1L%2<<)~1Yv_BHl*lLXV%$*l^0P&NPa(Z70BuEoc1-lrXh!;psawruNGA(^l%M`bv*V3o3>Z`YBrC-_E+eGw#l zyt~tHR04oBj8GQB2gmQ8pL7XFR_&g8folTmO*AKWi|QRQvd2d7+FsqjsXbV=JdEQg zpHZ+2qZSovUxj}ZIBL$7!f1wR7$zJ0g@*Czf9eIydT{*ae>H7$g zl{mmSRNV&JI1EyL<&*?#;>Tmq@vM&_&x>p*4tIxjlg$BUa)JoYmj!%RQ|ASFNYgk4 zCiyjoct2(N-i>#pG*$h)^6haPB*bS0-?QODG)~KOE zmy+>!3|L|o687>akszfoRe)FekLRcJ(v0+)pt74|7>n8D1X<{BA|?0#G@#hnwsa`m3iFF^p*2tuO)}sT2@i8N^YQ7&jsWY zH^8eFn<_xz!{MOfldSqmw!atnnQA5Sg7(y%XO_yl=9kMX-#OmBK}zu-oH3FsvCPumedUMHP(GG}1ON3)Iw@5XLH@v?svoi$72>cyr00jrDK0mr%&idKj{m>gp zN|cR#O5JIf%(QFj9x3b~`?;>hUQKPl2%FkDFSzeI8)npM~<{ACZ0W%^cs^|uQ|AF2dByn=pG|dH>2g`J z)WYd-7skV!%Xz7nWj(LUu-Mh^bh`^e)aQaE}<3qn@xgbU@8C!`+aXYHl{%E?|FyZ+bRvB zyL}_C#@T(3SC#dYN(;J*GGYnT751Q&A$|MjxG))Qz<==n^ zu&RMw9OM%M8pctI11b%CUv5%H+XJhNKHa}IYW|Z?(gr3l94h2WZyDqLUan{g|QWE zSoipNl2YfXcrZHoZOUSGcC<9jL0y{;M=58yY&di5YOpnsv(C0it&^hY>Hmoyqrof7 z|2ESu^8yMI5|RVm+`8O|dBvbo-e5Z3r*RZ$xz+yo4A!W8x3Osne^G*Wd7W{bbTg91 zfn=^G&)IR9WGmS9){!M0NOqD^EthOeFldzz4wWB2ya*6{^LBcEI+HV{^d%D*j7+;%5Ufa4IY{+J z-=R9$m7JemrsFiaDEIetp82fd28S3%7f9>HBY+zLS)@but`#)7=yY&}xW#??m9JH~ zJL`5nUq1ivAy>QADut8j9RRZOF~ShWbf8&XOQ5^J{=rdj0)0K$LQJ@#WU#`UTQ){9 z8h1D{dxZArcmh!>X^>V6)U=*ig)IEC&AN6>9JhkI3I-z3X}G*;@22NlA-omB+woU7 z1vIJGUs|62`#-q+>bIMF?(Ku?t8H6f7RZ|8U@XXxXu(-H8j}FfRr_S=D$LlxP8~2w zjAWdctLKOcCt7>0H}IMP?GRZ;wq135W)Gj^O3@t=6qE0a{-)=z>ogCFGUL^oTUn$^ zY6Nkz+ua<5?K))eTJlU zYscAccvV(vMW5HQm&v0AW+sCsBVZ#VYW9^Q!#SvKP*DEKYl-wo!|1TA8`JL~GT}~x zO>G+WXubq*RwmJ(l0=H-X;TV0K2#zcBy#K1$6=DsGGFH7-I3Is8j;{m13C5^hcVLY z0CDzA>#f*ZtXVm_XgJ0oRTvXC$^yYkUE%(T=LRa9K`y|uZE=VdnGImcZg5D0rc(;G zu5$7Wq&S`h01D`X$6voJ5N`UB9{t@ zRG`t84S=z7^G5m=BE0%|NYi0r$Yn>|pjW>FDC;{K#VE;mnyaDQr$E<2!*?=o9gO18 zerS;0(r{J-EicFNqqi?#jE^^SwOb*)9edXjse|4+JcgA$)Uf}^OU%<=OlC*cDmibMcZghD((Eo?ggfjCqO%GtpAQ} zFrD&vG`w(T`|3?TydtNaGP<|mSS%ZjqW8X`bxNGM#_eLIg^rCfJmf;8!EqJU*&$<+ zcFKOyZ8GviPbX^UfNkw1>ZcrIu8g9_$k)xIBx0rW*20t&HGyQ^avOMX?t2`Wm+ zO?#r6Os+mGng3067RU)gaRg@44Bj0TtE|tOJt7#bKY=DtA_Rdx{0jZOuGr36#(L~F zF;_!1t5BVgu`3otJ|Ozi+WRehDLVNy7I4_ZMX>y<`WPTtBB1STnGcnvOn?B4hCox{ z1nX|otUjqU{6aaOPc%<~nWq-xg6IrHGlpo6_fjwOJYNuY>&HG3EM{gh(_sv}n^NQ! z%g2oHX9pcgZk(zu29Q&Nfby-ZFV9c%feiVp^2TWxFjxc%%<3J>`w~x=i=5qfJo?;E z&+E&1KD}ICo=z2r{{`k~NpPlb3=6cNOsGJ!n4HT?J;`+)yq(4b@E>(g zB9|}nddpU0f;$YhP}xU-APac6bF6D5lpIppglL}HlIj=wS?()muai4rh9=tlTUIG>0 z1+XYNR;A4ax@H3k!uv(7T6D1{8Dgi3?@2S-Zrej>oWiZ(RYd5nu9J=-KFpkTup^e8 zG`TX>D?5fP!KT@Lubw89gVFNR)dtL`E}Kg1-+O*=;^y0sGb!s>H?QW=PPrJbG{{c3 zYnIYVZ^&w-Fo1dE73RtBE7OEZXCStfDtiznWW_{-3wX$p5h(8)X!0f~Zdo^sh*DBX z0}3R{MI_~f`v$At*s@U5YuoIXv{ANLlmXfHlwgeWB5YRVWY=w5(V^`@oyF#^@rHQe^JJ+B8Oc|8RiC8`POn6@U>LdxVN2mq>o; zv@9_W6dk}@&I$na7|LGe`IGSthk4+IouEYSheJ739=M=hKwpFwE-` zu-S^HB>{*DdjeXkMf%%enhQ-Keb*V6MI$9TEzl~^nX9#tWi!OrlpgQq?;YPQcBSXc zjVyC3gty}_e2lGqb!*=^mB0C+{M<|Z>e;?s&9s*Bd6~Yr-8= z+v~IpGZiWg4PCqV4aT!!#~u{Ez?P*n#`4j&iiNZ9mMD&zS(Vb;NwW0#LnLI{DV%^Z$*vfN*m3rA+?zg7*( zHQF||IagTL(0!6iT4OXv%N1T{=%dC#iJ_S&Pus}I>_xqXSUV=FReZvDmF_lxeQG+2 zN>8q#y6g~py-r%uUv#RkAhm|9SLN?F@@i?ZU3-RJ-)XcJ`j(ZBcR`g0a^>L|uqo*j z`nbxeAV#a~dq;U}RBeH7DfBAiG!D~nwH&ho-Duus=XsHf8=+PdfbNgSyOVdCm*sFco?d29m_o-gaW$LFV~)9JFDFN-`}Qp%)ChBVIeyzi27Me|tDCo`P6k-{jC zpd2kw^@VWqK+3zcEDu>6Mu2JG=?(KwNr8$*h*3EDT^H;!~7)ak8JhXd>; zn~Qch^g~}FrL4Cqcq@dr<1cbB7pZ>Zvi;)c>)-lL`L!R|pF8&N0@Y zm>s#2G+9DM-gLAp#2HsG7kGR&xoy4dTO|) zHdy2(cIdDO0+qNTE1o8;=Tf6hA*8xncUW0j6S_|VC!(w^4PznpFMC5D5hybPb~RXs z5IBL&0g?@B6yT!FRrs9$Xm(CnmN zjMUeGngDggbRdP&6NSmcg~(KGZn*8}v^5B7(aP8k+#5*gOkeYAkqPg@FjS*E90UYZH1ILIBm zo9=cJtnKH^MLx{7%oxDQ+3pA*wy>qz^HSbF&7Xboc%B#Bt~kQrH6%G@c@!9mXr^&Q zeQHJps+d?p0)ckrx~@YED3t>0$21;sNteq!jX=!PG}744_{4A&1T?A5UxuGNv9sGP z>I}%qJ5gE5*Hk#-?@6>QgyqmmFy>YTZ-wx7`~{EL%ddQR_fP-l%fJ6`jlG1S$_88( z+R`@Jlw%IXCYUZH9EqFMk-bJPZG&uL|?LUt9=otBO16QSX$vdrBM$i%88~4w2*c4v}{wNxh&Aic;wzZEX(X*gq??B zEW6F7my#gM5KWCyR;2z2ic+&|b&t`!imUFz>Q^G0cT6xe4teFhVQj+{aVPL;JfGbY zlr;N-!#|BpmwzL*m0sf(-d)vPE1J3|yQ@tnNIL*?%1A*2Pv@OTPt0)DVQ4U@FMXHs z!MWOARTL;xNUH)_l-mdctq3hfvEti++Ges2KAn#1r0_=URbEB+SKWPKNkz*}6)Dtp z8!8&F5PwgwZ@il@MqyJl=t2(P7N-vb6LF~%kaDvLR1z?>ZL=Uet1(-latLgZ{Gi1e z=Md3&bYPD_3JEriR1{X(P>1obZkLyr zQyP%(d_FDHcz_okvJ!2C&SBcBdwhC+Ij@f|^W)QL%@8b9xsuo%bO<_q!aNdBU;+UL zU3c6RB>l4aE*pX^1CpLE=V6R*-rOHysOC@>O@rK!XN6I;ue4c1l%VV6j;g@{Ia;qm zB9gsG3%iaDv1<}Aie7fy-o%%?j9sa{EoZkvcsu?=M<46@8{aE`_fJCHWEsIFgZdox3}ZNBrf)6=BGP6dw7PTkAk>d8@PX?vvbMq_A< zl)M)(rkR2i|AVWfR97Zkeg!#K%i0=_(3d3t*O;b-sPe|Q0C^<2a1d03$h%HbT3*Kgb2#qrOzuWv-R%i$bu$3megn!xT zpJHfX#`cp2HQ0z2UEz`o<~gk$q4I?)fUeXUPSiC*NGD19_noXzqq;PdG6)B+Fh*5; zoQE+{p_5j}Y*`X5_LLyT-ry_&8G);&;}Iqzc{?6uch3-4asmes{{t;fqGmIwNvA9` zgH7l2g^NBqqEbG$4dTDPLkZ@VE71||j%pObelF39)D~pv!G0{--Hn=VW98MM8?(9~ zHAk0nc){$N30cQUqH)To?93D*R_>Z`#EYm!61Y}g18r3>Rl%VTfiXi4c^8e2nh~WX zP18Y&Yu+FVthOjWH3{k1W?qqc!~m;@iIxSai9nxtrIAmFU_8z?h1Fse@MNa!_J zzZhNrA79FCF4>Xu zYC#glryPg0t;@R1Qt%Fg+>`NeKqBGcAl0*Da2v62lON4xi`=0=(1H!8I(+k5p#x57 zM1@`i(j2=>ZOtA)q9Kny3P_dB#6y2SomRKq7PMO-ydD4VOW-%3)?fYp_80R7x@qsRzvj!M4(_lNT|3J8SYsI)&yE?FY>lM%WZRP>=@i>Z2}~qjc6RDYR>3u* zQL8fei2`Ni(hjO?9yC9qgk6lmG+P$Jdd)Rbdu^R^c5~HTydLC#Ru191x~IarW*C&) zS(yohnHLGH@OA;mQ@wGh2gB7?)t#k>h@-$!o2df!1}vYxtnerES`5hF!THC*%Ze)i zjjWl~qRU6l3wZpA(Txl%B5zkV9~h20k3hj8!f$Ou#O<~fO<;i6is}VP{doO!W~YpD z{7OE^M#mcYa7ZSUQr(gB$JS+41?Nsy@@`_1y}T(gbYItn$&pP{gxCmFRE1s6G!62N zW!a>nX?lm_9t8V^C_55)9)QK+*y6+UTOor9jW?8Lp&wM5TzfaXPYV?WAbRmQ2p~v@Bn~e|kC3(6M-k0@(|Vp4*$01{gJP zGeFyPw1$HNX(!MEQ2tQpOCIq&Iu=KGjLH+>f6u-xXae;toQ%m3l$kN@>Q z&wuwj?y${ zNn^A;;4r*pf*+Esv;%#|Y=}}1(W6cw90 z448@**WAwIMNFfoH%nk7k>=b7`U+*utk5(P+0AV3bbh%zj#33L?_XjZd+9l^a<%2k zfNY#x-tzf;R&MR_;ZE-0hsO_NIv$S?xtMv$QeWj<1S~$C=XdX)E)dK^d1 zQq-mXX~?9*fDkllr*lEw3AWwFb{DXkh9Lu5=nxu7A$)mb{@tgae-WR$Z8z=2Us@qt z%+1RDuNxM;YvngTJO9&vR(|D=oS&U6KeAS;Z4=FrcYMPIn}iPn1uBKHx?z=~8_P~$ zvjwESP>WJSz?X|Yb>)|E(+f`*^5d|j)y^C1m!hb?k08~C`)UnJj}pQuN3^*I$x;KOnuOu!|N-*6y*D9fLX~hld>N4)tVZgu(K+aN7h&kA>x-+URLdHTB^ezI$3pk zA0sOc{2oRKqbVyu%90ABQ%|EN${C)``*0CO-n0FyVeL+Ja>SFZ2+|=y&{V06vg)ly z4HFu&7AkHAF?U`!k5Hk5+=HBFE&UEU@qyF90d8lFT%j<8sD^A<7v<_y5DppZBcCi` zLIo!Z(sZ|@X`CiBvh!RK#-tuQUOux_I>73%AKF7_VTSS}!V%kEAb5wE zQm&6w9r;dKwog8JlQ+A*T-I$J4|nX5re?ujS(Zgk8xJaSi{jV5{;;~zRU#ync&xw1MIjFiE@5GhZbsi^@^bDsk2utP~MhOH`zA|*glS9 z#N&tJEoF1ftASk_9A!8=MnhdnVnmCzDCoQ9WpGHnM`;%3oM4B6=g;GrF`90r{N}@S zzHi?@KE5~&s2u z%nma}sf)|Jb#+neJ8EP{)@uMES07cuIR#7%Y@%>)_XPmgAy8TeLLi!=QFRw`*%2;C zU(kLn27=uj6MHIEhP=q0RiEu;rkCr*C*;i(Q+DRlsw<5O{|7?k+OD?a-BSS2w5w^M zAY`i@v>oUQAI?83A*(gC&C5hy7HltZsMy*Z# zbx>+vhr&(grZ6p6xs9Qc1^XUQf_LnKL1;jT5@RtM`?_{daZ(@c2t5Hqec4@??_mNs zj@trV#PR+(OHm~BB#bvvfLIvj`C>^Se)H*D?#N)!ogsS&tH40(hQo0y%e>Z<+RMEB z@W<~iYgvk1EO%a((&2w5Xii`^ZTmp+#hx)}^P&g3#G$%F3dUfHhmI&cqXtg3tOdwiTk zijrl@A~hL7r-Qg11U(=?5Cq-m|J>a`12MWCC!QqQ5-Ezq8}Cq6=bU}^(BE3$t|Fbd z8?_h-dEtimwWNx$Y-HhP#!Ay&NVx)Kjb9yigN zrSc7_Gs!5K*~eCES^4t-q0mf=a0Pl$#|^X!Tg5-2@z{ni>BNs?yk@r_YkNi=^Ekj_Nelv;+Xg%6i@eJA}>*MtR5KZL*_dq+(S_^$A!vvkun zXcHPmiZWR^RTZb70XmPA$#bXCAjfqb$r17%og>mD!0-_6RP3IRStBzrToU8T2si%87-m10>5`X8j_KF5K!dpO*%Al~0v4CqKK} z?UtsIrgqqD;j`f_>B5~dC4}Cw(bHKi}>4LO%MAH^{JNI&O zZyye~cZd7qe78SLE7F?e$T)@j{a(6o_Rs`zl>)$RsnZK(4jN>-GW4A*Fq4v_mRhAB z=mX8O5-~i8J4x5u4W{`m&RmLN1M4NveX1^@YmG(?Ud4-oPC-^`-AVJ>0rQFnlCD?l z*57aAp$qu}8NDRJ%j@rw2+NZ&|4*Mh{OkX-{O-q%n=x)Go(~xzAx5WgM)`Au6naR3 z_`147DT|ings27gKS*{7itm(CBGQuH*5=A;_YSxt?DxX{aDzzTs;d_^;?_HY9qxPYz>isA7%?! z_9mTyOKq{WHY|&qA)78|0Mwe|mt`Hc*ZTe;%*Uf7!v5yw=Kg-K^#(@1P~uMVpLY-Y z=g(dqr+Apx{TvRH| zqvxbu4xsDX>#ObVYBO|wfd@~VLPq&oE6AlI!R_P+q8Q8$*duS~p8C4cL<{L7E_|K^XxsnPFWh zw^fZClr<=ExxrA;oVoDWh~THZj$w6ttC}kELwUCycC_VIY6wYmd5wr=JUtSWe5Xr7cymy$J=InEGm&kjxJEU3#%qh!8F zt{-9USj$P1!aS)`rR0yJLqQYGWtr|%Gecvh5~u-e62vu62f(PwBy$SK4^k1ELegCX z?+|XesfFcJ&&46+3!GOgLg==1J%)Q8BH3hQw$cu2XI|LlTPl*tJ3<*|MF(8R%|!P= z@RM^wGTd7nC~-5nRtOMPV71{+yFy=W>!sPVn9l-dWGZ<^@1#-KYxCIoYJ(G&z?PBV>uibsm^@g%+rLg2u`2Jy=Lst_p7AbB%RzS$JsEQ zd&;L-rmPmGPN9G!(GFd+F0&j%*LPRD;nAaOC@7>=Qs1~6up?v(%%jNcl#`Zwq!t-v z(97i}o5WR_ih^5+qD!b(AV|}-$Blm&!t6q^mqd6;gqPRfIt!Jj^D4z3-^K60T)+Q| z=?6bI?>_4=&v78isa~Nq2+Q+SWbEc?4R0BGJ*WOBs>Y`M6Q?G&|<2& z7H&Uvb*b9O4QcflidDwmSKL9*fu|WW*80~$Dzi{xzZTJ<) z+0fpcveQl0-4GSGveNl^%2db(2!7i2WzyOq;@!dMe4r(4rZ5fOyJ=SrOQpl=0JU}c z^*Z{tw}8&BfyD*TzKPIaTWu5MeZdM>=AwrFn(4WhO}YeB=a5M6s_LUbl0JAAYF44= z2MRt(*^*hMT{#K%iRYI5RGb-pNTIigJ#~SKTT;|WYUym04!F7=H4E^i+ug43(}T2i z>w;NEiJMKI>ANdu`S$jnSZ0(~+i@47w0dk{BkGmgyN7wk5e%E{5+Vqo)aTQ0%996s z5qZ;1FK5iIa~RsrH7-dGl20n38wdHRzHi2%x!!L3p^4xb8EN}AW6x)xY&PT7cGC@@ zb3_slIoQXSq-_)R%4z3FQ$nYl%<3vq*j-Yfu-bJ>d)hTGHph>Lr?XuyV9`q=yuAKj za50b9@WYpP|Mve{fA~>({L+}C1W22t1!{6ul8xlIZo!W<4XkuUEk+ijh+0BDL+RLxb6j}hb}8YQT?6W`G| zHU0r_y)>;;x`M~-&Fa!pj>b~n3#06_&M8jLN$iqWigdn{rw6)I1n#WTJoRKomAOgD z)2cU}O?~xtQoAjq$xUHSQGFnEoa*ZNhmapqei8t)eS`X6ohf2s%ZdFxw^Uz zFyP3jYzkmjF$RnPfbfd0!BSZ&FleaCmy!Y^tPlIyWOsMBfA!+FX$Sd}v?k6!CHKM0 zLi)jJn&oWDHArzChY=_(%UMWTurEZTiu^~242rF;?IiKZ^&Q%cTq+hG06R-TGD=e1 z4(LZAA1}FoLWJ#|D(o5sDoX*NXd@;=-N#d?sA?L}E1Dv7r7T_i>)pdIhA)CyFT{yU zBD}o*mRH&2<;TzN{_Ve;Km3`$zV$9yoxDK`kr9}r39f*kC{kcH2=OzC_sCU*#~@5A z^-brdx-I0WRQ`WIbjRa->6z!{cswd+U*~cmtfW5c zksG@3&>=`-YL8F4+wc3+F88%&@w{V>`8?L2PK0np8pO)C-<-A(RJVyv z`7Q;7HVCCCr-h}@3hk9u`IN8?A`+P%Q<8K&s~xrSwP!6;wdv@(M)hU%dI3x{hfIPE zuLv3^IkPCoiZpZhB1G1<`h6uM_L!QkH#h_Cc$gP?O-Bdre=Aqtso59D2?4Bo4r)Vs z*iWxsylR_Xa$VOq5F4^d`eeCr`-W+@>*fAmQXmi2#tc9A3v0l7@94ILGA-+jWykVye~HxETUibzo9 zi3m3-VdZ|B+w^KUE*|ENmn?WmgqPQUe%(rXbH4wBkLDl$g&kkwktFCvQjbbjWUGD* zL@?HI`I90AI&GHHqZ z1Oy_1XgoWr6?5tZ$)xjb;sU3kVFD+Wb$`Njl~b6$lB_wHLrlEd^$-UIihzx58*4ZiF1?c z&OjKC{Tf(q)jAd(A+l;x=^8X=a!#QE25x)?lF_-oL91q~vMWriuricGLPW$)h`2#m z`GAGlfWB!l9khkBMis!}Cops>K(%T437PX?PYw(YfkXmrD>;lUm86o%*2HqR??w?lddZ3p|Ze&@yX;m`AUK-VFGjM*V6?i+GyG*QBE z%#)x8s)7URJ$XE$n4-DXt4bLrZ^p`#yoQzDFP3VYRvL+`z*#}9wxMD@ zR1_xb>Qj|v08DKo2lA{{+w0g~jev;ULUD`qvX0!*__+{;Gb!GD3@_ z9`jyb*NrNYS4^kF0BVVmZUFHY6BGJkhs+8>Dl3N&p%PAM2g7v2eIZFEl0AX$k=)a@ zn0*k_Y*y&l>2S64T_$|_e%^ybSX${CpS`-LE~_NS=m{xRmYV`w z_?}+Pq>EB$^HQqY61qR`=A|-&_eI8r8LMJIj3G*t$7G(&!y94u+}}xNzchuHM0k09 z4I?Abb>8Iw&OFWPg`=F`6a*!xFEY-;iG(dqGv{co?o(xbC*o(sy2DU`4f*{6m zb2uL1QUtnM>S&s(lSAgrEcGsZ?a;Qt6bX8mprAp@tC)tar-)}NG^VW(1tE>6aSsJJ zbuP*ts3^9E@|AS{Lq~}ZQ>#4bJ&aR;E3h)O{4MLWPz4Q*Z&=sr{cEu)s)A>f?3S<@ zV(b`#Pyc{iktDdm>y@3FNoq%-J)e^OsTr44;6}YQ0Bp&+`~+pKBT!SZpAk?YkiD1( zoaD_&AsDR9YHM3rf5?|)6dKjb!LAOyBQ&%~U8As>9Cfc=LhKa+iwcNwOJv<$Uj?%F zL$kl+#G*;Owibqa)#aHrdSUFZcsnZr|$hb)JC^I4jP7Brh zSkcMCa>|2%PxV#k5{WFi;-duasd4>EXRH8+^`*w(@af<;niWXZO`HAVllUFD0`jc3=Sf+5IZ?85h*3Gzgro%LJJYaQA#`wc=4L~hDPzo{0 zkqg8`h}pVr;Q%J5vY+$!UR=HZ`Wqh)FBW@I+`c5j%j@gy;5LUp{CxexU;0OhhTYT(AzbZA&y9FHslNMBO6IUGa^UiIYf|so zN+#pnEY}_jq%&LnE$U#9<5T8Vngk(0W2QwNws_)1D{$YOtps6sKoJ%~7S#WV2!2CY zW>)Dm%<;gOq&yozq#ezcIvS~CzmtGx0JVz#4BOrDgPmF_2|)~hy7lXyK)!R&Xv6y zays+3AGdP69nvI+kmyc0*RE19*kZ;OK;MpSc;S>4(&q9@yuyI2I$I}mJk4mcZB;He zCoNf=KM%ww+wLaW3ck74Y1CfBva zLNVyHfZoWK19mgCgZ%ntyV;Iif>Q5VtTgnwAG+)7Z8x-#U^Vc7mIDky`twDX-SX#h z(&uGCddv`jrI$#_l|Z3I1h7#Bh89o>NJ~x&n5+roebe?MFjLt0=I)r^ezEz{^Kbq8 zn@{)s6fU{&k_a!auM*)d&Hvr6%6DIS^-f@8!Ju!{=d|bylYZ^kyAUb~n{o~-RoYPb z>7-9}VU5x_!YG_>qAN}ED1?DpI9HB+#eY%>7^5*uYFar#;PjQ~SdX8~09kNs=y-4y zC7((K(aCQw%Tm30*)3R_I;_N2SoNmRd6FE&vWulG<{GFrSz=vL zp1mWg@m?+*0v25j4*ifLnqVOPs2fgafEsYmrhQ78AksA}Hx96lZ07~~nJ}5)hH>R; z&Z(Z}X&%T~AmEk2iGo+?2-_?wW&#eJt4)tCAf;adbAaOCSeteIet%FuKh!I_Y|HuX z;a)we(uu~WFE-@V(*Z+Hk(-4qdYsrua8Q_M39|!)*Q5_aW?cj6F4VSEKoB)$h=6tY zL+D5Y+8ULbq6Wf*%t#xW6oqR~a$%@YxHcwzztM9j9W(AgWIn?Sp{~DxU#gLYE>=$$ ziy#~|uch`h$s22g&<|G2g}_(vXJWTDK==|(7*uM?ZRWw-5UxC?&B*zEupS|oGj z5c4$6t(0vXydI|o+5m83S%*%#M>#i5-+NREHpxduSb&W!#LwlHw!kI9uvf|==#qn_ z!6#33Qox3N~tL7fc6qf<%B@tfcqQC*XeF$%TS(;E*ElZUIM%+q@Fb@R1eIgU7i^MY! zY^av1D}#=KxEEXr`D9|eDba2KK*KiKN}sV}oS3-j%IKM}$?%-JBB45^y#q@}4C-1L z+sMqhf=l3bz(C-2nx+Qy)EliJDF-caS9ojzQwS zR~yGVi8UwhMMbX=2}B-+8ts^BOrbK{*3}qhtHnKk9#Usq9s%saVt}3-^42q=b7R&( zzo+5u?oKJ>@@D(}QTtpKwDTOLNiIUNAb$hXZ>=|I0K^5Pw3Rc^Mr-K$c9bdrNZ>5# zTzU~gG%cmYfmd;e#Y8%mB4>oyQS`nFchdTU-5R|~3|G#!oHM#|B!E>V{em-gOjlLz zT=%$gzhYBLy;B}w6Xou0T5R=f)h;l#e}$?I9fA`D;!%;DybsCGgDH~lHgfL0{M78$ zxh-SwA3xe;*ZO9_S{6(q%p-Q=l_cY*FK&yR2WMA;b~ZiM1ae>QwxiTS!`N&`IX(2F zLc?lCNrJt!?hxzcb{4y7?uHw^ma7W#$)(ptZc$ZP0;(|##mGG722HWFfg#GJkd$nR zs6k#YkT?gr%V!JI7XVY?jTY9oA2$Ev(+~Fj`tPoPerNZ|UbNOOiSQf#YD<1^n*Z>x zSw$%ZI{*xG_!#e#S_0y#`mILXb@L zsQ?|WIq$mE^-1+u6<4Xs;R>}4D+H+xwx^P%JV@xd+}u1m91hEJzuj(?19zke9~O{Q zL?iRgk*k-ho@(WF0;GxJ2u@X5vo#EF$E&{YnzkVWVv$Hqbx;n3OHmg^Xwx)JPt1{m zRn(+HzLt8;*JUAv)ag1^hXPeINY9>E5pbMF75I*Pc?zat<7myYEKLV)yW|w@3z-62 zrK_mM0s+gh<6>F9@+3;oz%h*&mRmV#MpAzV6#_gJ zX)QOq&2*ga4wHP6lz@_&=4Fx7*KNn$ZtMZz^^ynW`$+=5y4s?yz%7)ZZHLRQ3TXIt za)za+PNc`o$H@#ZYes1xhZ+)<^g~PZcRYpn~Ti<>qsRa z9SjiABLGTglL~*)tfycDh6WB*b|y)_kVl~cGkw=D>)H*YL}Hqlw~5vP7$629SQ-`8 zX8W`PaSp83>B)eg@ZMZ($aYLgTQpG-xKUg==yROk(oo4#`M zl>MQqC1{xRqf{$SF{l|{s2~wG@X!IM1a3o~Zc~!ZiI|LOg>Pz6;$T!GdA2oCX=()R zW1z=|wkZAIR;Ttx&4i+=e&b6d9SPGzIB6e=T7(KRv=LVKjS4XGWFF9WFq?{`VSa@N zP*O8KA`1CyG`I3bH>$8lmg^!{hfvs;%=Qz#5nRPP`($GCT#5e~z1)HI}eRX?vGfJB| z#n87N3UFEOb~}#4W+U&LA;Pw43$^AtNskRZRIouHf3gJtNeO`LiD{i=s`sLlAA~7m z+X+(8nx;dzy_KqFTIE55JEmz}@dJhsu+wN=H?-ZRab3fvSBfhs)RH_=!{UFmf9ntK zzyHu2KW@I5F9_00BK+nKMdjJLUiSaZUxv4S?Q$?#4bQ=O_HZEpVhtcV*7cT_aJsZ8 zwpo|uJ!fY{P|U zl}J@T3bYyoe}Z#X9c5#K8B4otEGlO6+v-c4w!j%U2!=$q%$vs3=xrPP0~;Msdt&*fAsx_G`=( z0D}qvWH!V4?Af!EjRt-bHi2a6`D*7-N4RW^prS)-Tnj02piHG51ob>?!s{Ro$Lk1p z39#T=5W0!IUDdOmhz^n*8Ck`aza>*J?R(R%i9GYAzZc5 z{URm2nM+)$$~%qzQlv7wRolKoiYBTADeS?ei7)?C?EUQe)ESf-@k2-)AjPXe-Z4(Kkt$Vzkx5QE4OL+ z{m<98zm&d+{s&pb!3a}q<2pmy16G2d0BWHQsx>OpNj)D3gI7QV%5Zi+7NA1;XFsvT z_lm{8+HIG4qGSo$FVxyjEpx59i5kTK7>It+2|?mQ=X_6cA#8iq=SbNE2^?ieEf*EM zrkJL*FZ4ic=t%^jVTq~ODRnJ1IkD)f7QS>Um;c|YNfIM4?H1aVuSRoc^%rtF8dan| z@g4d#LDT{>j+A*zs-x{}?cGS?umu#!q`N46PbELINdbr6Vg-e5ka5G%PH1_j8E&aL`&E z!7MGUk+c%x-_we@E(O(z!YkoYnJJ#xI-S@Zs;YyA3wg-sS*7l&9B`|cBUKxcTey|d zj@(DkOxuQT_q^^rv3&95quVBiD{=Bm8Ho;D6d;2Ns3o~Y>UTreEz+OiJEZ}dXWp)M z$n9qBx&%4ezVuQXsdV2s$gR~Z6Fe+O5G4T19{MKJVi9V8p7|-Q-gXYwV<@5M(2Hb` zC4!Pg<%W}9Tuwr9cenf7+lRwpk&{Z4X_7=J_j23&&2>L^^6L(nwQn%lfF2*LedTYV zX<)MSq?_n|bbKS;{^Y)$_uKiEy}t}dFNyFQ*AA90Pk->)i~r^SD34$EjD=_gFona` zYt5iED2^RmaB=`77J~2xyh=;?O$1>RE3<$AgiI-BFs8TLtwgAKUeNHhEdUfLcMUaI zC?Tmh6crA^QV$u|5?gQ99J_|@=Y?QJ%m9^1hV@t?sE&Sbo}55OcqWiCRx&!6hL-Zv zQ2~d>bVO9BR86i6LMpcq;!i&Zm19$x60S+L-vx>taC{&aAS?+x2&EPfeZq)FiewdK z!A4EDRs683#a#!0Fg=$SHjPe4vYD+|v&Ld8G*?x498Jlo?8CSZp>11vcB*KjG6Hh8 z8Y+yd{*J_L+P)_>p(4uyc^8dF)2c3=0yek-MUsx_OY9lTLlk0_3nx5Z;(imt0wEY` zox8Rfx|X}fga{7C9g^id&*)vK!T|LXQ)OT?>jT7vwwGf9pcEC1<%W;K2t1AK)r_7W zCaprn^=1hRy^)=6`#AWmYO8u?>flt)e5gt|bOR=E3Hmrx9HKk4YBng?s*sx~N5%{7 zD{PfXSPheTZ-6W>wIS?~vx?BPDQlbBNRka4VL+(1l{PQNyJO$k-L{j0N**LhQu6CJ zSKF)Wt6>0P^^wV+~kX{nuH|r%)<#BxVul{8J{zp47k9|?VASv2_Wy1=+F?2$g_N#Au z`SUOxkmMYE)M5F}oKhtef`o5Ft6_PtNLm0C!_cb1sLrycO_C6b zk8J9_JxOz7Ug=wTM>$AIfv`nPX_;5{kYI>Qs~v!RiJD;Mc`^jZXcLBfC~01o$LLYO zU6kNFV^fN`Fl^SPTUtP{muxF7BK@0C9Gs`Ek@NQoxE}z(+R3v3G2XP|8ImA>| zSgX2jS_VOmMWt=1<=tR<41BaYElT4hWyxIP(EI5q=hN(V*WCc!fWvV{GfBL#q@p$C z?FKVZh-+s%4xI|*@WM!r!lxs*q2^c!it1D`F3G9p#GWi7jLcCy;*P^tj#3>=v!u*r zIZAytNW%#`d%xSY*H`1C>k;a?L>ZHl*#IAd)pfxZ*g^1!a&izJv^HGN{-^hE{nO$7 zr_JqC|1#M1k_#`1@Eg+(Zp!h$|78B1UyP$iy_+J-Pmc{e^92fZ%8I6#3D!X1-(B6L z7+`rSp&^8Z)^sbpX(Zkjwpd;z?d7MYX_5eQJWiXBE6ZCAjGPKovzu@fl*U|kS);@V$p)KYpv8nEt!=ec6fT_w zuXWg~L6Biub)oGU)k5sMs$N-&O$ROxGsMISzXy`oN;E7*>D5&ApnOaJ3?(mN7B~yy|hG~I$-EBRdN4V^NrAzld{^NcjY&gA!}%5NS?KO*qH{qUA`1j5;Yj z&qejsaRJQB2fPnu^Oyu1ok;5!X>zGrvZP-q*`1A|VoF(ac=}|W_qd!mR53pkWsU0% z>V`yl7tnQ(x^h3r?+)^o;V?Nbhp}!qC>9RK6M56E^Q~NWpq|H)hV{U4pPcE8U!2qdDNN3(FyerH51vhO}efY`5K$*RGyCxu%up z>Kmnda!@Y$FK~^~c$MaMC>pY4U`SaM|K|PpFZ}nPHFy8})t|obcb7zXNrc~&b})VT zF#W4Pb8p{w2yxi5xTaUCnM(dAx`a#MC6K-18yo(aJeErgKw+%8@sT{dqR_TFf5~MW zMwR@)h9k^c2rJM;UDB$hJ*^ec>>$BazeB4m0=iwaU9Uk=(m)^)Lc^lAlQ0AzScK3z z=v8}WLkVr@Bdt3wq0O=Hd-W0~d?c~$aHJ7RpV5|TOD*k$R+!4;Jcw^iY(}iL1KY(IaUkSeI@ONz#U9 z#17<6v|pQXV<=JDH_iUxLH@hPrmX}?kc!d7kW;?!s+r2tmq|ly*Y}(Wo9Q@7kcJy? znCk!&gn_0bHv?7-DWnP`Lbs(bSf2lUI0~7+Biw_fhdG+ZpV$9Bpa*|V}<}bmd{ehz>*06*YWU*8O|zXZG4hfKzY#E*{Laz6c^r@rqC@~A z{{eYJr+)4dh|p@V)>iKb*u=#oaZbCdejKHQUc#J!&dBE~{^UI*&uSP3DPSjtCom!S zKj&OlKF_Y4y&)J)A(_v32X8&_|62inoW&S6FvSdHgi-9kKU>lB?^Qpyr#rB@-7s^s7p2IVV+t^ zrmm!{jFcU8LehDHYOBRkrW9EKPN(k+tXcVPMDbL1W3PJDTxCkB0V;*`l4$ALGpV6s zVNS}KJP{S+qF!_qnSBwf*4b8-o%;QeuSY9Vo4YQ*-iBBxe^W)yP$haEXEfQwDJ6 zI`lt%c<*P|?|<37T{KMwsfuqcZ54$dye&ErP7WK=NI1 zAy8mAzu)h>mKG5l5uyYlF=IxDQ4N*mX_62;4qa5RjB&foW|^mDPJrSjqdU)@GfB8M zLf0$I#{;%*D`G1bw?-423hzOyI$$>`Mb3E~tqMKFXL_wpPNv7T$k2ZSflRlIx_pH% zl^WryxR6(QBb`V?0f*R|dan2(XzV4^C|vd=g|5tf?h7FTpl~Elh$(8R6?@r2KE_## zmK(b`N`Dp>csD}x8oU>#Q7I-+E=UWF5vYxhGkJx(A`5!@fdr^^cSwU$1o#fF9w|YY zU}YA>_KuuXtOnIK)4|<0bE~SLF!Yg|T`xbn-yfW9rPq^!L#j+kZ&IB~k4W~RBGDKVe`wo{%}czmqhrD7>%y)J)gVh zEgVxxPcY=GW0<38NwxFDR$oPmGc%7GU_&k>0vXuC@y94mgN*5-a%b{ayKzjI7fGgB zsDzo?>cHSM1L9HDBn*QF4bHR(&s>)|Y36`~D6#ZIlbRIfsV$W!m#Ggz2@>qU66udn zFbR@B3iQ$psOYTa@tz?^5Ujjc&jS*-Z7)q$+e01u_RZymSOQ}RSz&Hkc2?>_1bX^jvu*OpYc_q|C zA64yr;i`mDA?3EK?;sb3+tmV1C02jgDlm!3Yo9NSl0#x}G31Lw&7jkvx}&J!s;-~% zbCR|iVAfn!e5Xc_oZ_}eRkYa*_<9&*G~o67!kd&X9ECL|b>}Fy7a`v+oo)&ll~z;@ z8kTYPWKCIi$$s$g=8vzw^ULz*#Ta>AIHd^qe+R^vt|suC=^zE32W>ujnYTDsfD%{b1}5p+Npc_*C# zQdH8_WuL-ed`B)9ni$G3q8vWP3E+<60wz)rAXcyn2CC=?VT@5YjRg0<^IdUAphA!* zrCpIPv?R#^ogW*b)^oS%VWUd-46JCZ2P_laaU`qSFu#*FX-o^VZ%CqeDlsRz5+K?_ zFcTn(3@QYzq}Yx?gPW@(NsP*2XMA@x=AzaIky_$jT+JFl^jKH29aJgXu*%MKWtBHn{c+<}Z79qCmM*P!-_~g<&~}Yv zAW20{M+qLt=9Xb2oi+^+8 zy~#nYRhjk#*;3p{U25N*AN~IQJAXR9b?A@t#hBuf2!Cr|eMukQAOH1VhHpQo32S1$ zBiliGE5I7S>@Ag8Tg?`@tAQ#$)`Vz+Tpj+l(4GFwk3qM9eWOI>wb=}YAd0=tUJ9FbE~7*nsWHJl>o{VPo)*L zR<80odIdF`vhk|+yXGWx{&m|BZrv=;omJOHxwF_`OQx2JaLoy8a}VJHxxg4!^NGkh zkT$fq)Io^+ZitIvH3C~Hcx`(5jAae#mZ5YZh`l@ z&X^#Pq6>~7hVa)n+5P13?(a{(^Q-n*@V6I!{Us6p*1hC8xXSZCd;0J{|MC3ePhxZH zRM*iG13;250Bpra$X?rAEsKmQcp-vkPBXE=!(7M$QD|u{0b6$ozrN|hO5=b;^$>Z; z=SX6BvxZp>eFAqXoTEDR-fJLp!K8@QB&^_;B|SVW$7wzsBgc()vl+S13)pJF?rxN5 zCf(=NZU?b3P(I8whk3r2;3r9U==;Mwg=JoWfrMHa0rRw-^OfSE2u}oV?ke4h1&?+U z&g`&O&=4!Xo}s9gIY))&YN-ls`1_Ts(1w(Dc_XZ^peF#>UTt;>!mv3)g+K*e8msVm zW}rs8Ri!PS5&=1cq`-q%W*MAm*pwCZk0~6;jOb`E93;CVt`rM3RJhJ3W60dKU8GRX zet%CaqIvb=#ejBOIrV^;F3apuYOIpLjd|B;UW)Jef`}O^W*G1T(kC z<3z!32b*ZZhM@<9FBNqAi{ivoojH9s6_F>HS@AMX-&ui-dPS^jS8r$dnFa$@#)eCnwsDXZ@xl7qH3iD5GtB7&rAi)LX*ipa-7BE zouNh-G+hCp%K>gt6+xZiBkcj|Qx<@Y0={o&eGkl=N)%8{LGHvh(=xHtLB7!&kGi9T ze6!Alrks4=RNkC-KfZtGKa6kQx&6ht_mT*I3t#I8cVGOk|J(ATPrO~7i7aWM*-yVB zG#-v)PbP&5W-wZ7YaIaTwPsB!jpKt-yen!j<)jK*Ef%@ zcGB%M*Vos>I7o_}=Vch7VDjqzuJulO6iY=i_H?>dL>{M6hKh;%GdYqzHAxIW1^*)o03AQO#sl^8L+XICK=6S|6 zlqpBQ%NkaBXF0vc!@+VqrwhE3f*Pfzsg;w}z@%v)(OG~&4VW}mD6)2Sq}w+ET)HYT z!&HIvDJKudP{F|NbQ;ZBr}WfK+G+`jZnHTj$m(N7Jy}v{{OLAM^f5+;Ox1;i5FpQu z;tCY>Fpg;ZB0yRyR^LfsA$=um3Q0u*P0&gA2D@nZ?#71WcL>!2dFm>%O5GrhWdYNCd>i7rhMPk$<3SNKsyw`gHZRt$^-{WUGu8oV-((%^&RE{kVPl+`qbT3NMN9w{0~Te*83i>oc1U(%=DJ&22xJTz&<< zfQ?2}_a#n$5Dm)-772ENLiMewV)&Ns^EI|Wo!CQSW?ccG@J*HYLNcJw4uihVvVaC9 zB`pOQJHbqQf}6~;KQ4C<$7e5I-oCo0z<-3+sI^a@-_6qu2Ph&z{uoo99V2x;@E$F$W-WB*Y-V75RBvXL?ZIP}m+? zV(U51Dp4YQTp)}h-xoX1Rk;?5SEQYSs{%47egi$+;SJPv9$|H*h8ci11U?c|#5{p$ z<~jK^r%0&knv^-iI?O3iLc(YddshjC(h6aphe`W`n=$IU3Ugbz!C`lV!BZyFd!DA8QOKg?d0 zi7IbDq2Iz3c~Maw!^xj~SILf%`Z$qXek{6-Jvli8{ZU7OFy>u#j7c%J)tn%p3L?cM zpsSJvtUo-*=9=?}9kT9BZ>+bb(Q{o^X%?w$KwC?D_N>rqfad{qMXsh>Y)Rq#$JMXV z6O|RvQ}ia59=|WSfp0VC`)dmTRLk{qZgJ@#OBhX6?2})h53mEYShuj!Qp3Pf_4sf` zsgO{asW5KTVpJ2T z+q3GWY6L~vQ2{4BBkvryKYe)jFSnoGUz);yP$KLK3f4;{@&BhRxPJeB|KZR5qkZqS z!Q;&j4j>*yY%d&FeWr3D3@Oj(HW2MHU=kD*u{1K!YG2}zGCbVv< z3(sC!c=#;?@1rr!OT@E%S;gtOXR1<2(8u z08^Jb3K>hvlvd`^qetB^Vs6&8u4}rX-EK$e-yG6dh7N}}eZ!;-ztiekLDgwb2XC|^ zsu$|2?+@mOQZFwHv|}1F7A50Kl(*C_ldnaixJr;fbVdj0l(Vn2cp_|JAO*-fkbbsp zI^;WwWfP@=S*)pDlRXa%bukM-2g9*Jlt;Ki7AGs4mKt9AanQZ3(mqj+D1{ujT;Epm zIIyusCD+ud0wS){D*1RbZr~jOH})VI@$UY9O_dt)-o-8ux8Awp0t-MKEFXN8E9Fj}E z^NCGo?CWWXSa03)3Q#eR1na6NmnWOYGSyYCJX@@6w7keV$+V{iF`b}W)Xr_F^gw`^ z7B#F6anTL;*add;xell4J1iGy4XOhnf9WxY=vT@44$zfm#cfY|Mj~|&Yiag^jec>8 z@i|aeJqSZ|CC;R^S|;_PXN<07%G1q8_j^)7P7B^Sk@l)y&06LeYz^9uDCtD?yTXqz zs=}uNKzQ7BamZYlpfgi=*GR`1{SWtV|K#}HFPfK6{j1f+O9cIUCBp5|OvWsp^eI>U zesoxF?BxXg*MH?*eDy#4`QaxYbxr7Kk#@2gKoo-EUs8yur5=@4HWgc(n&A)#l8U}m zPXqF!tK}@+vrP!g*bh+VM56(D#>&~Q+$^Y^NbU=0uvFV0+^99%Ii{Dd_Md$E#V>#L z#lwESyWJCI&_N)v0V&wzIoHKTmN08w9C~;%Fld)H{ZV64*Q3@ zv@VoNX&G_YdFcv{4a3TOLHpT6;~RX4djGgaI-!-Gz)4XO2r}S;(|rPMP2vRAI8CTo z=fDI;&}UAPBgCd7wCjB5nf(my0J(K#w?ZUQQJ&>j%QLdwfEvYgu1mZs1i}C%fw?ST z@|HEft2VR5*_xB#Vg(2?F(@IRA0RlnECC;k39H?V1IZ^Sa5h8ls}V8qO!9B(JvYO6 zJRa7tbnxV8*e`J8$^h-qaMObdJrhMfVNKMqrISnFYT$Oo=$8Wd&nVN|Q(6q}B9U8L z)n0EnsV}EV=2<@GJk>nUjft+T-nq)N;Mv1k)woj4rk6@iKJ}2F?`Aaw}~;(Ym6F$qkJd@*4=3UfUiTuupv=}&@UKuIyx{HF8oLW0bf>W6&P)Ty}970Bl=ui z@M;)RJxOM{?;y>Z$}~f0DLVz=sH~HAxJ;>(%D`NmX;iI9`rN%Y-Tb$=-~Dy_;;^0X z?csu&`FkZo_X+8nQgF1!$UmL2{15%NahHtvUpo;#Ugz&U&6~YP;g`Tdb8YeMv1WSgzPb+q(V3qJ9F7jkB+4DO;-T4-5d{b?3trLB_Oa5z3}HY3`YuyV!) zXbBw;L2ss*s0tPrS`e%_vUr-bg^0De(sYANPq|77cmkzJT$2Ng>ST*E>X54tQ6 zK2;?*gEbsI?RiUs9%`}HrI%*A$_;KptJc-oDt%K@udA?F4_XlA15s4b>C2lc6F~9z zIuBL#L11??o(?%ExmTg_R4i4BkjPc(R*f&^hvbBY5@6c`C^NR3>g^llAr0-?K?A+% zJT)G`Tg7P9Sk*q4B4wNW4!Un@S%jse!FjKMXU0kxW}I1}(P{oH@73RM^llspX``8{LX{ zmEKU73QK1O>hGLyq94>2Q6JSFkWE5aGzb=vl?friL*=Jw;;*&2v z`o*XB57RWSGwdK*HD{ncsboBPTltsGkn6P5a&*ydTmuvNm?kHWx!vv#OIXKYkd(H0 zYd{_jQBq!uA*ysLF0H{4>wvis`VZm^QGu^5@=5>=3)G3g9wS3^#n6g=U1+(kdmYUB z>UUJ>I2CMT*wS`6lY5T2rTL-m{_H8Xe5K+3Vs{`vKG2=vA04QScg$ zQCzlF?IHPkktP&@>%^z%0<^QDO3P zzCTZ%)pnz{%Dm}a&CRH~6iLHoUHWg_8*EIa#NsLu<5LUWG983t=a*H_j|)ij#fkcV z1Sj)(P;pdTqclqWp#UNn^cr?$u8>vAnF^eOJ-IPeeUNggK;WQp=PH^BSe}!(W)*l) zNpKV?p!1qK00n#(_v30Ir$-G{RB~y}!OC}>aT*~*N?J&Xs#?pnxKUkzK9wiy_`&g8 z|NiQYFWMKYi5Jw&Z=49T`y=C4M$ero*T&qK^2nIS*2ur*f9{wLJHl$zb*HLd@qhX{ zgSou^5fkC-p3#R(V@E3 zN*;A;R#g3`=#fN2Ncw7l&^Jq;D9t6uw6nFC}R)O{S67 z$wRT6%su3ZhdZNDNjA(XxvY(6yr?F^maqCN)!d`*QqKadE6t71bPJ_$oUMJ&4P_mW zunwU1P{qrT;a=2-4AdZ_4iz|moC5=`N(Q|ODS0JO5LU~5BrQ-ZO>}Y6cuh%l@$^K* zdF_UdS0zg?M+zxa%+!BVAz*ayKoU;0HtAq3!NHnUae0c_Vg>A5xwPx7&Sh%9CEpm6 zQUZPCk%Ut{XRFnPTC$+>Bnnsexa;Mi4MwUouqXLgC>**d<|s=gyUG50(`z3b-~Ooo zY-;9cFQRL|aYy*a#xCp{G!aS)d`u^<6() zZLZ}b$IVtEQl6GWID&#t>NL!n94xmr@z6!jj++6Dvi0TFb}ZXovf$nAojm&6%^-Pi z>>;(Wme4i>XzvZXE((<`VdiG2IUe>S1mWU59UYy)+2San>*T%*u}7oA*5231r@}8> z#mU*RI^t6`Q`I;FLz2pKYAdK#=$6qY0`rm;{~&MF>4wl%t2mx^&xsli!hvTA;{wG3 z%6Y*2s&xvqKiSNZVd$GP;D(v903b}ReT*x+F25{`q%HLZ!S;?5Row;Ksvcf@NpqqU zpj*IhG}q}LtcV(Qha@&1m>RW=P-`djuS-z82_3SQl)3Zt4QTkTI*&NoQW#&wKS?sd zFjEERzD|KRCqlh;v8rlkRF?oC-l9oRp(Q%oJVS7`#XM6fPJM8FDQBPkI(xJV7jrZ^ zB-@mrGI&H&nU>=as`Mez(YY7nN@=`$(mW-BjnZDHuvX=lwga*yLz9oHsThQkyMigG z`bCqJF$9cHAmVV2o z#UZ5u`hcd83BnSg+ch1YJfqUn>6De}FH31tcCJOYvw_waj-7-#dBI~WrU*-p4w(NP zEd%9C*h1$-s8-@V8eB?M?>vpswa=4$z(FsKDv}yD88!F_>mtV?b--r3rI2SX4cb#I zMd_vlj1}KgQni9bzs{-G>~(_PXgaKxSk;WGZKRM;iiJ^s$1}O-Gzk4V42Sx2w(?j? zHK8oZuPfD@!`6~O4ZHwX0f>hEeuJbNJ@oV-5b6{GRR_D+vt>hvPNXqPY2z*eYmcf0 zCi)vbjNd`Bf4ORv!CP)VBeyjWg0u*G##=LeaEUG+xJ~H7{O< zrT+k=%sat+XObE*OOf96DTkC}gE?Dw+#iwdFmgdV76_c95~L(O4ByByGIV63SO-uO zSigf{2W7EAN_6ZGYzwSRH;g;>^jrl_%UT@fpCFp~cGZa$0GVwJMKad@Fh3mTSGRkT z9r`z3d;N_!-~8@(erLPeeg64p&fQ+^Hp94;?r@sshx_A#3RpUgs4;K=K%k3ZN$V=# z4u-*veRz0~I(V}g!VU5ddS8DnQ9h-7g3Fl|(m8SH@rUyBv}%R+KY;N>@hNIRdB<#4 z3ezEYhpOtK`#j68G;#dM<&Ian8B<7cRU74`AV8;7_5Nu%fX>>+DM|Cx_f*L@qiGal zy08Rkl-B^+6g?aWu$dB;m3p4dnGfE>!6&M0n6)R%1hNC<+^BV5!!c|kX+#A@<(sy; z_XB4i@DaMk8{K(U`DsvAXn7&=vZ?kRk$z0rtI3ybJqCil8CtJUOz+gya-~8z&dkF7 zv?FLEC`yInS?@YS>Lk93eBsbt9i!n$0vDvDOqByZ88st%ejITUpyVy3UD zve~asN7?^^6QpeSRQCXyOOh5Pb!nrLtW?w`Ujh|T;!e?GxZoi++q7~LTEayGptp|9kI$ zUz&`c{p_Q^{MkqM_Xml(+sz=)=zKg_45oard7dTYt-Qy|ahe`Kdc;Lbrae6#cBqa~ zCxq zcwThdrc+QQ*oPGfLNzt|Z7^1+ns>gEK5?3&hH-OTFu#&xls;P;y&0k;pll!Z2Z{LV zV|R0NBe_S)g_dY0^_n`d_UoE0EYuBv;7L)ZVobvUV9}$o{I;rQQk3R*Iv7uVW|ylU z2DxS!728(wA2wcCre;{xW0@C;nvr^|<;J*>UoOFWv)Q1%Tq_DRDFKti=augacrD~i zf&15lcOGV*ogGyp?SydAbzt$O3fTnc%yD#WwY#e4^Yn-Ij)qRTrs%Vw^w~DUFu?wBo-MsI)4Z6fAdl&VI${~6gpwDt2Gj!uw=1$6 z?_nB1S-UL21$%OjloL{wX=&G@^;pwMd78tFTf(beD*UUJ%>+m-^g_8HTcb9F0TUf-7R1oZdW*VuDe%Zrt$U@$F}KhcDaPhv7Kc%YgLnoCs}L z&XY~0w30Ck&_KcNa5Cn>m>0!7HTHAkBqKgKEe?0aj#e5+JDM_NL3r{*0|^*cXDpczyRLV} zb1@+i%9kn)CdDIXfK`8!?fJq@25GuczOG< zKVDrwzPf($z3>0vgWvn`>e1E5pM3nq7hfEw#3=6ah^fCK)~^;i`y z%dvNMGswez$TcYExCX>HAmWJ`ZS2sy;5CgIwOazLrHWf( z?gxc|hPx|3T8c6=Y09XcRxmRvXfE9eBL<;o(CompTfzboeZt%f>niKE+s!luIka)y zNT62Jc`C8Bus$H)(Wu9eD$XEbM_nutm_e4Jm7{7I`;3O^m21qzCJ#6uz@PXnUG*cT z!dOzt)6;U3!&~Ij%svB|GU)D3A~H%|4=n+Qwo*>4sVwy}0Y60=WyvjhpVBIJJ$7t1 z>5#CgcorgAw8cD6H8tmP1X>$JwBq>hbpETX-npk5#e9v4cy^FxZUZ(avozFfL@14| zR#v>%RpXSqblQo*Cla?riN+ zW>}}Vs46%dz$NHft6rXUBmrV=NjjKef68G3pfQv=;OwpFtbvJG-g!+Vyc$w6R4|bg zQ*1zZha{zRVjHPPK#Z-jT-mf@=Gw&cWEwy8Z~yuB?a$h0t6wkt`oHsN^y^7VpZFxz zh6W4P8t#ZmiQO0T0yz&O(+(^h$#ZsV-D7JdJ6@G?W%ADE$EG|%2PuD89$EQw%vam5 zK0x77+~csux}?Lu`I5LjWEH{hnjX;pb%Dd3ZxWP5yi}-YEP#mdYAdLtphhx=WWlCI zkFJXc^#+2d3Z7Z)7z)Va?4|T!?&PTq3ucTheCbx-wQ(l!1bhIG#c}r2l3u*LpV#Qx z=JlJ$-+TXuKmN&2x8v^B?aQZMe);7WUyQ@>{`>EL_~DPPZf?GO@mH^I?;+3V+XYno zVopQGrft6c-n+l|d%ugqe4hU7Pya%GcUiML%FiE-7)|^Qe6(suU5}of>`0%v=;4sih=epZ%K#(_}qR-RR0cGf= zo%OJtizMmeU{~z+OkOj6oj?MCMO0Byb>N540>=uRS>n3ejPwsGDt=~xj9uv;B;}4= zT8Nr35OZm^0_j5DKV+k)<%mr)F_xI1W=C;VY0dG8%F;iXmH2(m%fYY#3r3)IO;WzgxXJsD(i z5JA-im^W?Y!C9smRSCDP@;$J{Gm=!nzu0WH@X^F56gLqn6qTo7_BQxZd6}i|@$_GW zmY+hjB2@386tkukV^xL!(7aQ55ybvRA62~u#=jIoRTwz+_m$yms3+G~QX>3rZ7)XltwPK$XC0f|6vyV)9-AdP3 z-rxlF3E?(7f=)Y5P#d~Sx;i(t=HjmW^;fz&nvrmMmJ0<#EJ8AXELgG0_a-JrG?kS> zHh@wkVDm8j@mc3%#CIg3|5Y#3p)b?Le!{#CS2ao%DXCE&wy?~LN_en##=^cE^DGZH&RePefInpzx?FSfA;gw zKYuC*aCP%|jQQ@>-4VK@%VzA~{?SXm!)qH(Av3=qHe>6&SWdcc~ zAhOYB4^?BJ`ic7gNp^>$Ia~iwX0MZO(or9NJ_yaQiYz-*{&Z@5y5gAI(8HUc1iuS4IB%{p@(xQXgQWQ zt6$ayG#}3DyVkQ%q&uN1$XImVyj7GH1N*gxEDx1^V9KtJTGbS#=zZZp-%a^?ZhrXi z_Q(Bu&zieqyF`1T75+mK;op35{#_+WoeGjb?rtvSd2xFwv-r zs_!=vjUV<85Q!efciwsD2Ooa;`~T#h?6#vk*4yoNUFPF)@1#v_H}iCO^4j*b*B)<1 z&L^!MrF)CmX$9yBFzFHc<8(uZBX{axs>tjw8n*urq+=CXFC$tI% zMhP2<1y=kma)e}b_WoYEgd{NJ(+ray5y*UX?^vqEV1>PqO5ZCDgY!V@m{Wd@i5x>J z&6GOwO>8;AL6ioIcfdT)s?7%dIm{br^5@;n^^M%9Mtw6l<4#<2>I7OJ-)0g3SP`bSEH|Jj<)Lf8+{bIvwUYr&E1*;vzCGg>PR;te?%Pk^kcJ>qM`pehqX zxrzOWhGD-xp#4hn^qcf5*ExRcA(Va_aVN6#&up8}!z2W%7wRPI?zO zwcD2aDoe>Kg@jnv(~*z?mF&$FFr?@V(A%u4J(T%7KyQj`;qfDR09F&(t;s$-9G~Cb zZLe;=_3nG$e($}u=|B4UUw!%I)0Zz_Ne8gm?!52jIXr##OcJ3)oOT$mb~hw~@7J`D z5(j~fJzt;x`m@8s!+-pfKl#~T{sldQFqI3-bbZw?)A8Z{;mM=7yS7u$p_0~y2^*=! zD*P2q3D#rmnH58kVf#>FBvs6sko2nT$vIL5h?X<^p{OvLgi1LykK$xG>nZreLq$XyHjQ-mx9(*(+OUb6CK{4 z5k}?ofRpIwh$F3*Qjir?Gf0Z4Xmw*$B#+oy@bRe~*mW4`Z@P^tpRooqa5-su!gTF0 z44J#kevmeGkRO((C4?D1S#Tn3Ocm7jQoJ;B-zAC^!qcOP05=f`%3ZQ@fuuT-z!xyg z$2sr&VV)=4gS^m2+cva-)#t?X;uZy{6>N#N{g78&+zxh_k&tJr9$k_VElVhhK;gEJ zYr-^0>c9vJV=J|v&x#M{a2Fy*e0MMznZV`T`K#1@XMXbC<;}kyzc||2T=@0>B@*G^ zOo~#G6t?&B%qeVo&v4Kdhb1Cei8rP^W-ffKAQ3)hYq;aTA6cVJ3>B}T*eU+)zn&KD zH>9K)<>AI7V(CJC!uC^?!dpPkHsR&20A1g=0ztA+`- z+BL|7l#+H)uAwLtIz|h!G+`DQW~E4&5*xbh{r)zZX53zHZ-)22``ssRyz%Q_fBxmu zXD?p9JkB$vP0>wGQ+VgAfk>6bIFp>|U z*6M^4_z%$GfdiVS+2YEPzd>UMg~ddG^f5Wd&m#z%tW)-K@`$2QF!43G&AP5H3e%wQ z-l=*}TXemJkVw=&m!`Nr1;lX}3q_D=GVN;R4&q)j(xFJR9T@k0-zlk`{gW09x^8n? z$S5aM=_3$(G~QGVdDYWwXfswwD#zg@rUx8qgWLVhx4!l8aJQ^0YC(xXA$HqdJ`2EC za_XgYk}^)Zu*jwlbbc&H+X#pXB@95f#YK`dk6y1uXX{jy)R2s+*1j(5S(PeBDy9Uu z8L-BAR;I38%~q$%5Vk;{?ABQ3V;~Mjy_|8?bDMY2I=vRJ6j z%DVU5V?MjhgWsP__c|T@=S_tF1JEYhR&XByXRHxZQ~PKfR7?{jOt(pTK>|t>6x}32 zzP0haBuJb0i0ehyt+k+GXf?PXOw+&dCNZ6bd391}_K7y*j^Iz=A?0ak3P^*vDi9cg z@Y%F376gq+TxLNc0a}51s843I0LpcNlptH5MNGK|DTwvQGsamIflOe$LzQuoSWrkQ zQ#PyS?fU%vho7vrXZyoJp3B|t`EWci4VdTWXP^(9J01Jn>cn_D9uCKo1afF*l=4|- z+6YX)`So{y``h2$-QR!y{`=u{kY-OJ!l^s0nu5M~1NCAojD+S`HU+2@(*UjnD_}6F z;;1n|BaX**6poA6uF)lXeintA<*T>};Y#At(nfE1Ut|f-xT^pBc{?DETITV6v#KrsDmY!T!xFZ`cK1$zwAH!ZSg~2^x-uT z{>vo7zZ2AnK8@TX`aZcw7OiEnQht^MomM8EoA}r;r+z$jbG_Qi zORhKTcC)Q2DPPR@-+zDqaJS#@=<{d#zTY4Aa*f7u;3!ERQFz7@uQ*H3P?oiNpm$v- z5nRe)3E%yxmnczTR0ILza}CaDcEbx9m4-O1BHl_N|;J3#b5m7WkraN=5$LgWpNR>CYvZU#Sl`7R4XqiS*iPFB7@MlE-GlKgplWEZlLEhD+Pke zEI>DUMbogcoAkre6xS+kKocR~4QMPd!qLu_+Q&>B{drJdMs7*9^(I3ZN~-P!9FSBW zFY1g^l?BkUVJLu}ZG@mGGlJ^Rz4%=yWl;Vo*+{OI+~sxK%Cph;gXKk^Ig*vLC%K2V z%uv~u)|CtRh~sx9`Hgh!avT=p5Iu*=ER$GQ7Ie%Yuoe<8a<8y4Rd}^rQOqQvgl>qQ zwbZ1AGGFYwF-#cb_Qy_YR{0q@wg{Pwk)Tug$yrC#00UF!Z|zsJchfL826CwQsbMk| zc%*(gTQ<)|Ra0=`emf<1L=IQeD!AKW`=`V0U$&P|{_x5v{8vhZ3-r{&LWBVrG7HcF z{z6*>(Em5d{`X{WA{hFT`L;}fSAf{0YX#THHRbGd#Xhnmz(9LN`Uen2_*MEff27OB z_#aOF|Mt7-;$Vltt9&Ib*e5(np;@nJH-WWyQ5C*wq&0-q13EuKBwoa2qRHmmMz>@f zap@ijITFn!2W=6a5+SI;5SikM#&3v5eEKk0l9(&;bh|m1QdO$yp_fK(-*-nzWpc;} z)b#t7@5sBC}8Hy^**ph)~nd7@+Y>2DZ`Q(@i>1NRc(WnoV%N!;PPDe@J26PaR1Xb^u zi@q$JW)wC;7D&j;8{MTz>gDWInh~Wq8r_HW1H3@EjGWc+_;5H)6rdT0BcVk`a~Q5d z;T!qBw^a!tGfPmdeDbEIsUf9hY0n3!W8&mVQne?jtPF3ZO!rp>GF6#j*2h*4JO?gWbuB92agY;IiC0_FAXx4h z{H?U`VW!48iT7V|?UlAa&Ew_er5tA054UgL$Ty|l!U-Dsrmhv4PTNkf7IaC8-d**8 z5(L)eFhQ;)5-O6;j_EH(EKF)rpdg>S0oKnDx-$e&C2%DTr9*wwc%rFBH9iDz$;G;9 z>g41$IUb%d%`Qqg$-`h4xj9`cscuP9<5289gB?k`KuzrpW7pKeTOOr)YersjT`n;P)V%!qf2o){eeM|35I|9Ut4 zmmkt;;%)+e}-N6b+Vi$LWhH73eZvg>!F?3k9D$RzeBb3X5QD>lH zZ9$KNEFKmYQ$$Ho)5OZU$dC#LO1p&75hCc0zy+AZmN+B|kdvGTKL_GavP?FMI9&nC z`;5{zR~0`61W%;525yt!oneG&dDo9>ZZMB=e>k2_@Qghj5Aw3mWw$dE>ns^YqWx)@ zBbRUzxXT&3WOAx&F#T1S6;K!Y37X;5k%9=Cft(VUaE*g8m^k9l*x-^x=vAa!nl5Rr zvKc+c?%;sd@{ge&#LaWYN}+538dF@akWZpg)z6Jvz}3Z=^FX9T#!xH z;E)VpN^*jzJSYFWX`09TyR)-1Nqf>}4bX^5izJ&>1Hy5dYFCiqAZNGU9}dmRuOJ3{ z%8Ox+imRrwBznllpT;3@d{VHXhlLY%5F8)_hUTzSbfwe=hNd6Lm?|d2RdvAJg@1dXorPH;Qq=+Tr z4QUv+CcWkFZ_p_wHN*f-ma?doNo6*BK`a|FK}G1QO>DAV#*ch1dJ~S{em?Y%k{Pf_ zw|0O>S@d*)fEG49T?%OKVhdPMV3N*ygkuNJ$RQTOidnvx_DpC(B-+ zAF4S~*b~?as2$>&`{C0kiK>#}PDcR5XW%!6zB`OVFTc>rS?LiFwTl(Nd&lEZz0|q3 zKuh3>{^x_qc;V5^k%vC3D@0KiZ?3Q3-dt?9tBbRZw0)8>CD$Nr_Nl2$wXEYB&Prk_ z;mA=^jvLyZ(s@>OvtFOcTMxrnHKoML5!%qI%%bZ=rNgV;of`clUAB2b8X#HBgbg`X zMwPZ{)O^Pi@u@B5R?FVS7w{zygUDBy}K1jd@XqV<(3jvTo|I%pBERSIz)^>Xp_K*p_)slT3SV!Sdu( z_R=U9Y+(Dom$#mwMIay0N&k0wb@gz6zh19FjWAibj&D?q6pq186(tmWb|qiJe5zg@ z4o5ld#Net5D0kG1 z_SDKfJmRJbG+acnQR0x)ghbP-^5GT6gc3ztU09bSPrW3LSji@J%I)DanTVpG^h+ul zC7rx!N2r5Sf~)5;R@dX^Py4qw>+`BCItui>=DZ(oB7FIh$PF%=JyFi)fnuj^au>{n zZ%w+*3gD6jF)!U1dx1{TUh+F6CK5iqP%s?8RJs!SaSNYL|I7aLC!fRE(XS=Bs-kE~ zBIKc<>?BqdNtJzVLD-Pv2GM4L0zOVLH^)gygnX8)$re2n&m%Y7(G6VQiClnLjePlB zVh}ftlg8`)e*fw2@$5n>Z|T%&csl|Z%)Moj7(^jW5I9B-0qd$H5mAke0P)M)vYEbW zK&B%GK^HnsWdnrxdb8TD*OwRDvu!IS(AXc_qTyILIh{iJD!?%@a8FZ`2B1qO{7>Zy zur!WG2H%Fg?W}F(iyt1}E^(eYa|cBwZDTXSgj>?8tRg;_U#)8CBgbJtl0l0c)!m}- z8Il^wq_zZ}corWVwI;Ra4U|j8o8?d zG@-+pr-Hmj1X3D0_M^OAkfW7bK~7Je5zt@5>FzOd$PU3o80iq<@%*;yM&4)tw1;kA7Epy_ zNuU%_BDjE!%iY(Lf z;`jDH_P$ywq6efDaCF);GlQ{9u11QRAUpiV7J!`1oO)#ce{ zU9a21q;b_$El}l{cUOf+Aq~zR{7og4OIV*^A1A45B+MUh3i;>U)Hq2?I%J{&1+jlKXbrG>C!1d? z47u0g^PMEYU0t`Xz_NC?+dc1ha=z9Ndu;5QR@&?B=E%uwytz96@cx$Y5a}-qK$&J& z`6M-q1aS|L$OH^)WYL4p0t30K(A$Z|2XdAij7NsPFLgY~QMSB#+qUWv56QPl!hR}Z zAqVD-`Yyo)L$e()ob1ANxu|UYx<+@X%@kjRw;(GNP4IG`Fy*=z0aCgC(xJ)iC;xeM zbtNTQH*}bs4}(Ih)VV|7@pL)?%^N_tkhklG;nepVxm^k4vYfNZEkynFB<DMEG~+hHzgn=H8meWT~Z@>Gq8=*UA1Y*>A1AHIY1htQFxB zMFXidAvU$qQTtaYogdeiJV9rYEhf2lVNnzVt#&F_-^EOVQr6amTrw1;xeS0qZ_Jrq zK|+|9rk50`c`%!_={g~q{6CZ^CMcg3zsaR;$^dr>Pf!w>Mo<2te5daQ34c}9G<7RU zaE2Zx9&%trW2pEha(wbL>;^zCl!tQ|N5JI3)}l^WnBeM1C!d?NEuE-y^Hlk=Y@}_H11L*5)TtXgZPB>ddN~Em zWQ46Rm`%&coNY?9+LCy-+cU}Khr{s&c?B&JNPDL|QDAtIU!@96W1Xo14==5DJm58} zPoB#rdzQ|v*e5wBRf3#!DIg>i7Ug=qk&{%z@gTKz)%D?NcY1Sm?r}g+ZC$suoYu!( z*ECYHRM5_;;6@z7fZU1GGAV?J3Ijo+Xo;r71)FtTFplupeM&Jz?92$MBAe&Ex)b#( zYoL5@U0u%M#YaMg#Z{e7i5Vuo_-0t`b9B2LX;4d!M#)ocSf-%Alt0J;sG{DgL2ZQM ztZk(PlABguKz?tU6#%`QTgp4P?FvMT)5)m05%g!u6f7Vvm_`q5!=yTl+WrR2HUnm> z&}YMX8r>VXwOl;J#Sjo^8Alqt+-w-s&)8OCw+)Kwi~TFHV_FHfPD zH;6hEdd+!%w2AO5WTj1mF~d???@jt_!hcV88~vN)J{SX;Q=8t{c$3mw3oRSU*+`q1 z)ID)&nSNCFgNmBqF-ubRHqcgP+`-&$hKML7upy`oXpHAnjgCvYK+x_7djzM$z?2r- z^RF?qDTA(aMTxR~9v1?&g($vNXek)wQyj*byRAxxoLx6)57xA8v)a_%Af-GIeI^a) z(nx6WUUC&QJ`$A`O+kyLM2O?e;aDI#Un*p*&?MXKpP#;YcTu@Cbo;Y)xoM=uQ}Cn2 znYr|wf=bUc43#Y<+dCwtCEgR^Ud|e`F8cIzxhqult2B+hs}Dm-yOHxR-(NmqjpuX zkDBGEnyR8yK`~EVHuM4N(2Vnhoi3qDNR9LW=F!T*jqo*qGB@@QXt5~>4h}{F_YrG9 zj`Fl?)DD|e^xWai*?Tc*Zu@#KsPf#4%V1ut%E=jqPOq2bGewW8PEbuN3_06;(_R{7 zC1Ms-Vk=k=5!H#MrXCnAGJ2!@%Akfye5SH_V5GJHC!t9lhNa%KP;Q4>-vq)RTs#Um zFG5_VPJ!y-@*YDk&uLMWbl{~Hr98VOOjIm#c~qTt3ems?am3u5gLjmVygl(?8arDx z$JUaTZY#)+O0UT?e{8R35a<Ztz^U`dGQ^8f6sJCGKMsfsZhy^%uP)0$B_yl5fY77e=tX-Kt~{A4Zu2i z5?x7)N2?5ayC7ny>Xhwm6xtYRpzwd6HfJ_8(hnE=T&S>f^4V^duGwfk}D}YpkfRt0T2~*w%qo0?R3F(I*t+&A&ClJBXlF-Z{v%b z%ZvB7m+Q9T25YQAB$PxPJXAt4RP`K2VmKV8m_1pQKV>B3&od#S*a*77$y-+PBsxRA zfdK2EG@XPyk8XypH;8c@xUs^orS@<}LXu=ed2^6G*zwqfVJ`7(fa=RwFpOh@jP)v; zn~E~a;KI+)$APCSS!-A_)wCswimI2c<;Bq#%QJSQfp&oCtwCl)7Xl;;WXk8Kr|0i} z``gFI=ezso-TpYu36M?;V?}F8MMt%ysy$Aw&oKAxl%#e7`2t{}!}HTla$8;czMFpX z@%{OBbJ`%Y5VW>sU3H$u3Mq&*3y6STaL+W?4!QxUdl-YCw3d`U@kh4eIZ z6XZ-fpr-+yfO~5fv(v<4WJv622GL|sD_jxIrAfS8ATl_S-e*~|7n($Nw}6Ui?&-mf zv0Sf$XfivFOA-JzfYK;2!}?N+i?XQEUrwn0q(q=o^oVh&qrVOjU9f6G4hcFJm3u-h zqZBG+^l`~X8qyrYTi5^*s(V2^iTG&IBjtnHgT*lD&j?NRF%hbcM7ekM1=LCy9)_Y#l{+IUVVQ4 zZ70H)SRZFE>CoBrm%d_9B$X3Rbe)Z= zG!oSZB&o`lV&LXPKZ9A~>BYr4CYAFnh4%IJbs(A!U1O$&$pJ}=R~pCeRN6c(u|b zbrRX}N;83?ma50B)uTmHPTVdL3yN7H@-5r}>8ZqhZA??b-i$iVWL8QlL!gH$0$w=4 z3L{c240a-*q+1D-Pfv&a{xEjq@t6g+F{l^V1SeXDD-pf{`UL3ZkWIM`0@JYOy1)8xi@Kg`|hS`SCZ$Xa+wB6 zC-czBn@Dzob4Lgp?^}Y?+RS_wTgI4R5 zM`=|+oc+2gwZzP_pt}P`5Nvlc^jSFuiNoTiF4Y*3v)16LZw{VG%5I3Lxe9PUdSvC9 zY67_FJ#|ng*iI!HO<+FUYo*y7m09beI!sFj5#LvIk`6awXs|I z#g*X&wQMfnn}~FH!DjK>WIuw)|F0)c6RuB(^H1a0IURaXze?qm=P*J@h92~D;7-d+ zY%PeoR>LwVvJ`~m>5vW2jtZox!7FvQQ1aV}nwSvEVo};N7NC-4Fn0jc&BV{msIsrF zul9$Nn^0rk{qU(StBbSqQ4T=zP{R=Z`;kfZfXglkXw*{<7(;V02D+h$q^t@;?k)0Na@i6c1MnC zEf;>OoQXaoP{zz>hZ;rXQr=7=%^0MKN#uV?(;sBZ^5%eUn_}l5T21NIdRb!DMbR&R;qN%ZN248{vFoAy$Z)Nm0 z_-vRh4>Q1`lBp%`PKW6IzcYR4(bHLO`oidbyI4 zvjGMMvI93^Q?;7Iq||W*)vlqC>9}kQbptdDL02xzZI{4d$rXVjJS9M*!nD||aPDb2 zH#8aQ#XwK@c6p^bWZx@T8p&fIrxlw3pgik z1&TXLBU_z8sVd@9bY|jqFj=l@*$V1YEUz(1rB9okIE}shf-5XdXQbLi9$S3I=TW{d znQSGcYFU-@+)ElQF(I3tpPv(9FPqJV25++pU6DdZWFUAKj1hp^LDhPmI|;GU^GR1Z z4n2b%{JYw=yt>@pT%VtxuWzp|*DL8Qh8lyVaJFeBSeu}dp4`&GWI$?Zrv|*)ah?zX zY7Pe#nJP=KvtQb*1FBld=VA+@1aOLqIa-`UJh^JBjYRV>_1yrGWLW=Zz`C-1aWu9h4&j zYrgL#bbb2%?%`qov^(D4J$!z6ICVq2K9k$_@RYQ z!UQ0d_e$Fb#+VjW5;aG#-Jx|S9qA9)@<}3@&(F5wK)=_pxj1Xfb!7{)T5kt$4!h2= z5`wrH3`l$h0;^N)7|tj14M=gi!c^Eiv!f$x0+m2mRJRq81XVdz5HAV}uF}Oqhw}=T zQC5lCWms+Dvr#UY3tTSJLNdH;yVQEgt6Ddo9X6oE{qT+R@;l4c&ouBtHHBR^1^sk> zi(F3GRecH8Kg7Py5#TJTa)#baNePkYk1^YyxrjjBOm&Dnrl=(odns^6It-iAm1rd? zU%+-RmeR;4iZ@9&-%o0$Y4h)d2YH1IC?D8xUZGBjv7pT}L2mM5-YteET(J|oSGjJGGd#|*4keh{|oEhK+Yy@ zQ+VUzwDk~J#sis0Ys3G5P8Hrek zXPL#|Evh9Pi|VEJqfivxi!kw-=|~YnhV@I)=%Cle`WRsXi!reG{Nd^8(DfhQz5n6! z-BG%dFiRGsFSssvhHXRJj5hN5bLSe58^+971%0Ilwn zEFZZ^@>tmolVTT^YC>_oV^V%4K+HInO9Wks3!C1tMw zbMcWBKqV1+Zb$7zB~NJnsg^;cU_j$>S{w%utj1rlVwr0<`GD0vtx z2xSriFeJAUIt+433YS?caSRlLhCzdx=h4F>Y0IZpz>Y~2fX}QEW-^$Rg73;TR7G;` z6tQYU@2h10MZ|!RR4^||%t*u=anfzFZM_yxIT2b#yQqzpGH#(4LHH|1kE(N|ayfK2 z1gwXW>f!0(!D1VY9{}|tOc{gBVPi)0c%LOHIOvrhK&CfXyvrR7No^`6TA12{>m-*z zu1HaO*k#OQ7G|dn{*a|8ktbvg4?794A+n!yITvcbN#kPFOm2OmHOWNx&(kj!*sB+5Nb`G>zP0I34Wz)|lAgFB zPow(EG%*t888b8!YG?kRc4)c_&qQN(a6y$bRa*(5f0tQRi>1Mm>0K6~59+QB9T_zw z&#Fu6eXL0_(<6wep|o|j0FM~c#lBajMnqRQb`k|@=|{HP)2T!7gTTg^3cQ+B%5`0< zxzZd!E>sZ~IYA}%8*xIH7Vt#IsjZc3HM%s^AiAxay1czT|LIR|FSe`As-e40TCY|n zKo@uz`nE1Ir<=lBvl^XS68@3`T(UsGOM#oR`>W1LG3H)O9u^y4b_`hS!Oy{;x(@Ni zoR~TyYFE2f`T+Wz3v1+#Aw%!WswR-*M3i zSsqDOi0Y}7KsAkFe>_TxId#*ilV&SE+&@1)Jl{VbjjN zP9D8ZU`H&iJg*vtjPwQusK+SZocca06#W&9k!!`T@Z5Aw=5k!+|A4qE^g8BYfI370 z_T;j^7+Xk_|A4uv(XuI{Pxx%FKv2ZU8wgt zC!NM-Q8rZup%wtk1wDUwm|p}*@qG}LphHit4_BA*1(Edn_3t|oe!*wz10}cAAi>{+ z9U>}*DU#~e*8bMI-k4{VU&=)UNuDZ&}QH{r%$v`g7W@+MCPm_0`$c<@(+2)!Uo%^{VzD4UbLj&0K*U3DIXz)#NPz zyrd%)SjmjJIqkhP2guxLKGo7_`CIJU&<-Lh`k>lE#Br3GS?7r63Ee}^>!2&r3#TNB zo+lsqga&v6ht=T8LF1-UbQ0D{5zx&MUOPw*6Rnr1ZjOyn7$)SBD8*I^gB%awGI71y zqHO?Drk=*r!_$Mb7Y`5U@IL==cmMd@Pqu8JJR*IdoYU)@>p%JS+YcW;ynp}x!^aOl z|4;u>j%?Mg`l0{yyFYY8|NS4n|Lt%8_PgKy_Lsl1&MXTCGCc8+=B<>or+-OqiZPnJ`LC||8ngwM3 z`t_O!|GtbUkKSaovZsNf)N?0epN#uG{Dj?x$kHtmVZ1i+*2Ek9G8ZOo6FSAU^oumd z_@Cb?rt28r_658nDDR@;5m>OP>@t!b)Derw1rYCXRhQsyVMYzABc_xUny@0$r(~jU zb!1jVPlhobXiI@@B!DZHP`_au$a$jhW8tXl=w!m5Gx5 ze1PJAtlBzEo#iq}n@*Dau+sCe$S{Nrc*V+ULqW9{?-mg+#fzG+It%52WBdiGpQQw) zlZIEaSJgDijzGJq2DnTf(sD}PHyYX`CeD*;A_Gku5%}daYlkXPJnN1kE6{Y1-jIgP ztE9eR!fp(kHGm4GLWo^AJU*YSEBjHhs{C#Ge1Auz=;722AZG?wZ_4ZQ%eJX+-@m($y8R{rqj`ewaecYogf;SYZ}91g$#-S2<%o8Mml?|<B%?-)vfWS-Du1fE{@g;fcUs?Ow4ZbGA ze>@{b^OZQuXxsyGA$MhLvP$j?uS|MxEPtij1oy;CY2~bWo514V@W;|3`)$9PqyEEr zm_HwHhj2U8bp zpPx_1(*()1!C6U=`>{Kn%1R#724sma3@1sGQ2wm2J=v~TKl|qW?VF4Bs`~ih&HH!P zmz%1vvF||SC^!fzC?QuBO)S75OhjYKA(p-(mzWN541!vDWJf)l^(~dIu1I<%uED=B zn;0lQQ@duXoVMeBFW+ret$NyH${Qpr7Z?R1t*OEvnars2XL)suIKfZw6LTj3pFm*0 z2`4UtK7uv|7~`Vt33*FP0U!K)KTPZOM)J_7Pxs>_4O1-2Ml#=if86a(k54=4t#N2o zQC8)<4{xuoE^pu5UR++>++4S-nuNDuI1c;g=g*%$|NQ4ams;WdyLa*i@87=r={J9} z*__AV{_?N>`tI|+#M>GEQxL))#}TqSoVFu|(h?y^jRYM#w87rbzyJPTdisyISGU(^ zhrX9fES)+&eUctpTI4jLsximYsm9nEv7QZ>uj*o?O%y>J5$O}#S+;AWT>!-7%K%y( z3pkb=P&VaI5NV99WL4r=ly#UJHPmd{;#&>r7*MNnJjq1L(oLe zDKw8D6{s~CO)D3(3*8F_r94KZ)v|FU8`5RhEeMt>cS0>p0NqkiIOXl&UQJ8l2Zi6) zC_AWw4f*n+@4*|y0LhevW#0_lc~}^RAV+9H5wP%)CTg6jzjJilozJ9$X!v7bmy4**QZ{Py(bp&-Avoq=0q(_tfKsO9Z z)kvFYt`bt?JLJXGlB%y+v*j<{EdCwu3%kI0P*u+3e|QK#Ip(z&v{@uP!j(pP2xLUa zipu?6M2K9n88WQ6Q^R=`QXJ?zLf+Xnm_4&v5H>;+&y4-HpwS3BX>`L$%q5@$h62d%TF$)fhM)NpK ziI&3|0t@2;tBafn{GQk9Ug`6Sj-hB)%F9xf2Fpkzj?u^iH@dW81e1(zNs^P7=(~;? zB0&&N8%xjtB$o}jL7`+MkePWRW_3Ua?(-b_ae%T`(a?{ zj?@0w?+=d?iP@*qa5(k-5bCxm+tr)fo6D=qZ~yF1KD_(bteR@^89sHV$A_oC`NglK zZ2HZwzq`44b9r%gwp~|sJ+Zq)Ba)1?(pNWEzJlZN>2!R0+IjBMa7rC}`48az}^2MSBFsZCp{dQh2oHo-xGvT_#W9x9xY zM)1W6t<#Ex#>doR;|bRrRYe>!49sTkyi>(DdS;nLF9PYnlF1oHl}gY{sm>T20b<}m zF6II>wOQ$ngX%mmrG%(8i*n8KhxAfK@qO)A+Vu)ZP7oM}N?qVKk=)sAiGj~gp&%$D z(9zebHicCkv}hojM(upGlDdM@RXoNSc=P7ZvRy#IjM#ypAmUm zV9E1mC!wF@DCS5lzjX|nmU$^*x#7i;?e*(55&nP4iGd#1%X4K+$HD2dF`t;>B;nQ8 zY?F~J_&!<1)U*`-zQF7hJPlYUCX7#;tN>0>24|RRHV5~_@bNTXb#p&CVlF7CR?>Oe zfp=C^qzqzr#^neei};m*fL{CUra9lNE-%(CLhvw8y=3KzJrK!Qvqp-<#RkwZQzm9(#{-x_`)}(4)Y9;UHw>g(&9vx|#q8o&GQyXU8aT!8|n zi4^71te|4wbMy_y0FvSV@WXvofTj1-kGB`A1}$3%m3OD(P*(-5bS+^Qqc_)p=&6<0+gvy_Y!p%D)>br>#XpT5?{n|nl$u* z%P~(5p9=p?%6v^3yGwIQE(oZr?&G4x=hT=QC`=2T^pNN3xvI&V<%lJ{%fjMLsXJ7@ z*O)*OPE=?sb2i?V-uBUB%SaE)EZ2}C^B#KE!bvsbrx=eXwCQqJGqpKnYg^>Cy<$Wa zcoS8S9^6ra7gYX+G8&H4Q8gtX^`B&lr~*zdWjImR=;m{z_A^#Dg|A<)iSU2EOI*Ne z@OS1Carq0W;1VQkZZ#2Fqs~y5#F|q3+gxyn3fdy|P8*YIy2>ak!vknU1?W((8k?r7JR7F@D80;-BvTAFUT#*b_s#j{Ql7=j^Y!!d8ZS3XYX}Md zsxGCm*{<8ox+xhpS8a83eJ)Mp84_f(X-kshXL%P>DlM9Zz_3E)!UkJMu5${&6+kFK zMH>}Fs!JgK%5HFMcy!>Ivf%Y%UBCuWN0dY%B8o{$RXq{d*;nv1Sgls_{inyri|yI% zn>WY9KFm;Alb|U{QKHCXs8)lnVV3^{5)yJ~B&&c(i8&sPqU7zW2&r^APHAc_^wMCe zY`Sie%y$AEzj)pq?;f7_hto8tlO#^LF21OncD+4&bNl9_G|o3yl9&O~p&&rbp6|0X zZu|Yi+KKT{C39iTm(PRGBzyMNs6j)%i(rtHG$bdoeb^z!%MyjP8M=EXo|Y79%k z!O*VDP&6D|Qri@F4~NqE%kx1l&c$|RsUVt2iJ7T-sXPWOf2EZ{&ETbo!MrY-%p0VQ zWuTC#&LOD+liiCH0i8v{K-cw|^Ge|VB3_d+86)PDgZtOyT(?}`X!3^7D~vjtY*_z- zmNG9Iyo(|H(*N*}H=F8fJB< z2ziLs8X@*Xf{ z(}|n~?bo0{M<~+EiZH#7M_&`+|8EGJFMq&LKRr5ojP`q}gOhEIZLlx2l(Ruocn;iY zx*^s^<-f0hK8-6w^dY@==^>sXcYuX~jv6%jr8{tx^aKo(dkvF8B01zN4&n<;lqrz3 z+{omIDv>)B10Z)8K~eV_6ZGvsE~6hI5FonV1YrPN;8erXLAr%z90SD0q!T=0L*;FJ zb9>QsgP}wxtI|yk_dVDq^nE{%L$j*Sw=2notEQGbCZXZte7jv^4J0Y>eA5uc3b8k= z6pI>4%?KaBkt3C+Si};l(nw}B`-VO0n9y~3Mac#Q1hCa${EQmEG=NTQaFtzOa6+ej zfu~NL1{zAJ1nOqP#}50^er)Sj^6>F^kmgTYKm!R%bS83;&^N$&lP+E@r6fB?&z%o? z?Qya-od|F$g0EZYr5<+&`SvhLcC4iZJe`L9-A?+jo^Dj*l#WBM>gw{FwA42@H$VC2 zn~RH!)v7I|Kbs~wR4FX*ph`_pme0HWsq2pY&^GS=>3IxOcii3G-+#Wl>-z~}&s8ng zSX#!%-P7@K#Q9W?4V>S>A0RtZx~s@WL^{)1QfVmxzk!bW=ewu2snhM1+=^!_Fh=E{ z_rpO>Z&?>)$3ZT}J0<7AZa&g2&!)_dmVQph~Al?s#>u$)+&qdW~7n6G9ffZq8l+IJt^U zc1g~hM;PT`=!Bl@b(V}O49>sLFdRNR*%wA7Rw*~Z{ymabXfZbh#uZeGx^2VU#j%sS zZko7*;czHvdq+AX#};Ut%c58cIH1@f*EIzX*)G^Tg&C#1FiZ;`Dd`92M~*NvdqXgy*}DDGcE|H&Lc45rzarE`MBB<02Tmeyec_C zh)Dsc6rDhu7!Z3XEe$!b@_+Y;E(r_X&So#I4_xmh=@nI*Ch z^5JU_bf|;`Y56v*?Pj%ZSICP6nT1G0^XODW7-2|0Jy7RLTLwJ?ZG^f$1mY8I2~@2< z$5!ds^1}^$m?VldZJPjX=qL?3 zMtIjn+SD0+i$JRlRga;GfFY~gGqsa)hk&CP{NjvDeUfQn$B>5>w7kD^H z%^+=m8L;m4ItH(sW_xk*=Iy)dtLt^!O6-!3ngJSzeAr7;>z{V}{q89Jt2BJjSe?_| zgC9p+0y%+@jt1DN;8bpw5`eG~+*tBi9H!4D!e;4WU1(PU1ff>K)J7n%pu1DsdQ4hX{OFp|A5v2X;q za4j*%5%_J&L6pg3N}malTH$2|gpTg@c1`xH4lh6Vyoa3{Gbz|jnI%hWxY(>Yt~xmd zLS39e@~^q@H4*-ErbA8|^+}5ZS+DF_Y-Q7iTf=K(e`}niz}p0RqHYk+Omk`0k&kNB z$IT~gW<0kz$gE^lEQp(EGbARrfjVAMiC(KDLKQ4^Omd1@q{OzW7g|Uz!ZJ-|o@v0A z=R;&00Q3(X;>af!!X*rVu*A`WN8JlfM^Vy# zG-$c52K2g8vI_!YRy#IwB%C(9S~{~aP~~E*b|BvqII=1MhXIw|C$b@6gv;&(THVuB zdTkVf%GMc$kVYMal?4owfte?-3+5q`rN|vGr6GLY_s1R{InVn&d|7?nNrMUNB{EHt z1p)~3U$3fg>-F~f=FQEUH<#y^7Z+y}`ZhpFmCr73{rvp&>C+Dn4|kGKo7Jl8$KCM| zb`yb-@`Fv8+|YF+E%zdfEh!JJ974(vK!G##QnaipY)l~=hg)veMh*J0QWMnm>D1rd zKUUsdpKlzYJQS3V3LjtS72I9Vc`h5M7vr@i3yHdN@JcjU=r@j1bLhLXGk!6Cz``=X z0Rvd*k&?EvoK>PjgZecZ;8A3 zlHF*=8Jbh+wIh5@g#S_*Q9HDx3@cd88~aS4&69zsDdfKsey9C~xwQVzto>iDeOY8v zaTqW)-0&0y#;=61lKUaEm}D=IFolL@EF8v4anYgA+@m=ydMkzlCtYo;Wz4`3vgwy7nQOAwc!xn0*-_F?uqInocL=exRk`{w%cQetQc zG|9TDD>^SppehWJ;5^=i{kC+V@qVIG0#9Hcuub))lTRTpSLv2sR7($sac zYBp!v%~lGC%hhUgadE{%m5WtKd)g2EJk8P#c71pE@NhUDimHZ8T7=^(5ry>OfjTx! z4>(kVeXe9v`D5Fz*#5HTrhuySfHiN@8JH6^j&d#5FkNotQw?LcKcvq%kMsMR?b%k| z1Op3c^hb!2vz_A!or^$|6LnT%J7v<#wNTRu$!g;`X_m=oO)?y4I`6rJ%fwjiTIe$Y z89eo+*I|E1DpJGgXrz-R^Cp*hmj1MA6K^z4SV-QCYD9?}P6_5#8H$d3Hp+Wapf4@O zf`W@7085nwIBh{dCoN0~L(~=?&q7DTDn6%mu>#y+rOm2dWa z?^#S{xB#rNfzjle1zy>JtXqF^zU{i9Vk=2MnFK9@E?>3 z!Fn8^5kg7%M&L?v#s!@!Z$ipzB9v_}gU3YhPx2KK)1Y^wz@zNNMC+r20 z4x_d@7#nLzRH!*qRZ?-lkzk3Qo02L>7X|#YKz^M7)(NMsZ_hWU?r7!kBsETRVPlO| z?WTQm>7X1ic@+>(D$7Z-3JKo$6uit5=B-)uLkXlTnS%$q$V2wqr~@tQ54fwt(<(y2 z4&S8XRW_1jf1?Q=7evu8>bH!-iP#+|&;rS6Q(S;B9EXH#BB6UqB#p>@US6^3KqX~W zaKnTYF-mbdP7i^eS8@rWPs`4dr5IR=@`2HDsTKeezM~vFZp1{^QP8D3c^L(j@FjiL z4%WF)#C7u!D41^nHnyH{<-`}qoP^SH#C0vQkete*|4o@vl3w(&42SZDI^0=Jh5!Kv zHx_P26C~nhO5W=9_4Vr?NQCRP>3cJdujk^QDJAOQk*3tD`|whi-KY7xINl}mQ;Om` zRZlYXl-7AjP;Djkt;wFLh4pa0vI8v1_^8; z+I2FTg3rY~iFc~@&+uOW-EP$Wz=2@o`|p#RYVXnpaSibMwTi5oK}`J*w)MkTfszw398I2LoHx9UQ80mrGGEL3t$_Xf0teX&dk?dzkgu{3!K)IjM3+=d}=sASeFs(w1 zD5ndn=8SzuZF^mxDCNOxUVO?5mTk#u+lo}YKfC2~yT%+|z>z4V(AC>mdk zXenr65AHU?v6uY1-Wot-2G6D^P!pVTah$`$5Np;>VVc9S>m%uzl| zH=b?E_1RgBgLNc;qXz+JoUFDeC#?k~s&yo3pl&CH&1y^*H2vUWPD-;%+2j@x_rDc|)WhmHg zRCP4hf_%m>5vNl`(G>Cm@;XuxqP_|+Gr>-pFf>|R zr8it`t}CMyC#i*v$~cS!6(}_*m02Ark9qAPDW@7S^riCoQ!Dq-n&n~5{!1t!J$Hs_ z;ow{1&%ujw%P8q}NBIBT5jyvJBK`$(B9JIVqQNsRYC4lPu`8reur)`Zj*9*s<(#3q zVT#NApM71-Y~Pe#MkQ&{*H+?0)+p6niB7CQrzgPH^?|`&}XjX z1N8mCgrU-8h|Y6RgD=!8rK_b(kvx65UAa`zQr(PGly|$nIP)-Ks;Z_27d(xC0#kB@ zO(DG8(Q0v)Sx~_Rtq*nsCh6Fjd+P*bjY_^G8a_yx8Pztqw3W+rH&yXYi;OlB0rNwl zs{o>QD4R!EYEX_4M~9d)pyJ2_Uti2YHI2GQOu#-6IG2vSl=6Vc>w>zz>TwpERs+BPuhbI>T^ zwYKOPv+nDY>W9*tJ?>{_YSiAq1ujxpcMlrMga$bpW_NbnaIQQg@2?3baK zJ|8K!MwOX@iQFpJ{%|f$UAiXYHbm)1edejxFRrA`M&z*7Di> zEk{Ul=+RIIIKl_04q+iPh1pj%e2!t)M}$8Oa(lp5B$;9UhvAu-okpr<1&OH$d}jzuR}G4i`~sk?3_42);yF9_5|F-1UR} z!jKH6LcqN`$b`;_j2I!Fw7WuOLQy@r4)RHX)P=}DUqJ;(K`trLus%uj2d)a9d=%kRMS_ez#g9>A zJ>N_zYA2ew=q;72PlW_46)HFBL}5h>cSiKnMU=i#Pm`@j4gQ0g|CV5M>R1etCfQnQvBzBIh=aCOJ?}Dwp_{d{xWof9WagGxEIt5co znFLcEZ87d4kqqOIJ!l9{tg3qGd$K7|n(IeXGjhj6)pewj#vC=yqheEa*-Ebioy*a1 zCx4u#t|dUyD+i5(R|^m97^hP&Uqd@gT|Z64mZCC9fL2Nhl%E(Ri3MOKq&0-QFAM;K zg&&ZIHxKyi5HXP>Z`-!6>#8cNvOXS8lAJhUHY#)=Dg62A`FJ=?!^kCHWR~%Bk_=27 zCf$bR6=+zfPOtJ4{`vW-puh^6=7hj1r_o?k8*CoT;)Kh^U9Z--^-`!IFErO>d3}9; zd3~;VsRUZMPjE}d<(9@-(vl<&`XK|Ks$|M#eXI037Zk)rr5Lq|;Mq&utD35)s1}Ax zgOZ54Efk^24WY6`@kv7Rg?YLEwYkgec?yeT7QR>y%74mzt{6+K#+Vy&4VvPM+M50( zYD7cxdOlO?8>P7!+I~PAfja}E#&N9cCVLi^EE({o(W>(iDn1~GIdqMx;jV`$R|q0T zoV}|Nhg@#C4N!K>L6vlficv;mRvR$c6w1?VIj=`deomwQ?E1%7Kk%U>W<*WFs?h~XdEWaI3&RbbeQ9`=q7BKF#W<0EOS&xs&JnNX*y{w&_Z*P@ zC0G{n*A#j#uAEBEX>y}H3wy13@dobG$kh$)8bgHtU*y9h8`6H0XonVfLbJL zi$S7g*dM#qdb`=2bwe2D)E}jjE7kW^Qe`SDs%*~`2!bX{7()hAz#tKlGIiZbDKF{j z@%ia=JbnE55rUS5aA2qK-J9ha01f^x<5)nEwagjlnkX`08k`SfHJgnEmoB}Vo)lH#MsFiB+styi&xJb|sx%J%B zQ}N6eQVa#$iX11vGEjxOq4GA4A&wCEc3hR~M&po5vcf1RXHo?Gyst$ zH&_XDPZ-hLYVC{d3C|(+Rtg2Oxzi>M(%~ue3%^76kNUJ|7A9maWyL9H(9Bb0#u3i` z`t_O!e{`uZ0#>3B5iF>bFPKS)nOeL2=!09#>lacwy|D^R{>3vY^F;7#exk(Bbi7(r2|I60jsYC{$E}!m1pPw7!y8 zB=@vU_3r&U7|rib<6+!x*8Kn@atqJP*d6;m#9?G8m2M0xQ?!PVqsD9#8bi{_9gfFD z*p8e8Nr&LK%t1cO>uymFJLcyFE(D*JNr`ku={s;M7c;oB!zdzJPA~L#qc7Q zt&f6^LyOX?F|yO0q5`sXCaG}IYzM5g|4g#gi9u0RiO8{%rVsQmnWe32>t@z*C|=B* zx?Ue6DY@K>(jf;dFOMVAJhq&(;yC3k3dO;qfZ*CY5zXYlqj3Q;eQGA*SqF2`NV9q( zturXb**vVsgBO)QR=~Jhs<^D`lM|rMZTYCdsNRP7wH5N~*J~pDaec`x+Vw2k=;ouv zHI-Ao_HtS_g7M{%TYAK=QfkV&GRstCSxK@bB3VA@^pnVu+!Lsfq3-D!ARHalHBEUU zF945M9b`qpB^3`L2U`nvj%V$sM>GMW= z8|rRxACmm?q14%&5~F~ddq)a2q7{g81-;QF2TGn$aA``N4m@p!(a$Qr&1O1=EV`{+ zh(*8OWP{VlA0v{(a-*sHq&aaV_NA4C;F+%)6!Oigy!2NR)g+P~4k!6{QZ{`2@aEz1 z=zRaYKVh&5V?tCQj{4DZ;^&Zk45h_WvC0Xy5NaR=VCwGq08-;H%I5;D4eSF+a-<~eFMgQj>O+;c|jE%xtA(vzURBulqC$*X!FTIW;tQ4 zkgRgp3CzJxoi<&du7)Tzz?7y$9ksH=RYtGu6_w`mp2P9&aEnHxAJt`#<(HheU`N#s zn&Vh?tXlX~5I`{V!S*LRE;P;8uh&HQW9$f1@z=j9?sw*PEdfhHhcb*@wxO{vs!KNK zy{}preJosV?XnH0eUYHbLQ-1%1Bq%?jKf8zTS6F=Iu-CW*=03lfn=>wbFrC~R@jaZ z@5hpx4J;Hr)Ez=G^O!LK96@+WWcFfPxzH*x-U4WoohC@%VHhW9%YkCx7z?WG-0Cud zhC8ODpawObLuNwjW2&-JAOijX+zGVJBq@4YI5{}(g6p;f5I7+$hpN0sHMP&(5gM1M z)`U!isRAT|n^3?wxP_v^2q3blVJ@tifMriD-O_G%l9o&2nq-urKaEo_8LGx^2>aEE zFa}pO(UxJDbcH0Thz*CrfDNoR<^YWT3|`0m{e4L@VKoVouJ=UjjHI=?tR<0l@*Y6p zF4U;Lj;%n`h2u`7ybFJv)2&aX>DR%_})8fdAu)$QUxZ3(%9qJ6-qc z{baeI4olOllz$W_DT|i53F`EN&4^`1$w1wddUevJ6_hU1{bN+_rlX9JoEZ71=rB=8 zP~qL>L5nSH0FEkBg9fWaLv3Wyt-E=Oh4W9()7aUD4g&itW!V0qVK7PzD>cjfec=mM2wdm2P?~%kZ zkeytS@hf<4J>~U1S0HFjAPi@xU>R)v~|nfMpN#R$9%ZJy+ZrIM%pY>(W=wTm=eV=o`XOI~@{OB)Bo zjHHluwVJyrI^O|0k9=Pz-_|8wlX4_|-ye@h`B!ZVm<$JvSolds$BfsQwyDq0w$G1G zHHN6Zf$db;${CwOaXi7@Vjh~N#)?snGL25e|(HC;6DGwpMl4ag;x|+YRQT zwRF`Is^?Mu&34^xx9jaz(qpw=%R!~8COggrT$Fj>?N_>NScS|6Z!7?6IJhi|Z1xcn zAx#hyrw10w7b$w=|9n|)&(9z3@31XnAtoPI&Q-HsN%((ydcM58!Yb4zcy7#7S(U>8 zNjyrrVliA0pqc1Auj5DM@XPyA^}#DmCXsp(vmYnP6^XCIF?^t51O2=(m81l@lJZMf z=FT`vlGY*(rAkU45|v6HsW^KWjLe$eC=Ksww5Jq1rAthVx^7fdDi)#z#G7I!_EYsP z@}aKk#^t4Z-Vv&UF&&+vx>RH&E=6a!XICLbL+*Uk(oM0iMzLNosrG|XlY`0gpVzO~ zMEE29a-Z(%@9#%cpe(Yj3RYofprJf2(n=|phj#I)3EDStYk(VnA=awLw;}|L3UFpE ziWg7X7#E!~E9Ggi=(AZ66F@vf5CLpcDZcV#)2&d~7`g~t+#XbGQt~U+MBPNcCwbt9 z5iJq1nQFEgRd$QWJs%a2AopKc*#i0+n21_G#Yvg??2Em!#u#=>;PYaX{-yM##PQRq zqvNY16!U~F<~(O{{)~X1vpcKCo}>qWI-sQ0t>acIa*3MN9y5IcbbGcPk#cOF^|-ve zPl|9?7R=mZ(kUGtLZyYG^?=ux#%Qxy8Tcw!$HRCSsMo6%7pYZ^6IJ>0RMmr7 zP>%9r+LGy{4-zw_L}1(Z-Fm$;WZg-Ax?H#C=V$NV-b&7^Yj=0IpN9Tyv%0$6Twk3_ z3QXu&2I!j?U3S&b=6Us*zwnnfpabwDTK|mDk_bk#ld4uv(k_geAlel7vx?XL! zkB?7XH(p*{kC;ouX6>vralQ-4E%% zDC?+x)SPE(C4eLJrIH|w`irL7`YactiY-i6c@->Kn0JdMEEF3?m=eNEj=uG-+Ij3F}nvy7OiejX>6yc<^o6}6vdpW z8fxswT`T%@}EtWnNps*F^YZ{{rpZaq0ro2d-E$S$=WN zjuiaQOD-nJpi4U)QF2hjKEj4<#$<_TzDJVyVnwnbB1ue8y zUl=$_h;dL)d4?whrB5UfuoZ>;3xIgAo^ZN)Sxn?qdLXbEHcIhEbCMKop_~YX_!CmW zI-9E5!#D(_d1gY+_GWS%RkYcl6;T{yK{N&|f%LStbwfEcX?dq%=;h~|W{sA@0U5Kx zY;tYNdj9kfxR{%}QG#Mq*S>5@!bJ{;L)Z0BPfzmAwuQrR*LCtz(gp5!&!zX0pX6}% zyPeb-t5qWnB4wuQ&3KbkOZHXC`V?Pj%EK_PFmk^Y$rT-_}b!s%HAG}THze*tGL2USm4 za~d^aCkX(d-r*k?S>Oj?S6o@%$p!Vtpx(A#ZKMborkR*bCF=XKF0S9akuw7nQriT1 z13S|lLKl;1+38xB+kMaW4_paJnPHl#BB#Qe>LBP;IVNSJL8`S_T|MB{W}-$Z6o-*OwJgEPT5&7!Tp2W zmckMBBiSloYs$Vg7y{^@NW|rRW$622AN&-E}aMD2Nx=6t~ zg`^D_*aqcJ(QCAD3+Iney~4~nFu{9|Cf&6h9v$qkX#`mf))SFz2KfcHK!`6!yA!pW z`Okq`2fAb&qk;hoTKn3pt-RoFTy``v!kf}$cX%ONM-j8`jLLXB)3hV5o8pS`K8!`wk`6h1ylGTdU)#YG&kkaE_f z--Ql1daO>h2#||tzGw*ol6?2g#}8LmmsREa)A8y4PJ*%=vwZ)}&DHhQRubq6qtS+q zAW$ka%k-=aAbm&HCYdVJB#Fzwc%3tfqz*#59rPh(Wv`-a6sX%zVZAw9t=HY@ST`%s z?VUe$LqS_d$(?ddchAq%AHSGjl+vr41(l9;u8{9=!h(u?vn%t@LYzoTFebaq1l9eY zrO;9$+F3K9;-iw9N^_Wwza=sS+SPc+2$eRh10t2o3s!WN%6xehFE=@hD@<1B2`YU+ zDaQcgFdcSMp;vjr>WZdQ9DFE8h;39bEgpXrrPJXj*CO6{J@ynem1_^qMCe>5Ae(Gu zMMk5ENU<)9221{vx~z=;H3PmT!XH0khRTstnqj)~aRG}m=qK&EtbK9ZQ-Nq$ji#Ie zDOVG-Lm@gJuEesmuv6{pJjpC|(*_Xf43J_`5!;Q{kJ{Flk4E-JSPjt!RW-4&QL{*k zaxfDkM_DywQ_W2c*E+Rs4|7xqD2H<;idOEWIHS{!f|@lHW{iu$m z^P~p(`gOvJXXEJ}mN}61(5b>#{kwRx|-Ks5T@bvt8RUfc2 zM8E8{3m*shhn1WeX(**n1CS)_b~~FvfYg_bh`f@?4ZbW6dXwIhW7+HYMjp*EI}cAX1Q>c*RP6lyIo&jUR_>ZwoTO?_q(V2Y3we}*H;(Y zo9nX=@2)qSM(U4>%GSyyFiN38`bTHuiESyW3R}<^S}b=Z^^P`$l zs#Z*_^tBTd0HjZD@gcVxs)A|bn0lh!b2XLvsN3$VsMHeBmqqe&Og-LIx6L{DCv?2 z+6)oJApI&DInU$&FZSM~NwO@x4!gsfe29q5tg5ai8chNu1_7Xf1{y_R!9l}93z|s_ z{c}oYgk*p~b^~4LnldANdegmE-#O<#&jd)Wk&6g4i&pYe+&0f`qQH!^&X;8F?q`SBm)% zZm~eV3(#8F4l&r-UvOIk882N9#H|>Jf8 z-PQSWynfBepuW#ud|-_;o-p!W&$R4 zVk3e890>8bGsH4+tyFnN$3_}jKA%tapZ2@G-53v#hkx?(fBeN4@9b=xp2rXGzdOCW zfAcVW{neNM_~-xV>0x+$+*zlMi`&8TT@Df~Eg~oL_*2((_JK2f<*m0{mbuDFqiKoM zR>SQ}vCLMsdQuUS@=ngnbi3^jd+TwZ-h6qvoUPN^4|}`0rg?{tZ=)>402Y^Vgr(MEKdR z2%jF;uf8Z$E)LZr+gB+s?*)FNC=B#eEDWuyB>5e8OqEZ2r&v^QndYdTTx;UT2$IRn zdz^a0XAjGdf?2`VB5DIXIp zQ!EMDDWeTtApN~)%DPz;-cfXwj1hzFbefNypnEtaXUKnj#EU)T9M0;#%#pp3P`ll=MYFs|2| z_14D&q(?0)K;PBFUmmib$N`*l@&Z|h7-{*4%^VF8iQ1jX1qb($a&4W3K-h+WUb#L! zJ%0J_-J7S!meYmP`Ni&{hvWY3+sCiJ{)zqXi#JE>rKj-(B~>o6?A$aAwf64Sya2Hl zS798j1q{2cTOqx@Hi22eE)(R(GVhr#5|pIo9^e|6$7v3j3-k06?TMiR=icnWfr^5E zBfwVEo3~$7m2}#mbPzhP$lh`%!%7-SV_$ZHeiK{@!nKy#HDb2VbpoAvIa()xm+Z?5 zU`Z*3K)NHTd!76$h2)C0qr{DyxzA6kr2yISM5`HrAY-)qJ z5sAFP&T&=EJP5UWM>}qMWnqc!cw8_S6ANG zMEH4kgl`_xy9Z$@v5L=RaBBgeRTk>d`i!&YU4>+0gqAu?+PGGJ^Jb|}N8KD0g|H&g zEBNnNH0of^iv7=$W+b9jJ4v{U(#8h#F?&i$o38pB9_4&~K~T=Cv_NTj z3d|fpF~~-l6C_Dn(E=|>W)0!nUNnzWp*P_;O%~#<6IzyJ9Vt~&mkPI9`@aFKPP{_+E-k< z;Mcs2x6AeXQdGnTxJBNs{?f#u?;f8Xc0*rqu?uHn_(oQGEOhsomTYIpE|8R(weCzg z)vKR#*EIdmQR@!8mEB=@`}XYvtbZS@xmp5LqwQvSczXER*S}?%^5;MMjs3o*!nPm! zhjF|VDw6?40$*fE+F^kj?90wF1q>u*j^hnR9HG!49;rawM>!tW_KTV=^7atq!;bcP zyGarVGga;pmu2d^0fvg~KCRCVT>9RwqaAs_b2Gp`T=VfVVLXXp?TTI#AAwNT4ITAm z0GOfLL*ut~Agw?pUQ@wiDDH6TRyFbHT}cs=U^j&Vk#$<^1g*OhL^fs&v*SFD#HWD* z#OH!35KH)`5m$%gd-_x`&q4YXqNUul7kxO;qr}~i<3LT|bz|QG^pfgK$lpgY^cr<| zcG&b%e$L5~N~KFKLe1C_(3ct`((+2Be@%p+c@`Y{`A`42{Ki`>WD*-T%%SePLmG-P zhi#JosgWoLCs5+vqh3Y|lN~P*2X5j1isY@nZPqzaF9Yies4}ig-}jIavB2JRj>3u< zds#JZpN+y8)UcWAlnph&Bs>H8Xl%tsXl)Q{*Y6JSPp3{c&37P7L(wR-Phx@~v{8Q~ zOBO>M`OFPv6lw<42pB2>J!o$`wOwCTP>BJ7qM1bnpRIqg_d};Yw5f8&C?U(MMZjAm#X7N~ICF-rPz00Y^Ai-ND(om4yzV|}5JPREr(FDS!oxm30plAgXw!&1} zw8Dmg-`V{xu4g~&YDmI>QC7WnU(Hr0Sfv5pMyO`;j+-{}=#go5fS!(ax7&4@F-ynt z7bE74AZx$VMgc2Pl-Wg^p@0s80hfslODm2>u`zEw>n!24DJ8(+3OTs+HES^r2w01% zBlT4N#7u;Kx@<0!G%c(-XP#Rl0XBi|Xoadk5QxB6snUbFwyj=AN!o8_SD35eu)1N4 zAy8e$yG8}=wS&y064!);)leoCk2Y>ZLGs+I#@cHl{9N0?Im1j!JtDw%PFY$Y@^rE0=WH#t%ku=(=x8!dHQxrs8<0nXozw=1bZ!!Chw zs0X?N4B1qHj3~u02)~uv?N+MZmFnY=1dykuC>53ycUqy+IchP1b()+8tUbuswienJ z7IM|Qc`jiC;brV^GC?}7ie9pr1*NQjU|0k%jjpyljfQoQ3IVog6P$*zy;bedF>CwM zSTx-o4(Idud>ZYXznpG%fQS8Vv;+&^UXUs2Je()!cuzlk|3hq^Ed_qz-}*Oi9-kgb z6^C9L$9vt-Cyq%wYm|pwPXJUa8!p;q*h?$}IxArc!o7=Cv=RuWF8bl!gWU*+!|v^y zr>}qd)#Jk7Y&$<#5)nhAqTDNJxkZSU?$KeZ&r%{1W898p%qZ>cLmQX)%M?yi0wTdDNqYBwmq z%_u`g^}*V^myH>(aZqL*o1KQhLF~fH3sxeO(aftS?`tCbj1%Ek@6y-roWL7iIVjwd z-`{zqT&5|-;Wp-+)`Hb-g?w{Mc%TqqD(LJfJnn}KU%E0+ekn7h3sKHr#t}Svz*?aE zQE00`DYc|&3pJ1-Q^^5Wr12buV~9y7l1;NXYNL{B$O;&|D=)^qlYmU)s#=Y&DaBWq zI7KJDrj~#_K#IT6RL*n1iAce9Nu*kT+%?TK%`zRN2}2gsOM+Vswl#$s(3nJda_VKX zV6ByKgxJdoiM}R?Z^n2!o#YEHpoCB~`EKyzWUNQ#T;CTccvTPATDOyMi4h%{g^>$9 z;rR#!!U=3b8`|OFF`Uk0zrP$GmgDhyo0ng<`531wogq+KNzNe4y z2o!$OA|j>ZBO6ByFFak<>s^eJ1*w3;yhfLRQtEyN;$_vwxR!f~3%uBZVn5CH8tW8? z{eDf0*2Atkut(M}RME@p9wGys-30w^w@fn!8@$k!DuX(YD@j}z)=yL{gC@HOyG|OX zoRebCPY9Z06O4P642i<&OOmvAF8Tf-vLg>s90mc`qznZtIJ5nVv4sdyuW0$R4IEv#}VYkXv|JOOwZBF%i%(h(U0#ui0qPqFQ!9rxt7HLVtb+iTOo!pD; zAxHZH)-u>%un~fcwRrXfl&@jqUNv2hp(s}rm=aPby4^&dCE8g;AIsFduc~CV9woxf z5%X5f2t3i?cO)u>suKrU^-UD&KDFD(xjx(W;ZAmOl9$)amI`ZGN~QVNwyw0(&Tn+njvpLaz+YU78Ehh5xCUEfeXU~C`VZpjorreBzRq- zKN#}TK@O_qmPl~Ml3$X0eAtm+vA%ut7CTM)QJgMkNUU9N$V~Z28%4konbnZa&d;a! z&G&X%c0CvfeZPD2^p@c}NTa|G@!h*Ohr{7^8=s%wPt#cN>-jP*mZv&s^el&6^XB2r zI8CmsU;pIG-~8FnK0Lp-X3=saR$CtBL-3Ln5}mcE9hbb(X46sb8L?38+xj!h_0Z>> zrl#p>w1i7R`xGr)X-@B&r1TLAH8ky*?W|xv!9*n$&BFX$G&zuN5znHOABw#{FDx#Z zo$Mxd9H>ev7Nbskk^NIL3)VVL<9L5qgrTg7iv?c2-Erq68aPO|uev{}{k8k2A)HD} za!@LJZUZSTM;TQw$G;_sP0O$EphR9|x{`71YqjVQ1dAh>AkZnBII^WQ zJ}uQOO-t*$t&HC=F_J*!%u@nvPTn0>3}NR{xSb@G%IZr<*5}tO_?iem%XaW^SpL-? zrJw(l>bQCU^M9{y+e+6`V~2!B8NUERpj>^~l%L?EMB!jx_~FP9h254o5j z6lWzj$&vXbRH^f{Ahbx+?j&kR#g|0b8EvzPc379i!dgFc_9@nFST6QtLMEjynPmVV zp6!m)7eU1yNfYqA{=1yg_ zfQ}W$=saV$>)6oZ3amBCwP2Koj@DZ?+U+_rPFC6I+)Hjdf=bRyf?tq34gHk*gH}~z zJv}r?TEF7ty0RNuu0PQ!UT@R&HaFV~BGa^JVt%gsVAD0Dtt z&R5XP(bvu|&+o@^vXD7WOHYMtq>)I~bQUvtoo`o2wawE7+sw<^uE^tI_vO2HyeXEq zZyyi)y|urX9WU2)zPveF>*;$)p3atnun5M5jrbDFOr|6Ec^jk}4C`Xc5GD&(^ zSJ~+aFb8e|_QZtwuLMs*ogKW`ykT;;%*P>nAYfSzSz9Hg^)vYiTue%Yy>RdVoNV|+ zP6+o?6*nbqG?8F4V%|;ARyC>>yc$g*6Je~IV_fKlZov2_7?Ub4nNcv>ZrgcTrCSdp zWp~@XPPF04n&wryYS|~T4_9i9zJ}b_MEE)Wtbh2s%OCv?$+uev zC^y*u){FgSNXik(Jy;VJ$sZactkg>MS-zb>4~T3exhXP{(f?sal@=Evpw+C?ElU^? zk%qV!mg`pW=1NXtcnXqygJ+ZVCCl;*9Vr4PS?}kVA|{nFx(5{HnZr|NY-C6jnvbTBgROcHeyc_1m9*{gqmg-EOd#y?wK9VAEk0&2l{M zA*Yh(E@X7BpmjjBH3JlC-y*3UOk|MUjUhD}opC7eOR^eexsBY&HEe+?q3P;YSf(am z$>};(!yvTq7--LG(KV4S@-*CTbw}ummWN1PFd|M1n3F*UBx+l!VoNOu(T(orWD6z% zUD%Y}SsF@2NzN{TTBRJlF#b2lpuvOwlvA&7|73ro&?{?6dp{kd9*)wpe+|pmI!0P)C)O=YcZzEI&kCKteWN^ z8`8xNR^5_sTxigM)^b6jN=fGKm?Nzf8jcMIr-K1N5*ovzJFvH8b!vybXlcUlNVs`b zL{po)k?wH-Tqu?i1T0t?${fS;(`~$2dvs^+XW`-jH3ZEgQ=3C`W|}c!>tn%}sMki3 zW?I(4`pRLjGuO~i&qFO0iHqBY4Z{wiXqGe9 zt4vD(MPB9%!l~UP6AkFaEFkQ@^7drG*vRF;{xAWZL^aV|!6r>RSc|?$@>Zg@su0kw z$v2Qpx_czYGH&q|7@`ShR0T&dzhun}@@|B<+6ahmQia02MlE>C0RmE+*U8R|s1EpP zI$|b$H}xh>oR5MRn(kJQ}7Dvu=F&`btJB2br{dAHeT7ulK7 zZvyWhYA~omGLCBkmo+(}AP34Kzn+`)*Sq&WHs>V5{_D?cBK+LuqUo!*^^V`{G_x#Q z$bHRx^6QG^@0jt{h2H%yLUkMW^kD!6&(Z@NbTE^N^nU;-ttA0}8gJOO$i*`<;(@JE zqSp5n;>lSlyk_KmMr0<9NbP@?CC=BYoDTCRDSX9iXe z(B855YIzV{=5F6M&4=grmS&E};pxreVHge%dn<=53)&0WaEGC9di(bXqKF;$yv(~{ z$dYMBLp4)Pb@E$Jwl1?5Kol&Id{@E6)ESyMIK2}zTHS8g(`8A@&}c_sJ0X|pDmBVA z9ktvGLC&5UY}gq}6~;xsiD0W=ijt)bl_k%z#&eM1WUK3Fs!3)dxauN1!p;dki;Y89 z#JbUQY8^{8lE+{JiU#t-Bs~L~e7qX0+8jBfn8s69+eOV6RFOsktake&s6-&n;GAKk ziU9%M%8_tF(E)b2O!IV{SJfJ}u2To*a@9|*Vg*@?k7~u_c=Ln}!?mI3>KK%;Gnp|J zUnS#w*=)-;e0j<5!|gxqe)X&N$JMW|QTH_wer7-3umAFo)<61f0TZfu0ykZu`XONe z#U(=X(QIA-1Z}~ny@@Fny?8*fo+gX<$Kyk_mDTc{I!vtHTcxx}RckQDa_-=g1N}yt z7Xsim&vOG1X=QMzwc_(E$KWQRV=K%ENMe!340UP>1!dS?*n6%;Mu|uy%!|meFl|_Y z7?2_c1(z|gw09i(9-#Sk+3f~Ph<11@Ic;F((|omKa)o{y;N+OU!Ok1`1f{ypO(k^+ za71LGBy#L11YX4~H-NCOtl3~Zo9C3qWdcux#y{Xg$7X=sC>meq?;cIehzQ@SkgI^| zM!-T+^;lS~`}hzx?8DKXiaEmILV}(T@4cFCOjm&huo)vfp<@AN!7Nze}JPa+zg~ z6K1QJoLhx4PgAlUk^-{}95tY#iT|t}SjIe1_Ir@+BES;u7=k_YGcjPd6Wq`uk&2p_ z5|i8m33W@l2{#Bk^t@K$1q(h7BfJ=;>eM;Drb!1ZE0gzx2RN}I( zkB0+vc4j|k>G-VlnwE@TqQt7-96m7>A>Y?kzisrskfu=MDVVJO8(;1;ymd71Br>ed zKE11J5vK-!qo{mbJ5((ViFk`gi0AiGZZF|_4dde1^13E`O@yD-PyXpw)BpLutN1k2^VB>{11N) z@@RKZxh!d+=GP)N99VQx$}^mHSuCJ*%ZOKTea~FZ%`*g%wzF;I_31RLU1-ziQ76`%sNp{HissV^8nOhNbla z5tNOYd#|jukK8D+_=OdU-`M)DZb>e^M%~v$_?i8*?eahU>GCJP@3_#mV6*@g4iRIk z`x{!O(T5^X%U?1}7=t9Pub3YjSrt-=)5}12*zMg$fZLGnf$|cCLn=Y5DPz+3H40~OpQOwv=DniY#Cv)$gm(;H})CiEGEM1>8}(B5z2BV6)LcH96`Nu3b{gX z@E_xf&NKynJ@M;Pl8`A;ix|XtT0BXlAdn&{dLmJ>u_H}xCEzd&{oog^Fg+JSwc8-p zG08<0I^Au*`6sihvC9sWJpe=GbT5+80=NlVUd|QK1|pjd-Qd6iojMN!hJ8?9;&IkrX+7T zNOdYz0|7V9mL)21Y1_NHJBdR(<(hpvsK3VBZITncU4xyK16n|+g_O6o2=Wy&S5Y||hiS8jntYzZGKS?9 zGWwbbKfmqZ&@X@d`{|#2?eYp`dXF>%!y|h{mUVPDOey2WL$7#9l&Au6+)2&$*zGL& zQSn6soGSYAk?*w?!lEd!qmnj%(>r)B&;$nan}~RaYL*r<*fxUb2A2tt(35(&1_m(r z`$X|VrcD@G__9jSTNw=GfTodh_eyz@KyO~Kys7p{(3?C}wGD!v4BiwfkQi2Fjm|im zWyTY%gR&WFPt?chv=Gx$Z*a@2ozz6D0k3EmcffVh+EOxCUN8Gm|UuE17vPfU5DL0 zcnMnKhMDdQN>%jvumjxh2m8zC=a)Bcp3bM!)6-L;K-wp{p;|HIx>wguH{2m+Skj`4 zB>;-g4z8nYD*`sk_{z7NeI-t${eslU#NRbBgSMA2fv+kM1Kd+Gpg;|o!2njtG!iKh zTUxA<3*D7iujREWgz`MlVUh__-V&-^w%vWHT4|fSjMjis76mc6U3}2VSZl`Ql2O>cra0qaa80V|%F+xnH1W92k z_14~@w=0?;W#=}$I(s!IECYaok#bk|Y<1MWKs;W#;gh}ydmU>Q3-!3GGefRGItB}6 zRtn{#tyL6+3?e2=*g?|jtvDeyI6*2h$;;B!urAZGh9%GlUmiz{19oe`@*xn@tT@X) zH!)I&y9yUZsa;Ehh)wiFi1bp2Y1O#HiR!108t2)*G?|F>rbC9|C-94bqSg(_g{!(= zlEGkSTL3VztJ8 zmIx4Z?VBpZO8IJxQZXhE+Cjl#Vww;81J2wsV*|JA=i4o<IMUJE(5vUzg1!!C=H(>ANs+1^gs9so# zep!2YniM*>4e+-!SADnCCVO}1+E-d7MBu1q=U_wTF5UcBZ=Sy!&hsl$<~0$1h8BGr!fuVa%89% zv)(J{2JKu(v}cm5)7AhbXh%#tV;K!xum}Pru}rsXTBZfwt7u9VE@m*BY|x2%F8-x} z5xg*>SytH-@&|ntQscwiSYVCS~i(T_R1rnLlZe9uzrQm2!DCg7ATVMp>PT5 zE)cpSV#zcB?rxB1dc*D$}52wxN7=e8hhmVf&{ra$@p$WG2eAGbcp z1O!(`jXj|q%>K`w^N#rYHENQxX+~}M0&NH47X2lesHZz`jas?Wj_Q&=RB6x>toylK zu3$+|Fa)>a{dT*I<7m&~Yyl?C3@eyALc$b2L>3C-l3Wj*r{C5vPE|gD7c^?gDA14V<_4>-tly` zuMpbb$yl*a7h5rl+->YAH43xOAxyG3P31%yiAAp8p1S3pV#qqriQMX3)z9SwM`(tt z`j%S}6D6Vr9kL{9!Ai(`t!^BTM;1%kd%?^dUm9aP)CUWeSL`;BswZ9}cw=E`=7w&t zm#*{md|CXnj&+mm*y@O zp~*g_OG#xRpomBmaZuRp+h1KSCz48Y+g2E=l@4}y5wn9XcpD^C$7-4j(`z1t4;)eq z8%vifWbZ!i|Ln%IcU#sZqr%*94OnLHwqbMCfxxuzi*t0nk_oPM ziMeh)F@(@4fL@zD|iS{7Gs#W>#dSMjH6|xNs^w!uoJ$q?P6W$Bk^RZ>R0Qt zEc0}c-4TDq*b{XjSFsE_m@)(rYXcpT>unmx^!zeiZx`$M>{D;I+v$A9aEWC)7BhiE zy;NBTF=CL|_she>LkQ}OFo~fwa~O8}g=D_t;b46f_i#Aot^-zr4V-noZcgiMoGsE) z`U53w*YBv->9yHIhKiOkS!FW_W4KnP0DeKo9Ti%e$XWpaA~+F3*alFb7N)`WPV~C7 z|Ac>yo2M)eS`)gr1KwV`HJSA4;gX$0v6Bjx*TGDEi=GqF0 zgidIHD?06a1l^_K2Qzq!Wh8*PA!SvcD?+~`cF%D_D^c3t!VE$OyFSWo9PgRv&1+l$sYFb>oQ3@T8arPq=M|U?)Ha0 zego6LfF4wAw#-c|O-3-T^OWveQftYefdv@V?^>K6kOi73#=0<$;`wk+dY8aPMo6q` zTI!13brYl!9Uv9JOdk~VE1`hg_LYh@3sSoXK3QGt^>($U+&*Bp-`NLHRU9W)j>oRn zNuIT4<0T5Z3H!>jp-SI4$-dV;d#*@M)Txc-LF@>DuvOCC+E${(@QXM$!b)ko%ro6m z4bT1>g+)r$42pVzk$ZBVEAxRBn#5#^uZmGcI-+5dKNVE9D)-Xkf;=I?04fw#4#(L| z35cHc?>pJN{=6o_fA}BJ2mg8cli#Dyo@6$??317rQdX>^cAcdVYP(o@yADGY4HU7y zKCVPfDnnG=U$G0EO3~VfI2EfLUBLx@UI^ANmg-=%x|Wxh(}xcqPNz%auIV;j?LA&j z=aqhYe8`Y=o7g_Tya0i?+qnsYHg~#!M1a(w1)J=5;;@6&g9X!Zx*~KrYaBZg+VGUl zkXdolIMV^D1*zJ}XaYIU{y=9y3|f(VPzMAq0b$XY={`^GsN7VFnWm~vRBl+hPqQ7( z?l7D$SBpeV+ug=VPP=fF<*u!X8ojQix=qNx!pSQxk@HN2Il5OYV6srILuVWFSn)>C zt_DfLiReZJa4f~zX_@KD*OFXKJ!X1f1&ZGZ(g12B)|MpiYfzeX3Hhmzogp-Y$k?Id zNF3W!D8KNfUMV!XCG?#LD%QvqVnHEr>fD^kF|bk~kc-NW8S6YDPI9h#BoT8pe3_EQ z@d{BloO$>#*#!ilLRfyBp-6oOCXq;y9kxU4i#aKTRj&kTrHbNeiI%Hpk;t9wyi{l< zt+9!^a&;R>j4}MSS`;k0?-LK(PQ`7!1?(mvvE8E{a*zUye%K%6l99j!1=5F9!X)&s zBZ)z7Uy+MnqOv&`&eH4*8}`J>qRD9%PIyqy(#lRpKXleP_kdUz`G~nq9$&j??FyJ| zTRGGN9*jyu7Rhx)id(wHa2CddTS}KOx399Unl}>;rhh4$Esc{%H#z=fUWjeMayYn( z48Ry=4*n9SF)Xih(bq)ynf+LF{rOK;xWOf9W7f_sKGIQ3ePVmWTs^~on8X-D+$qgvR5|EB1IVBM5Xxe=&B2l=rCA6 z-q01ulb(nkq@%M^qGd1q3An!XO=w`FUbt3-GY7RINs^O3oW^l9k6&DoD}%nB;D4(_ zpVUH&lyBlholM|M3MoR!4DlC`v+QW{z`x9-y8!8g82}E)3JfZOvS&zWjb(fwM;`pe z%B~^^jz|P;2Vt>FXQOeIosem{+}^$Wa#?FHXdi4%CAs*XL>HL$$$3wFNv*GxtxJW2 z)*T+Zz&hmQ+1uQ*D9%EvC|1#ATr_GN*LB^{G~$U`wgavQFUH~J^W|cv^fuo#-{uK{ zKNq}2090<=U6L2Rl^iy3tRli-CD;_v7Iml}bMxOXSzQ%3({FX1m3K3d$;m!$pN!nLA3zU}#Z zvs7)#L#rlh#`*zI>UcYe+HaZFKKFKw^Ey9194rVg(?oO(bXHKyhXztq;Pkm=%}^3x z*C0hwGRJEySMrcpKdjF&1kPF$Z6jQV+3ODH?d9e9&6~G}{b1h|X_(S8r8*$uU&uU^ zFkQvfN(OZpfIM8l>ylSJvJTn}I?itx4X%zVd&Fs|mP_^0qXZXvc>)2kM%t!})#Afx zH-x=S5{(+jP*7;V-L8XVxyNP5c3YmKH7+K6c2rgf_0Vp^pb#_lMo>>EtwmZ=V6XrI zgqhL5P_|837K9KeQ_~j+tHQ{RpCOj!C18Nq4RAhR0Jz}Qsgky=T4qHCs{WnA6(<`IXlZ0~H@s5%K+si9 ze|fuNYtXXnnCyr;JQr}A)Myz~6ChbAT958s8VuCLmg5syyQ%W>KSBY51aYLfO&pPZ zHisza#2sg=q3Ws$U`F6~^ix;9JKp~M?T2sr(^|`w*Pqu!_<77l(?9#I^iO||65m&B zxZBEdxp`W2hn@&SQa`{N=XAQrC!z1HHye=IBGK5JB`Ak-)w5zFl_NxFR@rUWtxLMy zrswCEUw!kdAAkHYui%E=uH*X;FHP)+-H!7^48H865BtMHwEhTsc@6+lz`G#bCTj{g z;cA}V94)26o+c&>xeNqhneB~lFhK8n`#!hR_4#!E{`(&W3`WEAhi6#bck!^_0drr7 zGAFK&`ZFzLt9{Ep2*42XxoPe*F9L!FXip-hFDXsz6$FL>sgEJ8Q*Yti)58-cn{9JE z9trqdEW_cIpu{bd$`pdsRU97;X4x1e`LeDH5RqQ!$q???Np>{?!o(~iIM1D*r@=(p zOywjZUTCzai^McRoe{0if<&O9AqTX(tbJ*D4JwR9is_14N6LVPNGmFb@P#>BIRl_C5F)(*J^9 z52^%6)68`2Vpj|<233$6uG|8LbV$|0IaRi2E}P;>1y^!um$=Nv>FYQ?Oa!&Aiv(!y z?UJxv1*XN*yc>GEORZwC`_2lrd0i+F6x?P#|Mv<`e|N2|AAH~EfyZ~p&TQiLA60BV z_LLjB0_7TH4v_P1)7Yr-WdVmNC{&@u%;oIUii(EG)t(6J)Penu9Q3lLgdJ6J$<-w7 zIAJE0UZ+ZCQ>+uh<|skY2vJc>TCFj)47VGvBed3Hf`d|Q`=Pm>PqMf{|AYk}gsEjHNJQdn z8P^&b>sBm{TQ`&5qPXrlzY-_XhYomjkQy88xMV~`Frk`Vu^j;GCNALAzDW{+0vV$ zRPr=>E03I{0V}p?UE`I4th}&na=RtaTPC(R@S-Ds9CLL07n(Ej-VEu#i zKmsFM`+i+d5~TGUP+Ki3(C~(AAhQJH$TaX9X`pbiwV5k?^zJ2a*~!eLrkS#7M3Avx zO7)w)Peki46GU1_d{GlNPWBzI4QL47iU8hbIhLBgIaOY3gUKbaJI}t(NDDX9mv;T-U3n^5jY>BFW+Xco2%sd3kL_M*wnu%R#c*unvZOOv&w z+7>)qwkM@By6QN^s_FFpa{Au-vLD``FIV`sV?CFKzFF3MJhTo7Ky+N=coBy3^m0BN z59iYrq^4-8Wx3p*`wj$u%ar{t+^$z^90j$o2mox{lEulhUw|FZ4B(|@+rDcd%Sq#< zQ@#w>_OKf;&MS`M-~iwFrh}$71h`$AU?^(u`S4&J=ePi=0jQ;Gnsu5pt8xm|q!qXq z7!FfjfRy-*P~LallJfNO0##31^8{l2Z`w|j5Q1S`mO`{b5oJ}p zka_4+myLo(z~@R48NZDd-R70$MZ~Tg73M-OS@MASuZMVylMw?m^bEnTw9IQ2igQ(| zGJ{wG+Z+bAOaoX2L?mx8Q9IyOx%XL>8qx4kKnLmw)Ufv4I|@*w?W@WHS0OSWOi+Lu zx6H%S+XS4{#VRctU6BkRS4K`&YZntLe9=|XW}V>Gp`7r^+n}Bq$yFmEUvZ5b{bjw0 zU6hKrwYDk98hLe9HKx)}`8L5C!FA16;*dd)Y7yf~%qlanyYbUhy7~WZf6(Z1qeX5* z!n^K_TF_Eu$?h%~1QcO50Qo!U8g^8nWk-j{j0fKMYJFLV*jl{|0m;_n(cpyigB4^h z&CofkzV(=o_=o@O`g{Mx&$k%bp@TuZ#XvlS5j8l2a@OJygRvkDmfZDKP>pRO z1gMAs@qLeR-^=NQ1xYKTJI~r2G0Z}ZwSC`x_rtIL<{Nv>l|A3Q%oY&)-X6SYrG4A> zm+S5M`TX*7#$I7w>{sk)Fju2uru9~ghwZYiP1{~?H_M_e)<`Gt-mpQzA>khCV$*e0 zYR==lAa2L@HX>J918VPWzlcz_T*hhbA;Z}oZ_}6Wh=e=f@wF%e@JQQR#F?pLgnfs+XUuPE%gH zA`WQ-g`pcM!XSpB%LKN=Lg9ED@8+}%fH`ZO)@rKuaiZq$EeZUFitz2e5%*izSI#zy#1Wg`#~Nr62~M9F+7E5J9^QKM)S zSIOK(4I>2vM96W@?ht#$!^0z#_F`L2;VH-iXk{UhF~F`lQKu+KA^@Gg?2ZIcu6al~ za-08cMz~&BmAXE3l~E|sT7Q=flF@uiu8#EOw?7xCy-5X|I!yFs58NkIfuT<^FP|>_ zw*BBoaiUF?#S5kcLA!f*CU%~a9s-J{7Q=IAw=ZN4=Os3N+h~f3$o(IUunF=4GC2`) zg|?QgKf7Vb%)ya!k6LKy%YI)e5a6t`c{>7&B-PO(HCC3*2}%r9yx$bp?)|04OFHv( z<;*K(>t3_qYa;y2U@}h+>3{fb2me;kw^fXi`TmhT9ID9DHVvcFrlzAz5`!f(iDcS? zw;K+Gmsg*o#ALL zqvZ{CLs%FxtT}EX4dx*Xq}{Uwzz}l* z%S0Fc=FO8G*ElYl6?&m`DE3z?#MQ#4WZ=k=$48zH(F8;O4PzBa8vCRB;vmP3(94h?}b6kaGz{eo0G z!kM4b60EFj$eOo%lKbFoVc+I?f*_sDoqgPl<|{r_yGjksMa8Ja6z@^iGfMB7(4knW z#*C|DQSUZ&nWB=4f`M*|`P#0RW}!abYc70Egr7}2xcvJ+yZncLo1qWW>5)_o3?n#; zyu6JqP**N3rkymO;DXBDV7Iq0erRv)`Fw?wt&8U~q-Tzg4~%6(K)?__0Q0gwKcBw& z+h70Z|MJUke)YFt*I90X%%sJWd77u=;o<3N1de2yZx+ye17jTUHb zg=V#O2iiA!dU~{cX3gk4pV|%yqWAH5e6&C84tvZ+DNYP+YMh$Rj>XYiZ-&L-ViAhc zJPr8?VYffrt^l=?q6tCo$k~@11G#a`@<9bg16v%QH(x^t=&(E?*f7_;#iFDZEEI^P zXb@X2H7A3)sb#!&NN5~m+2E!d3p?Gh8wj-(myyK212Mw#rOPDlF|5~~m6i)U z3@FlN3bGM`6G~hYzE^cWZ)1fV!inB4`vRZ$lFru~^ngMv)jLnnRWEOZ3}1x>rt|bP z=717v?qCaI7}=^K3v}SBlVSpfq=;QMQsuofV`9PV6O2Ilq*VD_PM&2^$;yt-@(ok_Wl0z7kGFP95pG&5%jfm(XVmm#K>j2D(?mAF@RD6-Vr-PUVv zjpHWuqCs!2Vr;TPv@)P zA#1h>Vp(sb-AsJ@mNh=v2^MrLfWCkK-pX)FST!sCRPCGH#wk+!E#>-5THGBizgjl7 zmPerv%9(Hx*Ukt$ZOUa%>%w01{Km3i+lAQLF>qZHgQIG6zI8c3j)cB} zb7>M7(+=Nbi_^nqc1c$_#(~>T@*DgSN${8ni^$-~FoZu-o@Evr`bLV0|H3( zabSugzpZZRiJkqQ;viVx?H`Xc39NZt!4|-E4uyd9<{3KA(U5 z@ke`HAC5;Tx8+%#&;ZZ79!}-<%QMER6QVl$umyTlE28FZXJMKGGuVsO5~=0X z#kr~JujNpLIz|sd3I6E>cH|zrig{SIwDiyb0jSee@Pf^lk=`gcq zi4b*Btsf|pjPTX6MSusY`Xd>vt1WaqU6OvTV=~5ZdRAnb&C13I_<8OQwJ<}sjg4z7 zdxQkcMV0{(RK7J?3vsh_)vS^TUTUhx-Qj?d_&D2LbiUklFl0Xx4+JU>K864p0(H5m zVuy#j5Yd^tCiM3_7^JFBEq)aStGy=%9x1{yWkh;xVCLa-N6B@%@}7G5o1}fEQPdp$T>vXk|32LGchAqv}Too4b`Ek)KN&e zdvul+x%O;3Lg(PY+TE2S#7Hww3?EX58Y%+@$AW{m_l$D^lh) z5q@5H(Vu>C`PcuPj^UB={Fy)qsw7o03MUMLR)D^mlnUurl^c%_52y3Vda^mC{qcxa z1B1{)EQ6DEyM^iNZTj0^|N5{0=C8l~_PdvtbHKcDCQT67lL>}%pz|X0OmGF7m#J%G zK*n0QeRMOd=>0f@RA<50etp;th2BHrjF02I8wQ%eLf7N%o1$@%SYH>cB(W9&vJ-Do zZYyllJqKKYM!sl$!I-x8bbUTuErDIe`RRN)?y*3$Cw)I03N$&(G_G;KyG`@quwxXc z6fv$?q14JnW@~wizIoPemdiLjJwBjE^sbfWWcC&!MG-9OO&XWhIu?VlOqCKXa+)vI zl~*dv>TgJE+XkMT0Xy0o7KpFnKrB-=Wdkv6fwkfif$|R(7Fk7Np-@V(7-*WuzHeY( zKidTwF#~q&B_Z5az!qo<dWNK>0BPmb}vSJyC|xgT;s`(fO{7kyT~sg`vvCo5Vr+e#6ef3>`J${ z-;%yGB?X-J;PwfY#=4=UXoEyjN?Q0qwJ%2n(1E26a!y{7-G^8b_sxd8(T&iq5N<(< z!-+|!F=?<)PGC^kB1^1iD^05_@JAopQqjLcc{i zN8R4%JYfb1VBY0+we)9g5|lY2ac-v`e--=^#3%HR_F4qf0p zEiW%8n(^X&?0C0*46?>H!zU6#R%#w1ktYnJlpH!ql@jgffdZ zhipu>yOPWp5vWl(BgOPc@ZmDUjy@-Dj+w38C$ePp<;WZ6pG4C7KPcT<7PD)1o2LEa z)7!4UUa#;*;QdCZPzDwVQmOPY#_BS~L^)`hTNQj$@C5AiBDOnPPzj2{)W^0aN|}Xv z0TQBDv0ytQ7;330pOlOw zf%uAgWm!gGFsT%wT-)`#mUBp>2`})f&^$c zLIs3Mo2a)bIL{pm_a^klD`b?b&PO#1*FCC8BM5;=Ko)c1Hr)%dhoUuD%lJ2c^Q&L{ z;uqh4|2@De^0dfqoMsD&k(tAyo+Yr$<%TIGR7mFAHFLH#EGf?ka)lg?CjHZLh$eBc zFPCKk~2N+m(K5;!%Ex4CP3I~#D6BIzFd zkFF!{xwW@iR#e=yMCQIKt`+p49*3klN58;Kumd`RRk2Z61}(!j6LoHPi6(UnltHR% zRUg@lO79lSV@=dRPC1O?SZwVYUSf2UkcJuDJ^}hz$Q<-kt${nPrHbh9QhWgHJA!B^ zp(e~QESdw;3tL1qb&2XOR-)KHpHC;I{D$|KOrt`|oA{0Tw=A(RM^0QV&Nehj(B!sS zS#98u>iMj@7i&c)dvt*yOO$wQA(FOC%Um@-ndyB6iV}7NK|guwQGSyWtOA>8&xqIa zazGB3{h!^3c9WGjxZo^>r zsWcr~D);T+=2NlB@vijAeu?w{_2tX|cl>Y-(?><{*Pqu!_z(Q!{rJa!@ceK71; zW>I)uh*9Y{-T^3%oLD|%R)nuPRHufj##s8HccQa3?eojU$K`N1!k>X*n7cBV99dwQ zm*uYERzi#-H0cXSRh9rf_!7JJfHdJ6Xauq>;k)M)}=TO(NuD`*5W z_8;pqTB@@L8#7gOQ6+3T%7VocU0FHNkjomUTh}UI!sepuo*o}}p;^`u%P?-78tHu4 z8v?<)Sdt@O8Jb>fQ^M(T>$(#cQRVS?eD}qhufF>7c-Swg>|-0?sh2PV#HWKKfLWg% zH1L|3i(2a49}n*##IWrC?jH5gI zn{`@fwv!o`p%F$woY-z0dv+(*GQ&4mlC8kMEGVp6yMKJUVsROE`#ol@)1*ydu6rlO zV5KWM^I1N(OY7w;3EQl!q*q@Ik;`ge%Sp1xf_3i|s)Vc0k|x-6aP3~Q3pJuDB1don zN~k^D^0sTZ@2J zRR$1>;fv&(TI$+8Zt zWFY51Sh$6EcHv608#-+Ov_MP0$X1xRI*TDJN-?mRrpao}aT#SRZ+T>;81+V=E;B$G zfu=q_O=(;vP5ENiSz3a<({0uit0fN9zPhV+1i&|}Iq$&jvt|goqqlJ-xa4J8q0s-~ z{mB|fYYf)6`P~<9q1=zHA&U2v3dqzl&MiD|=|T{}^|mmvJ3QcfbpTG?Ryb4(Xi4R8B%fn4vKMp zQ$F=|@+uO%`po2A)rWCZc9%7cygMQmioPanQum`ak(|0(Q$056hu!!?cN_hZU$fwA zBK(Z-q7TRUfB*OW&}{lrZe1#!PjZETA6V+^T9vho12n#|Cz+nN_He=C)rBNOT>{5B zI*X2ys)QPRu@q;m=~_ZnxhZ2DQmJSdQ|bYicnFO<$|-jJqvsm3Yds z)exBlcB#=tM6v+-+Ol$~6kSh#$)+i(ATdzu109Se_VjwaeJG2V+UyAFH?@3-QtAp)bNz}Q!jlJK%CK_g@4Y3(&kE1mJ*PKYkP0N95LRt=u z)C0V8pZdy*uzz1a>qlHek^h9q4IkCuT$Mntx-%cwxZ6%OY@J_fe08KEb&umv{=y2e z9+4^Z0-QHlRa;uOCw18E?YB-ZFFi2ffzP&8c>8%-+>$8-LDLay*n-v|@LVa?mSS5% z%NLBFAoV&|h57YAqDTuks`78F;yu+n#WG|_@LgK}{HyC<9iOdodi7m;O@yCQB5cCT zbUQvA?IA%GX@Rzhtr{Z!01m&*6I_*0$;+(`VjgWjEs*=~yPiyj5Mt%qx{6ImF)+PQ z`qC2N>E-n8x8J`1@xwTd1vb31c`0 z51P|99i2QtQ-|(#sSIno1S=vAJ$#UINovVe0z^C%H(L|ODXNtwGj-aF;mVyvjZh zP}l|vIM}P9`pI=q2`mQTqISQ)xrZ_);64VRPPl^01hU!*@lkhyeFRc_o%m+SwW#X? zgd1=Tm}%{8mY`6J4v>WytGbRX&S6(-YcV{KP00Hs|3casWnt%ARJ9;*Gj)jW9a zO0Z|C>%(M^=t_qQ0IyaSA~fm%h->0h)XwKhM2}8++5pAb$u82@MIc(w%>yx4d~G$5 zs(Nr_RHUi{;H5!YcbP0krp|ryX8H@jWX^L~UbEn9BK(|2qmM_5KO?8Gk_L|7`QNblN?nt*NC163-xVW&3wa(i~e=N*S9x^ubow4qd-* zdgp1e8x{ab?48LGrhdr6ej$!CRJyUo>vs)WWDM#0q4X_gh)l4qqppV?$KCHB?Y2I*4C(;1xci9 zAvH@h3q`)fp0@2&-(WdrJX4d<83VK>-0U*Y13hqh}wuwNvyimSSA8MuD&itH8Xl)C6dMC$vJpm%q{fxOM2UG)T(^cvt^qhEh$#^-LdcJMXyz9z!Y2rs(+!SCMwkN>&ZSte>Y z9!X)7=jZ$b4}ByNst%nb#E&r%_C+9XrEFaU5fdG?OG4p3PrA(GZN8i>U+#*7(QeDt zVn^fF2)74V^w}ePyPd6#LjdKj%0dTHCK*>$Wt1*Rlun04WTax|U;sOpZMLa`Te8vD z%eCC+R;~(5lFm^WoVBWn!lNrl-CIMNkHfYCnj^$4YE4BX5x#oU^vGmOzNFimH&4T` zCrg^rF!)k==pp0X6mjw@X??Q-;`<~IO>53|Wk*L!K$2ywhOwY$PBZNsl6}7AA;_az zZt-#p&$S6b8{zNMHL^gw=39KrEG}1jhwpQM+_3`CFqv~>yMk@#WJ3;ABh*T1O`!0e zfqZDl#E5kt1QbuBPLZ`v;u_E@fEJ5P^cp#mgG0Y!VJ$cjg*0>!(VBQn&;bT_RA zw=pS!D(vm5+;rBN6%w^B!7B4l6NOLgBR*0K#2x(DbW*BR)B9~z$#ycQznci}W8F{( z`Q?)-YNc6-Vu6*d4lxLeq~ZW1PH#_K9Q@^c?uWjBO%Gsm3!x>;ehb~cLRP(a*ZF1( zh&-4vna>)2b`-l#z)Iuv4mCS*rBXcM5y`y_2)GaekO@9|JKjk zzxk8CSz$ffg+c(LMHA>sQgQ{{1B|MO|0L3mE)bBj>WATWyY;($Z2P3-p-@p&au@nh zGC2GUfmu7+bTdysn!Mk7Zw661uP51yp3V4SZSNeth;0ML#5X7 z63lk;cDuNwWx1tlKm_M1pJr>FBScv}!B9@;0i2K$i`@fWeoX|vV1?!dJk|^tychN* zGIO8;xDp$8he9K+r3yBa`b}kOT0KbJq-=c#)VV@a2{|DP)RCqBu+=+iZ8Zp0H^Uao zwXOVMQ7vVwU+j43%Ro_b{bH*g5rgz1*_4X8%D5;^recxZdR4(iiY?o;@UCO9ZT0Tp zms)E0ZCH<;x~-9|S@vLdRjOw2!Bk^MWN4vgj9Iy!+~_FTVZ$3k02%KRnYo za4UCh_Q#xYUBhJBCKFcLdvpXK=PW#k-n}T4m;4k z_Y{0V`pGm$&TJSF95Lo2AcYpXnp|NxJ7Y#Y9QO1I&Jq+w?4#}-#s(aL(mEr;=>e@w zD^G&Ea~@~5?0%s7q$jy>kyNl!Kg*+2hxV?$V^cJ+9yPAt#``||Yk z<{$m^r{DbgyX$p4U#@5Xmvxid$ajB$TwQp;+!D9U{?&2}cX%ri>S-Jku#PO`*%XR= z;AVnd-6zwTZH`p`|9j0YqXtNdAi*Gpj^nrm8dw^nakY$W@pTy|V$gvJ;#ino#O#}P z))T`-q9}k*^V0RCpF_nZkiQKz01LvPqgkIhEs7|#HCVWGNlj=ZzDYS%c<|OWD8d$k zHy}yfkmiwcVmNrp$sztX(Bo`Z>kMfrr4T$o!qnvkiABtbAnZzvMcK;}Tx)$K6;hy; zJ%bxc{YX?YboP)2cXcST>tGqy`dxeH1&C0jJr*UFpjUu=DuRiq5Nou7RJ4S%(5@%n z+dgfO=-<@P47O;UR5+w{?7f1vRuvCJ5zQD@T96c((#u_#d8dVR9|xEL&=?T>f#Mr>q#zE5bW%3o*n z#ov)WUg!PRroZ^|@>h?~QD0z?)?Sc-APYpdMH5R4Eo=WoT!3eTC{5%-9^tC#6Z6)pMU-fepJD_*1u6U;suq`o zO$%(K^;?!`)naNrdA;5Olu$r?wv21<+Ev|1e4noGhJGjWJj|CM*Z~Zng^2MsfwB)8 zrG2AFvjYTsh*LvOtksG3nu*c#ha%u|qou?nY-WMGA*VkJQ`p50x-1RVKJ5#7Fxx`j z_L{-hdD_+ucNVE3Ub7HY87K_BR;bQ4#^NXy@@W`e&9lSzZ3qDN)e`|L)kH~}hWyCn zTh9#_5GCktS-d3h8EUgqEu);TK9ryuB(mt)i6Z?AC(O92K0AFxX>zE*7|ttIVU(Ly z;>KNb6|Y!a>&qSVTeZ~Kku=cT44aHCQ7{bWkfMQ%c0+_nrVG8JkelNuJ4)V0>0S?`a5g>uKv310&9UH-L-9!@2b2DPFUkMj-#Vh3*WhZ zaS7l^-$@I2;Z&t*?y9Js+C+k%!17!xoU{%_a<0=(iksFI^9Di%4#v}6zPdiK1W|F! z342}J3f}9avti$ijC#3{6?$`dCee>%5@FSncVkJXW<0mk>eFi$d`*O(Q%CsaoA&9@ z085sw&X@lRZvIpPU5cWL*_>($asD=q(=yo~yMD0%cQ_)foX_VF=V5>7L)3tZhPYT0 z5VZ-dmaDaI3EEB;OS-;29QM{29S?`o{xVMXrjY_Dq^)h6LDCkfL;_v!nRTn8a}`)b zWFy*Maam5ncTutb&Q{dlMK9}~Cc+VKH|*`-KYaLbI2^Wji1b&oj?G!t=TZ@qVS#$C z*UO_n+B@|90OEh!^}8LZgjXL9E$r@xwr`09^rRx-cy6~b3Z%gHw8Nx#(L8ug(AsVY zh~zlBXs}0#7ARxd!Gh=s*%yoztuBGrxn}``^4+6zEmiA;j0+2GvibE*A@`1{#V&!9 z6#<0u=+XeyOE<6JGqm)aaN6>+w#NYjI?Lc2pzIn*wt&z%vAJ!QNeMbglm$`)l|9n} z^>7dPaVHES8WdW-3aq3QB@MZPF*QS~tS4L+KU{7V8UPv&`vu+&>%-A3i)kJwBf8 zZ!gyucLM9&W9$~iG$(nh?I3m7bQrPC^Z(D@n>AUotl2^C;W6wfhZ@d!yQNlxz)YYv z44Gl@!~^_let|Ga@Pe#Sx2|-%`}UdYROPgXnB1Ljt>qD!=QLmqS72q3dC#rN%G^Wj z*gL*)4THv|%(A|2fhtMGx;^J5?)+jYq%qGgWqlVHawSxikN@e$69}-_8Qq2w0z?-@**G%$eOtD3Gs2 zCK+Rv2F=T&Ae5GYtO%%?qoYM^lF%v_rJ+bmY3X z;)XH^#6bjAF}m>b+oJ_jpcWnGwiqf4`KtP(;9HzD^AgoH(ZFGOSzH)t?aMp|Spg@v zORd9Ts025`#jTfZd`exk5NVjwA+XvuJiS~ZmI*KO zfm?jz%dlb9an0*?a7~lzhqKK8pIfqW-*pLH@@VktuhL=zBOcFRPVS7mr z%~Yju#S$X=g=)CaBh_%{Uo8FvHO-|Z)FRetp{U!7P>8tWpFUEp8`<*{ge4ZW|4@U!`{0P~OkWcu&_>u`OAAy01Un7J5XTN>!%G(fRhL#}YZ z@#L4H9?OE{W6U7_iv|adJjRTd^J#zB zL5d75UB*n)I>)9XRoux~16p*LWmyN=Ru$R~VnxWHmj0%9C9F%-OM z5H=HD0lihyD6wEz>;=$V9^64o9N;omas%A)wsU_ci5yq9+`TzHchZwm|ri43IxxFG!;6=aL|v!-^%FujtQ zhos){NvwpvC+r6zu}#5r%v0f0%1k&5^;jN--4nXG22x*%RvAUHq3rE2nrxtB!x=j% zB{Qawv{(WvmtzW~8RnRZLZi7%HKXYIF9D?G>m^KdMQoL%5sH>;5=6L+lQsBNqpXG> zt7w^A1|o$)gv_ywE6a+pj3QYS%BcfzEg`WY1ymvNQ1+x{>LN)Xj z(h0h{I=gzB6CVc&)gE(;ug=;j%P3ZxKR!Ofh;EuV4a5~@fpWL)cDt>G=wavuJt*3t zi_2Z4_*;>)Dfz5&FKqdK*%vM^o$+{)vT>yRU+jtOE~h6zSoixq_aB&j5$Bf%Yx`ge zb)ve>rG`rs(6d@b+UvEn_xO0f-*t}+p1fL|1E|D_r%wT*R{HBH; zsAWD_P!X_@G#r$YKdX?sD4s+7%ux^hGLG9#H}q#q1@Q8nV+G{G#4T!Z10Z^(VCZKu zrty9SD3hV#XV8umZkAh*2>bx4!gHx6bvn~S03OIyH8N0FX|X!uc}BSt$g!T2#1`gF z+lrHb9G^rJR2FMtXsgwcJR6aNG0V1AOLNIc@z%r-2?02%*j{=yqJiqxXsv~i)CiO; zx}qamgtPJ4Y$9Pb8nSyrz&vHhn@WBNenn=gzkte^pj{O@yD%m!-EYfi4|9;S$~0*+4`I2g*@Q zdwG%vCZP5UCcOd}F20Fs8FGm3lIMD*v2T}4k%kM;~* z*BuUrrs=lZomJPzqxFR6)9Er|-3Nw1x9P5LuOX8*#pAIrGMp3vXJnbz?qVXrgBXPT z61M(}EV!_IWjs_2Wn>Tpm8)d=8jQ)HP>A|{x7*XjDORq?h~yXKeJ10%P)ceM_99n? z9bE-xOY>F%RCFv=cQX$5_*EZA$83Z?$pNMh(*7K2+*BZU%RCJ~3{?RZN~s6fA+RH; z_6lKcSlWBQN282x*dBwW030ydO>gJ23IwCTYz``;R3`(~FBkvXx}s|ngs`C%NwJnJ zD=SN$?tJb!Z){&UqW6-amA@G5>@MMt~p;9YJc5>*G2Agt~$! zc9ayzs*&D7(QQ$hVgW;61#4;BBCk_qP4c2Q=GVNUY@u93U6_#P5n2U8d2+A3GTfQE zkXy%b#jse)hbwP`b{htkp6@bhxe;h&uwxU@Rl2{7rGK#_Ol9(*wVW$n$ZR6l9yAwL zf>-0txn(Vx*-9Fu(xF|nK9MhrfI{Wc!hA1E7aC;Ssf0Z1OvQPEkZLk3{W1%odPFL@ zQKHv6Qo&m6ERm~I75#QXet@7&t|}}X86Of+Tfn6SEF)-@BmS${`05$(IX;E)zCAzI zK+3#E-`7O=`HV(g+_qLyl4l)sWn@Y+XTb5Xx0fxa18{DVH@I4s21Iw{k5D!J>5}qb zIm7Z6eE<*;`+l?*XPQQMaN_};rMT&0*I2x}yNAJ3OqWxCeSK>e&)awTaCK!Dp{0bs z`P=VK$78$MZLJXuwY65@4*F`SndE>R@dbnyp-0MOkY)Fy!=4tvo^=ION^_F0lpu~G zKmm3$a4)7l%u-dq-RwM+P8$Jg5Hc*$rnJQZmEsy0nmGagHk&OZ=(_fB*x6IxTwP!7 z5AVKsAF#ulF8$DLHn8@kN4aHrKq8aA;`_sDq)p52MiXoC&@p1 zsdpmaSZ``KIiJoIEGb$7{_-Lzt?dl|X6!Myd!QLp3}p@(cpZ6_TvPd57DM)IccC?97DFLGK(rsN_P6+$CX+m0uQM zMnC`co9VBwPD3@mwu7&U@H6_#``!HhO=hW{xv9`!a;2960b)$uz#*V+rtwltc4M(D zw|ED31QndErGSP-Rny1k0!xWGUe3eefQF#ITrAC?yrt#2-TQnzefo6&{(Z1ix9b4I ztAze``FuHdo4w^1EX?L8H|>Holh73Pf2p`8w^+B`Z0$(moT{e8w%vBSG>#zM&(sc$ zMQ5w*n0RHE)Cdp+qB_zcKsuLbjomc$3`Q6a4%0YA49)D~-)}o9=3xF7XS<~ZnyzgD zW3gA#wcwdl&NAePDWBs6H@DpuI+%HGV5L;zOHJYzT^TCLOjGG5$@ksjDqV-{*`p*i zEmm&2cIwB$dXWrs;g%%}@?R<{kE>aqpq7ynWCiyI^iWD)vw6#9p@xokrkw);6V=kb zU^1u(7ht6eyCx0;J!hV1&C$N+*uu0aFv406z{iMa8nky4_H1}*R=?ET@~!SjoV7AJ zlCTXCfr68z#X{&bMb}U<=Cbu8>mym)N#v~uvI&X0Chi0o$c|JCObK2uAWA253nya) zNo7j80HpOIB!_XW{FxVdAxrfAB`)ebbYftJoW_MaSiBgrsO?GIgry~QMWh7_S?5-6 zzo+3v_Roe5c~z0&3)E&cJI#X0m>^*=>VwN)6q8=hJ*!s+;+Wc_U9;2Q%BOx|`K?neu6(9C(?;dQn9wCBF&|$B^&p z;eUPi_;0U14gOVP^feKFPDq*fkN&~*pa0{q-S~pEE*>@3kyvtstjq-Op=liK28Z3& zE|hT^XpoSJ5ZT!E^Xc)46C*z+2(nS(q~`oEUM|B7#V-mo^186RFfU=5JHMD!31kf}ULX&!~N5dz&_ zLcK61Nv1}tO?xJGGjbY|uY$d2v}JHKYmJh~dx=`qE5bo~575lAG1dNQ#V}%esZP|D ziJ2tp*7=*=c9x^MBx<+@O|)j6V^SO!>0;uZZw4Ero+1jMS$_c$tVwfm5f+3He#Iiy z33ym7fEqYB(%6WS0Fm(;Cd2UMfLc0eY>-b2^nC2+VX)tB+V(jMYQ&16{SvLCh^nP^ z^NR?P4`s1exnB%9)3V+xH(P$YS}Cnxbcjq#d-e;=OnzZx!G$2vK&~WbSBx?jx$Pt^ zHP5arONzls>r5j@DTwFdN|*&obuEgT%EXJ&v^*WNxDYQYy*=~f~qxxuLFT8H+ckkX=+_zdHNllI6)Dm+|m=8dwTwDdY{b7NI9wvy9 z&?GwGWJT)5XM?U9geL&9=qqc&EbeD^=mr;0uK~XIQeD+kZf;e)} zH!vQQ+~fk&r<55z9igN{P|}Qg0`wO9{4iLmOhux9<*Jk{%UWmIF#b5@TSj> zwW~g--I&glP?cjxNQnY6B zw~}G-H9&ty-$C2bN+W%_xwLbFG$2&Tj&S(dZr*WQ!Y5wl}AF*W0&YCC7h3B)Syo;q)61}>p;19V z$kA@;5-eeC_*Kz|$rIa*B=$97o6qi#UM-B|C_k!2xi5{|OyVu1`& z`IoVqh6~sTIuZ?;uky@sX2tp*?F6Lk-`^twK0Z?2w1h%T zYD3-kmL(>IDn&qRLnex5QYJgCmT&gwez&(Iw~VJIUV`u`q$vgDX#}gGM%u_ImPal^ zluwIU`7(8MOWE#Ul=C&^!chct_169$q@;y0GO&eBV^sq1M^NIZ<>q)i9qo~}+pRhw zyZ_VE)23_fe|QnY+;&YxBU9Ar^HevqJ1mmRLFq(r3~BXS=(q60F<|!KpX`8EokxtX zwW%uODYl6iAeNE09tUHs6@+U{8`LPi0UcoIE&o8uhwJr^3B!O zn503esugP`4iSiXArcun12u|+#&b-`r}aseIEHcj>B9%>kOF2|UeH8~X`9W~VxM(sPfw3e$KyB* z6cDcADHE#Awwuluw1sgsY%5<&tjsSH;i@L1;Jc)^cs>sR;w)>z?RIlG9Qq!8rv+L^ zVG&Wj7`~!g@Fgo3Tr3FU5-N#@?JW7&(ZhN$Y&URFvIl9JYn&fSmG131uCA;T{pqKl z>?FK@|4wpc&3SEM_ zLLv;+@}Nm)7`ll&

ku(5UL|8&mkS?NO&sw1F z*`llDzjXz;{%ZaFuMSGT+QO>g^Tk=%FJ+|{XJbjds##`YWuodAjepnay^2veBcf!A zZ%4zwped`}_;@^W9@~)7L83d?fiD7(vuDwL z(;{SO2^!|^NL0`@W%B{y?7;fk((I-0$5H-Gc!3P#(DcfS?G9~q1$HMRKSKElez}eVOd!s|ycx(L1G| z5#p_#cCZBbkWJNSYDe1;Pn{sN@7moEUW^;-%zuq%=K ze0e?6v9rPjA_$4Pu}o&NO3%Cc@9{t7+!nd^LsKR@LHxO%NTdX!AJsxF$yNl`?jO(+EBpiqjc?)0-^2 z6+mbAwcrMWoD7*Undq`LA+A9Pe!CP7`>RiPch}c97Er(X>Z`l^hqmkPA0IP(alyt@ z)jy1bTd`}y6$ozc9Uz*bM7?ACdLWv0&y8E z@3TZ|?EL}~2s$mbr1diMSxq{fAhwL1aAVZk&^{q{q^xYJMOD=x`|LJn=m0w1%)IbuvPJO zb&6Q$pD(dID^O*iUNJ1zs-t^v+vn%fOs0F(4V!ZRg#*|Stsew2wirO)t_u7y(_ z(UN074orl+9_DmBouuArSMu%cHF_yavt1`sG|mr4i6jDV8Ioucihyo`hRelib$Vl0 zfv7}%$SBDb?F;4N%diqx^IqU_pw9`0JBFOw?J!N3^BEG}^-PCFp>M3YLHch&kpmY0 zu;1;r-TrWB@dAS;J*TH*7sA!yAVA~QH728C4&2?{Jw86#Kdn0?LIr21tHaP=>d;cr zJIHdKty>}^4O|EyS=hYlwC2TlJ;AnjW~l)?8#@e(fR@AqC=EoG5O*W~9#05e$D;x< z$>RaseCXzYElO}LKn|kF!&t+s8y2#E@!FA6O$$!sL<*@kVT%zwvi_`LFA^BZ2*S`l z=Zpe*L1v0bP{PhsZh=wOn$oPOq(Ov?#h8dg?q+K~TN~5g; z4z8>tU0IN)T-4p@#);Y>%9TZ$sa2_WbMZHhBvg?5p=`G-htUHjvU>}6qi~Y2-W~k> zmtT#4{^rxG#OP}x{H#u7NWb}V{zqTu)^bDdi=n5dhCg6_u+RgPlMHtmi8mI4t^Rk^ z^nZ31W$_*oAoVjA0dQEM&nu+*+tGdq{&%hJBIT*?FV+Zt|NZwBZSD4(?O}iT;D25X z+mN?BwOexH04)=L--38RpaelHA+p6xrPk)Gx2{Mto7MJN8ryC+^h@d$vy>*~e``l* zH{0%Z`|aMo!Al_4-|FmDJ}q01z_Qfup~HU_y{;QbP>QTQp@RjP$$e`#lG!Yq_ey0B;#Ia#zY1XxQZ$n)?j()M^%Zq_x zmJ+^@rh54pvGjA-ZDDueQ-u8~EJoCF1M4Ip zQ>cbjZN#{aarINx_gilMD4$mIpwi9Fjs1)gEpETxT`p()iKqUSCB^IO>k0O``R4ZK z&HFbNc<%2X?7*xeY^iHAGdIoiVBd3thF5`%2y-8VLP`N72y>!&o#Q+f^{+WrezxQq z7dlr|I?p6WgLqeSwmM_s2vxg*nFFI{xrG!m10-cINVKw~-5g-tfk}9xBAv&Q2ujTf z8rl#^_S7PW=9#5P&pV>$uN##VR^PDl0NrzrQ`fZM^P!Y!2x`nP5tZ2AmDV$gwdP)M z@2IaGTvhP?#X!~?M}1t^l`oftvs|U}YGF~3C&GZNxT-Rkr5tTXFD?7D+X(_Za``N+ zXcxDT8eitcms`M>d3_~5{@t~rUN5xG%7``#LyBFyP*)N$BriVau*z<)CQa)}D|&Bf z(Fl?%4AZ|<5ja^nH(1buZsc=X8fB3y5{sCs7VREPL@40HI=fnzmT6&ds1^~qGLzje zXE6D1b2^>IejNM9G0j28 zL6G~vfUY#G&>-iYu~-w-Zc+3vJ{aTX{EkrZAi*`~rC4&*DlqTjx~wRAPlR`WjGx6U0c6>`>t&_Jjm(s@nJ*G zqP<4z3!VHBsOZ(!!4AGw8-Y>kY)P6<3mXP%B@Jhp47k=fS@W-?rMJgy0;tbcwXk7@QhYj68+a2>vC!1sO7m<+FC5ZY4@cXH^x? z3V3;$-d@f^*R1)HT)(ar*HvO!_sTv>O5|i=$Qn3W#o66)Axv9qu%w_^-e2|A&A0xBq_sY4Wdg z(bq)y`Fw>ce)Vqp<{c_Ys!M1K#Z-YY2u+lPWe2=|On|1l3u1iVU(V+f;G=b8Etj=_ zRXbx#l(1Q3m)LTU)?jl}x2N-&P|QTAqi3Ixe*gRL!CK4tczm+1k>zcf4mo!zgh|>=jr1J>WXbn5r#Hs0J?J0?C z0I_ek?T9+T6>mr0S(0sc06ivX>tNZ}1WSj6!axo6_U2Z&7-|BXx4VsHtoQHVZMPeZ z>1tB6z+a?zazM_WPv?uB*}gw-0Se8j39O>?0EdnflmsaJ-B2fNZ7d!i9_{2<^)cmMJtt)^ke5Plyor!BU`mZp&!%p2ev^0*3aCObabF<&o6P26|?Wz|*kZfL&1m z*8}}iS@s0UWmDx#eiIE)xG;{uW^|blteHbk9i6sVP=td6bu~ z+ghFu#SdjJbi=UBArj9>@zV(MMRMwZ-4dY7v;ZpRQNHuO2zREuY#D#GApARZW0hZg zxtCUg%W(B2#U3*B+@UT;TIi&?Gj_$>xmr@DJT9tV5Hp^GokZ=qo2Szx^w;n`c7%B( zZFHP2L@L>949mFH6<=Sj`Xacd_B)u3*wNH5X@PkLxf9YMDpIEcyfBYPdX`8BmAI{J z1(f7)3sfjAJVKV3LAtG(OKQG*N%G>cb`P8Qm$#3|6mRUH9Y_m;L0$$@B) zzkx3~iH#A07ou2g@$`)R*X*~OZ@>BW!-tP{0B_&EQTDvLzbX5!wNsN_%@C^nQ;Qte!gw9P zpJ4g>>gtLXOWP%!+1+ca5=8UwHr@O8?-?I!b+kWy_-V84-rn3i+}{J6N<^%sy8HW2 zPsfMb>zf078C#I+g-lo|ZuXcw2jay^F5FAU3yL}8V7b=vg=Nb)k9PRX)6I+}+$?@1 z5)Vxw7SR*sE%*xY*Ot>m1mVp$Y)zc*#4lhpEC|lCY#_vofa8+A*?E-24=t2&jMP)X zL~e{zSJjJ?k$aItgrmnZXo{d^Tnor;P))NmVNNKNK&p3=nC6QSt>jm_>WYzBO)=Gm>LrL$CzNtI=NM+ZXBSgNWJ}_j2^BSZ0P9qu^)y32S~GX1#{)PFx)Dju0KGs$ zzi`@c_t}=K&%aSKN)^ccBRzB1mVE#Hds&r zMu^k5?Y14X4(znd0V}3%COJ*a&Z#)OQ!bYCvvC^n<`(Kb9+Xosun=L<#Tjp6>nt=A zDj_=Iy&OEUAJuw^`#o2Pi|X*^xwzBmUY2Jz?S>;;3#!daO%|oAut+&86;+Ow@myW6 zD_(7@AVEV6j)r(ep{L^i+~QNq7IrPUew7HrTW?x|AWfG;OACleL4EGH1Ikw%ZQB4b^4rC17Qb+l0n7ZhzSK!)QVD;qFsQ ziSD;=u2HlyF-vL@2p2}rs;;Oi3FA9Yx*}yxQq3-hX0~Z*4GVtHdNK)tE~Prke}L?% z&jKf5EW_*wTQGoqw3dmyXVBMnaEt(7-tzsXLtoL7Kha?B+hV>1L7d&TL-*Cx=!yv4 zoCG@Wy7tZO&CFTg!{fc3klVL!hQ4pCeeMS;eL%Ry?(+2XG=eQXIOc4eP!5-3o;Phb zabC)XsTG;ea&6nDtE+v4JU~;-!9j|qBSr0#RW}jaLFRkBH^>y62HTx2RAF@+=eFGn z*N#4t;muwd!G(4!dpakA82u}7=G)@6i}XcPUC^0tFe`!4M@x-F_Rd< za;J(;j=1D7MKS!4I9r(=@<;uJi{v0jl|;#3>=;?BGLQ43qosUYz^<_7mH}q5wb%GL zg2yT+QnZ%qhW1&E(Tn9%eDBel;~km&^t-R8|M{Cw$I!pZs=X${&k-z>HgzH!sUj#% zpLe((O>yO(ca1g(q8Ww7qo;CFd@s_by_+(uUb(9escNPP@Duci%?46fWQ=iAiU+8< zBYF4!{f|HX&~4gj>^^BvArU&jSzKRTS=h9nNL*|c#GvR#wTwV#?+S}Ks51a; zB-3aqEkm|SzTTR`(ePnpQCx|^MiHn+FmtNLfP%0gYu>)K<7aOfZf|c@*Y9`x&9?jE z-8HWL+_N(LZal75xBg1QgYF-Zy_m|;fUGVvmudc4{KYjY`Z-0Apb7P0}`0#jj z*lBrXO((hh7xb)e_n0-&$vRxtyJhld3}3v%lM>wy0az(g};Sudph? z+RectQD}p;z?Pvcmd@*H&=t%DFfUeN@Rk^FS{1rib0zoEB-XNvoWs8QA9=rPJQW1h?Yt+F#4r}zOqTxfRk%~sbAe(MeQxe%ap`KtF z55=#1cX#*x{d}hv(m}E#1yj;F9A>yHO18;53NUV*I-o4n_ z$bJ?L8QANbTlSYh>mP|VUoIEAXvsyW0~i{oAAk7XPEpqZL$u$&u}8HQ zbDYAa+gdZaZJNV=kA8t`n7V5$RM}J5gTFxiL@ko)uB+;e(D|^WN5#+9%#sT@7^nd3 z46lOJlBa>~Y*o6tMGSOl#;G0%C6+8L5<;dUtUEYFzGSB0>Ip&0T zop3HDf><8ULBvQi(Rm&s4QC}4+(fgmeEzgYYF zBC-iokM^i)5II)Wd6Vvpk60f}m_>&d+>IzP)Z%Gx`HB}sQNv~~O%!sX8544D9ZJFB z3$ZcNeHbJdcyZ)sS$RD#mEIiX1!w*xt}TcY5maCa0>}ZX3{uNKl=UdkFOO%}sp-lb)!Gxp? zw)6%9{f_g|R&X0-BhoOUC8uuF+}vE-S^4?H2RnE;DTW{?UeZhAbqH_YzdaqF?mm8k zzRahO5BK+0{(t@Tm)F<3^W_MOd8ok+mOQRAG}EmYhtr<2qw9jPV?wIkrBS$!>1 zdRucXI7>@}E)-byS*AwbB~Q^YP_j^{wMMdrp9`eau-FmbYw^uVOodoj#bR~fS7jDY z0m(cAETisWij%ZNG`qGFwb{0o*C!@mqy#Fii9R@Rm@HA&d66`81yPySm7e|DLa+Kf z6*rs-|5`R7y{x9pwUiznbnafe8mK$F_U^xne=*&g#;UAZz?&x zO1aL z8vm$9Wp@J#avc^x9ovXd^r7GmGg?d_ZjF#XHiGB#r5~^kLYkdo-E`7)gqihZ<^Fs+ zkGHpPE--)SpB_)_ue$U3quo)B2u80{bc-RNg?)>Su`uCN1>xUFCBuMa-z33hM_s5X zxb|1*_PK_6ML)=XRvjL2-rDO63$dx&fN^L|luEaOwjBA68)$o4+Owb1VR+_{4HG%* z;T|6!t!=!%zP4|E^PAt;>$$zX)fCS1;^D9#&X?=MwVl)Z`#XX;L$|S{Z#72jhs)K~ z)dU?>P*6bh&av;$=qEtc06et;;HZEY0b>ecg`r>(Zj%znb;ag_=2RpY6a^X1lqvt> zrE_Mvun3q{)iMcBx&eoxB(`DeM1d|#sE}7x*b_j~)7u@syKKL$`~aH`ny5A1{^mT> z^@;p834Cjv#8Eczq)3YWKxhcM*@+=C;31+65LVW|2T7o&xLv)Piby|il@?mH^hBA^ z0xTCNAc|hfF)zx{0~(`C2GY74t{wJc4n*=QtE?V%?DRX&M`j$@_36$7w z!c$$! z4Y_DYe0Z|)pemxOd6v!>Kx%OS)ew5sHOyQU@hb0H=O`_@J^~gW2D$=+i`HY!SSSD*_7ME9*@Vv;UG zm|Qb5mIkqaQ6k z3?R`5r~ZJ!uqczD0Zplf0j#9VH6^t02%CsW0UGF1i|x}i=$+D&LZJrp?ntO3yE3kv z$Q1VzKT+^k+HN^o6L18dt|q&c7$cq7U-8+MKIOl#_J_~>#bU0G!MjkeVyOoXx&eE2 zk)9Zl+D4P!%3qoB;_PwG496~sQM=V!9@T;5W4LofH#S1Pz^?+?tPhHqoW_lAQSP>< zBT>~&+P#iO zUlZZy_60Muw>Np)N<307vT000hXRb(3XxY!mlx}I;mE8a#7$;R2#JrGF4K(txUV&< zu`JjEuLu%gOcxGDKqZ`%I05hUnxoirY_^+y*wdnKpD523$e3e1f;+)(x3$KRCgwgb zIf*>_$j%7808K?p(z1Xv4l^>-B)3v{5z#vy9gpRM;oRF}v^5rK4JaQ0u#y@GogKew zxJ>zRz^;fUd0>)Q#|F0w$T}xA4OSi`j{B?^bWn{MyG;X#!{vB=eRFgD=HcPa;*fQA zRuSCZ+#t`z!)DtteSm+0OeJ?A$k%nQiuaY(Ccpe~CEGm!d$TUZ!CuxSYzc;#oy z1)P5i+Eg-!)f8RLnIU6@=xj@!t~4)A`I3a>RDvvAOKeoF6ALf5PCo5W>GM^1FQX*; zn8N3IUT2}Y{c49)pq^B_`4#O?V^vq?f1X)DWc*|go|!Y0UsW){_GFmo{{SKHKrRrT zJ$f=iuu>IR1mAF*R~&HtIyFfuAtY#!*b#|$TRx`*W|H`$g1`zgK|(Q`rSO7VmaN<= zQUt#$CPxGb6RAt!5}+^4t!3wzxuk_YU*T*r2jR2NOP>`a8&vy9SrP?M+4^0T9$Z)! zdObDP68$eIv4N!_^~$A{PWf~vM|xNilA;Rw%1AU*P@72eOxhM|e){_Lng~Cm$*85G zwy8o9D=DH=+|5}E?Ak_7Z%hg3%DDUs)YKl3cOhcJO~axuMT_&4p5?HCrNo5L z&5P)Vl(;2kg5H#4oz12#(cFsX2G&k0mYK5ykuih}8DZgV=FHrr;qwIqts)8Vj(a4Qw6Fk{59 zQS>#OD7#rAWiXIhH;)6ONLh-B`dvKc@V_dPFV7()s}lT|Yd~U>Xp|&(#WD=!X|c{8 zQ$C{RQN#yX;GtuiEc5wl@sY6pBol8){3nWg8Yp2&2K-1I=fy5qA0F(ik_cr9DkjQt z)K7=jD^fM3g@nV3OG(BtAe=BbMerb!jN^X4Rk9R>So^R-E@TV10%R8Ji8d`?m46mW zSphGcLspw^^U}J@3Nt&~fC;-IC8J7?kH0z#dh$=upw+%A3?NtBPyE6!fKCvnD4a}} zr?A=&vq&e$Ly;Pl^^~FG0+c1?FU}j3k5yrAXdvNAlPguZmid1OQ^%sL@?^i*JFeB;ufQpbVoRg05z=&g@0JYxtgi-`2oT6wQ+?`&!x1IL zaS__g?%9A|zDQXx$zkkE1t~Uq0k0HY0J>-u4yELfcwQl~l#{?m(7&TgJk3?OytLjG z^HA|6f^X`E)8||aT|i(Bs$MD?Vv#E;$y`0>YWU$X+4JE|j+13HcDzZ8S_Tl2BBk6dA8!EexJ^YvNatot z{_%I;x_|lYr%N@ywu7&U@bepu`YP?VxfTWwvMrl-W1XN@L0q3eRswddmtj%ft)gt? z=GlsN`;$zAMymrssX0LL5~4_pU>ng5!D8>5H*X*A@3E4J>C&GgIs3=cSstVb^VgDo zj!#cq9M+bJI+!Qm2q6?Xp{2lz4%d8=j28O@?CvFABjAJNC+$)fsUj>>HFa{xwLqgZ z)k^m!aDQ1+2JI>BPwNFOz%g~r(lxcV)xQ7u@#FbyY5LT@t*@^3mh(P*_|W(L{r$aW zbi3WI?}wKClO5kU<;G5eWy@;p;GFK|w?`pw@v)rOx=83uC+}NIh6ZSS!}9^^Bw-aT zBPhg^Q8EPzg%JNwgE zmxfFyb6KZJ=(Px-YZ6<;MVB^ig`()8R$e?%LSDp-nGXV7u3RL=nukWW^Q(T|I`dq? zP;T)*a*Jd0`cJzDD;beMV3&x)0Ku|mJ|4BvEV?RL=n<)!6~3HXEG?E6G%eOhetEWv z0LvmlCT%q)QLNaZAbB9vyaU;%Lglk_+hGhRZ} zIEtu*Zv}u^56_8c0SD2@I>~l1S5rujZGTr^UWK+_6X9q16{@t~B;fr#VHyC5grgPaE?7;*?Aj2!0GD^n_xH#9 z$43=!Smvvr5f zn;SdU%lT|C@Nn4M2^(Mw9IS^drf*I@b^+U|VU_@wvY>nzaLTPZum%g0W3R{*>3KjJ zSQH=HH`SbHPRx9Zr78<{0rm;R&u*r5yNr%=_4_mJb})e4;~H6QK^$F(KW z6RC$Q`r1@_mZX_(@DlAJ7Gr}=tz^qGk(rHX^yXIgh%7`Qd zI?Jr^4|8cb3`ix6)=RnE5t~z-Hz15YN!cMU3)d`6ZxE#r;x(eJ$2l##2Q+`g30VhM zH4wpxA^^s`SSCy`sY&TS2})gx$u$ z_aU@&IO1jCS>?)qOnzwxCH9K}uDLp=TZo59SK@fB#ETdZ`4E>!v4ujCPYP(^9 z!e&zYXT18XHMus96?NBWl3eng6k)JP^;NN3ms^LRD`{R=n1$w{#bJkRl)(Nh2;@b? zoRXo!vN8)|xXX73_!Sy3C%3fhCt zFTVY8_9L`&>eo#8ng~Cy!6>17A#B<%wwo?UH!5qoA&kIGl{I3aAwEg{4Cul@N=8Co zp6i}j7>c5Z=!#z_z=@P#_S-FXJer_BJU#|`{95Fo2uXW&b!AuS&;ubs&|03=#i%Ew z=2_Qm5P<+ew5(lCPym_iYHc7W)kZS$$KfTJS|w;HxCaF!g^SIu_V{=iIgwx{Pb4l2KU`}-`!qc`Rdm0_36{6 zFTecq`uf^Zrg#xBlL94Uqt|3b`gEC){aesftk*t0K3SW1*zc_k?Jp;b<)kW(Q2bi> z;CB3n?S|%zIOF30mH`$#lmMupe1?7TgghHu7S_@*p!E9Z?U{YYwbY+NB-(Q%>!wZGB zmfqFY+(#ZWl=C!6RyqmHiKMiB5E1M$LtkF%@Qc*)iS1=7f=wbryNg2^p}bes_U z^Vx7wO}rR^E1R(B@Rj-eq)!F)-R6QfuGk>jVNDC?fb!X%V@vleviVFO)FW%&zCI3K zKD6vPr9f7eXlj)Hk#gZWhroQAP7%1uD!qg%NmQ^{{YAO~3PMA?wL`UH#SG^(l`d6# zqa2x$&v-bQGdT^qS>xt)zR3m;cotmvq~uR9C}9?KMK~s}CsZzmoE}Rp6-Zagu2)pq zmbe^KAzFN8*gU^Y@tdFjVHmc~J>b9m2nWp9T=<#@KO4wsx;~8Gyi41SE8~}9h-yJ; zBKxVKuD+!kl-|JGp@Jseq8j>(U8h(NS}dcBZ{V!RP24Ur+ziwPFiVpcnA3{s59(pq1g(Lhbf99w zrW5U-Wy8*gO4|rb)hG$Q#!UfdRFZ3NB$*PS3at_3D6bWXk$a#iQDXC%=O)WbJ# zZnwLQgXN(G)>(x8$H#k1#&$w2omu~qrs0uIN#ZoDf$;@`Y~K&(;|XZm;WGB;L)vY- znmtrC^NG&KBf7W-ta{PZFqTZo9#(;P!~$3S4QM6k2(23^rjisWp;9Px0BQ)uMLGYt zNbe-&1J6;k>~~3b!0gY<8L1H1=ZRQiRyH&!(XU9AE?`s;S|*hIlt-yagb0?K?C~a= zQd&B*dcyIXL|O^Cg<$R=Ygf@#xY(M*)zT?v6y@uxl3-~?&W^p%$v!2r^pl7W;5;-E zvC~;Y|DjdVS^!X)N}=;2k>c02BIm}St7a~qg`X6C)0LjSfMPK1BXxmv0+<^u8_G;S zlZxU;i;-bgYCy%1DiCec^jwYU+5Mt`Dmq}qfuL1IO%ckxu_B96qe%KC3EGrOS)~70 zh5JGBN@}Dn?zuuPTC&AD&;uX}Sj=J@QJPMvld}j?>LqN|lXjd~=n* zeqZf3;6+Rl>0)vSzU8%AGcG6%II1^^8Ugf}8)o7sEl=)tdj!{c)V?QTmc^kBB7d@I z4tQ#|Z%$GLE8e_*IGvB+>Z-MUO|2`=qhL(ZehsAdNdOORJ?=%cOJtqm-9&w; zLYlyhA*2A#ZsJvm&w^W~o%XsYq2MJAE(*l5MAV=*6;<}i71eAJP+@mbgaQOml8YfL zEMJzI>DIY_>YDey`O$@+eYVTs&gUO;{$E`Dy?^CW{6Sw6Bq(1K;eUW!7*L7&nJXpE zL4e)_q|icrgib`5({lL{t!Ni;MrzDLSfB&((UONt#--^V8##UAz13)}DK8LL@D#A0RhMRv)g-0dn3mI~zw+h}N2d%cUp3 z8}m{a@U@|xfK6}OTGU{LuI&c0o|tM{SZNxmXAx6Fz0?rKNglswEDDFRE_J;yir@ln zw@?EBz?Jq-K(dVJv4Dd>oOpP;I~?|wxQ700iR$XGpZeZz4@UoZYFg;tSmx_5N6Uq` z*H;#i`~G-$_u=EuADYT<+Ge-2#?Nx3^@!63ME=QYfWv+VDXiMw1&phwd9c?J7T*U+ zhNWy(i$u1-r>851_|2#YXw&0r81Y!6KC#~lMFW##fI>=OpF@vw-@wt4?g=#o)@hKC zCJWNov7zu^RHKn3a#i+NVx6tgQmGJRw2^!^Z=%Y zXG`0H&xjJ(0cWPB3Y)ckpx2|lQ>WK0g&`&yNRUOT{sWvX&eJE+5B4Ot6ok{2(F$c1*r&aMzkc*-LQc|H(# zX&(cgZ`2Fp)S$khpaE1)0pPM^M3upKK41I>t8%k173?oBnw<|MM#U@Y)&v--dZW zHR?4H{=rMaj84L8;@+jH0_Xy^?JajjVm2YQp@fD67lgzbRFqUMw!G5Y=e_N=R(?M| zJy~yJHNIVGlw7fre-;%QguJf4^dvkqu(FsacZrq$G(J4sHK7Y#-F3}4pyNDWPUBqM z%%xz1K^U3W$k;J*f!9IH8(uRYnFGdoKILW3DZdB{gs`J$(Dp6mB(;Oeh~lx0!zgziLQK&`(Ms1c(|dcFPwLZ|u?a;))S{cclFe>s zofsNfhio?jlL^ZRO9_^rVbnu47p!JI32Nj(;&f%MaHMAtRnBo|Q^pYWh&M8f!%1B!#}w0RtTtdA{gA z0duSjoo_(2y6up9`;19ekog%CZyJYW(RD22Si^7GaF)1ihSxM4qyb|lAA#pQYe0$@ z?H1;v#M@jMS91M_F)3u+AtflOH!||MVVp(HK>~*tX(bKrVyXmnKC3U#s%9z!XjJYN zAEjl}sQrT8vaF?tX2c2?tzjNtdRQ_Li z_dBM^*RR(d;s5K^gw=+J?Oa>sJ|Xu3aaKv=D+>B4kwC^XhE~7=$Cj&TP^N!A3l6IU(T1y$sX|f>T0v?kTABJ z%h216AD`|Y9v;U=oV5ip!|i=H(k@Z~ZhVA8OmD)=ID$?h(8wJOHiW(y8lBXNA=1c=R;%NK_P^ z7oj+O2>H4weV@iSSyg1uGID(`XI=DKff5hc7fXJjBrO>svEW(C3%w%|zDagWEzW0> z8uvnaq-8NlMwv}KFr``t7Fwe(f^EgO#H~w36?06)P6{>4AfBYvq649^_Pl@VZom1t z2^St~Tw_n?Q|Ho^%m2W+f0pwP&Jv;fH@^C7=RY|2lS~33d=R2TFKu`oI|AlG^*apKe zTE;=b1Fbk)Ga4kPz3iyaaZ+7VqT%CY!Dj&Do(M)e23<;a%umPTfc}QOX11J~-PvIu zk5876(43_t3k!ir?T*$0N}a?SB@t+|uk_tV`cOZfu* zD-&-v-F6Gpcsr8AVQ-D))8oT-vY$1=d=hqu3>4^^k*Gyr&)>G4^-IWezQGy@h!J;= zqowd&*I|M;BcuWdnv18n(2Or|iL$v^H+rS<^2J-gk$4D+xVEgXh~%7MhE<70O0b6Y zo-LF^EVg$l@MYRA(xk8wLZGN|!9KV{jng!>45X&HClCoPfK~z6i4^I?=FPYd*AhiNypCWE((>UyUOa7iA`idjm zV10`gif*{Q_^AbJN_%}|r(Xn(gwb8Wvuk}t(33@ortX{2TkTuA zd`@1GS*_r3$;yNq=e~CCKXUHBw2m-W4=&uha0hT{_0hQ>UH&e|?|uB3(}T+=yGEQZ z4)?`3ehXeaFOfsJypU&sAgt!j-E4$V%L%&FKC7H4k z$>S^*R}}1A&CUp-K^h}$Z&99X@B$w1AEhk<{KRDa=v9$UCHzt*@^;_z`DnT5u-_d| zC77;)&h}{55elzwM`+3H_D$1mHx@0v_~PBipFdh6)X-_>N=(Id0!S6<-x8GsGev!) zc?CeR2$Uc@UI%(c2sD7bBVQ>+Lvu00Eea`>6(|iv54Qc~X#FIVk3pr+UBX@JCXBs= z2qJ$# zvTBl1^hEydo_k^R|&)CSQWh%nq%>AwPBgm6Qu}^9cxfC^XciNi9%w1VNWq)=P&{Gk|A(Ju?TtGF)xkH+j(_5g z5g4KD?B#9Tv(!w)2Gw$=Ke_7Y{JnD@a{k%5AChH6_hXLs&tpy}E1JD~%I>Lh7YM!i z*F^aG^@q+!6)mep)T1zSp-QX`cqOi${dOn{_s9)R-CQmwPE38%HkMnQmL-e2qiRwM zIo+nic*j>y=#3(WJK^T``r+vbNXO)EZ?Ei@_xJaAcMsN``rQs#m}TR#`b#j9&imbt zYR4Q6(KOLf2M*m;sI1lF><*M{Yr;hMFi3Rx{6&W81kea%tu1+=7Z$7KGsV2+9Rf@{`Ri2rNnp#&yk$#G z@WGnnx`ogy2r)B}m9gfSk#3-Rjd-MqR8Hv1%ACFwHUNV{v719sdNzymLe__uOes=h zjnocxw|SyoTby#y$Kf8a()`j%xuW`iFqI=-Rra#VXiQD1S8wVbKY{64$zXaQ>b1UpW7@&yOzMJ8K8iz0Xhf zfwzi0gOB)E&iy6+%+@K6l=_bUf9()o6X7441hw18G_NK{te8>Do6r*wkjHrvh*T)b zt_>_vAstdza%j_vB~#LxGWp(V+Fm@zF71u@y}h%7u=WjjnWTiIGfGmH3WMqA-8pk=!F?V;zpy1G)RllbS; z(-UWOjrN6d>y_NT&WGLJ9sz?f;we_8679p}tfXn}etUa9oh_qpcJ0vH?|Vyu@7}$o ziv`p}G>^1b*EC;gILRIq^I>~raW3u9+NMj)-1FR9lVFcy(a;hJ&m>&^m|5!6(E{iY z=&nO6ALy3#mr44|V2N-78j?(Z61faXhe|kTSd>iE5xD~EQy+s}-~?HPDv0hKp%C2= zK2Kv<MZ_j1#K31?0HO26Ku(UGvXtt7oPyi6wQ=psz z`J@TlZi>E`6u(oU?Q3+O^OSF9g0T%ck8V5bQywFc$F%)v6|ko3S+ih%43 zIw8S5B3;?j7NBKmVORZd4w(H{_ChpiB(DQR9f{7R@Q=Nu1{*z3!d&=-R;eBPrf0D?Px zQND*NJ+vBPvOjq-(g}m!%8A2PUskKWh+)iVdF8l^%b&SWCiEgtJi`^eYNX-|rM;pr zED-zV=BxUf|MVD|o?5o*C@I?})H2JEU$=5zd%&dNesIoe*mulx6PATIj|}?x&bdE# z?tk?D8sk$pfHmc;3huMBPmcX(CCt1&@oOS{{rW#fjN&>2gtzHFXXHvnmx6FCH8G-nl%_mH*$_H$BT)_lyOapmUH?{lW@ z%f!#C!ce^6iwlX~5|qy-3O^`=l4DPEW-rG9&dn@ijstb$>ME61BLfP{UJeh^XmRM> z*z0j(9HGT#FK`+yH&Nj--QK)G(+|ys5l|FS_^ZXG2t80m!*pso6ObvLGy5McP$HTh zvH;OPA(X?<$kRs;32K`A85%=f+fA-aHFA-!gZRMx-6y$mNfy9fn^k-@Wd+4*#A~EL zghd^a*|9G00G#n78a(r965K4Z-Bw}ASf6FAc}J4<~6Abpxg@ zp&2MTI6F-Oy!|tfDr0v}An}g~YW!u;`9GOs=3o8Er7Xv% zzo6dZSJo`nn5~8zSAEF7ry=Jl=d;VbbN2Z=J9l*X$zhaA#Ek$yuZi&W>whm7+Lbg1 zRo4zFELM%g<72Uy(TCd-PY0?(3ZaFzz9WGh=JjFDgB6?tqW*lo zSdJ^6cEN)YQFnHm%lTvxFu)GC>4$OZv5sro8*l^cma?;_zBBhiaC8l@2;E_$X8PyCB?$B{&BOl zv^`z=3$)X6vw=sHc8U;Ao6qCJ1YCt_@i^Bs3vuY1u@_qD zC~C4fO9U9CsY$CZ3}|kA*^*PinM^@wp}8r}u52wSdu?yRqBlamCMp=hbWkN|0!iKQ zk&|SNRlKzmyrgD zc+!pIOTh8byDFqH(%&+ykPhy{ifskgepwR^P;M$q&r{7@)ZimATOQBsj|JV5b}Kfj z(@2H!qTYwGL6L5gla$zvvsysvE@32C{2;;dz zgZ8YTi~8#7`r0m5I~W*^K@%0qtW#1a&8&t;t_riAJt*FM5SA)S>m<)oyW9zSEWBw| zwwq9~uQ~-kZy|P^Q8~ufbOfXhtG$WqNL(j;66vX2vw(|H_c+?~C)p;57W1 z>we>!V|IOxLy9MtPtKjax1Sundvy7ui+3)5%<)rB_Ro_YjrafGTPeOK!ar;-Dh5Rv zqA)?pU!}Mw5k5`;7%Em)zVJIDMB#9-KBl%`@8F3uzz(em02IpHnh2K^OA)9Qi^a7W zjH#KH=#v%lEs1aa85oaR;-ZY;aPRYy@h#4FQlg1kNMQLC*q}tR5-k``gk~VDV*uCp zh*^g#O^Pha_5C1IQfnDC=M?JQzOk5jb92K@VZS;D*^yXmwa{#hpa!J&r@bT*D(uD< z-XHHDEMN|Ozu)dGl2{n++6@N6mJcj(U3%*$j*pKt5`fMp5}!r%i`(w3Ev%;ul2#<5 z6fh%9aSx}b+*5ko_kOqAc89%1GP@V-d4cEXfgo)-NUM=-$j7LsajFPc! zORfaLgq=9iE0lQCdxLMF4;1L9OOyqdaG3mQbth{G3+6lIS3eVUPXoF@nnulqB(}d6 zk%4ubXFCHUc8BGC*EX)9_rhk|DSK5!6=mkkx@m82-<(b-4TLFU+fFEh+M?D%a3k!Z z;5s6f8*&kp&NQ0jP!6S6s6_Lj8&_L5)dWq3;yAy|*x?VPcUdb;Caok@cLw*oWr9&3 zjgg7C{)C`%10JEuTV?8i7m$d(iI}(2)lyF+vL>#VqDAj%50A0!o*Us%-^+!3_`!r>1JgZB#UeKMKKRW-vvG-;@wk2nFSVZi3pCR+! zTg9Q+Vv9|**jBd%w*c9O0Y4Zr1i$#vzsEmGuLcAdFzg2zhNOlXkea5fCc8P+aPQ6G z410|5x7Lb%ZZ!-DwpnTpmC0f;nVILDefHTqzF6P4hG*vQeVU-<-fhilvI+6J@} z+Xp{9K3>O3F{B{k@o+cE>W(VG;i0%0i)G zYt^g3(2f%9Y*4aA*^%cd8ULHIrTXWb|J?iWAQXt*1 zHQ)mLhh=k|=fTePez&(bjwKJY4%8?laxmZAiqJT?u4w}jWii#U^KG!nYMW?2RVdLC zM87U*OG86vT89+G%x=layc|2(NnimaZrt)|W2Vxq?3GcbjibMSfr_RaKF0#?>b*T- zEDQQvI?jM*jSs1C0K`HM8IZ1^pC=NG9kUx9yVZNo<5Td3hdqKpDly8 zlq6qLHrUCo6DDvR1A~^66Nw{DsF~awvr>Yp6K4<(q4OXdl{c=e9fe|&zMR%%uf;Ap zvnEhY(PiLXXCf$66^vUoH6XZ>oSdX0RcUs@e_K?Do*c{&;vvBFs)|mS6?8G?!aIg7 zxh;-(Rl%{}?pj;nIrm#v+g|YlV$0PYui2Mv7ydS~V_O`bw|s~= zDFl@mK6P$orIW@XyKAS+GoeGM=SCAE@6qpZ(yp6-3#OOYD=VT6Z^VGM`lqD zC#v7V%EYXnv}9kHB!vUH*e=JZi<5$J6y#ITZ`oRAtEa)Z>&5S% zzMEIW6IK*jwPH{iXGHBSC@NUiz{V&m``P7kK3N&OTA~Fbgtu`<{ z+A23(ky8<q%))5=El^NRDX^Dln8(pvM!apk@W+SQuhxs6)%ji6a9Gc`43eY8jXY@=bj%>(hKT)qa?PiAOWwAs0 zOHr-;{kVVg%kVeIqW_V;Vzx!y$X|BnG_KgWYIXJKtPZD33QwtcjFem0l0wvcPVT3{ z{hPx5Md^O&(tsVE$Kc_&vmU+_!e12;CN!ToU&jqI+hxySCG=-(B?cq33UcX0o@X{N zL(2`tveYbo6-TjzY}qN{Pkkr&8e;Wluqp!XbNo%nX-Alfot|y$7OZrQ!vJc-x==b+ zoKENKIG)Z=1Q@ulhhZ$i@C4L@(`fk;dl&vL(0hvIR>EdYOok*$;ZX7 z%vz-gk|$6eZ(zOFjb=1t_JXe;=sW_h9|{k^to_WEQhYSP#DO2p%&d4ckg57lFap&c z#nA(y5D3l_HTL4{M4d>kkC}BUH=IHc03WICgl6V3hz~-evXdQ7*Y)D}+B4dD!J-K} zxS1T#U>}M-&$3*fo~{b|#g)~(Zn_ag(5$;R@smG(3gzY3$_I3<2e^DX>N>qk{e5#4${FcZU|5`qO!U zqP!^U)9L#5{W}9dQeq6YH=fQi3Yih$io0%rx#_gZEF1$7SEG!3a$Odx<^22iZ%?OF z$A0EoB3!WR+J}b+l(Bgxr4dw~x~U~J1l_c+Yy}r6-0$}?BSoCvZilg42UE9YxY)+r zif(LM8G6~eJCDodbbkN-U10uzp*=et_jh}H3-*vuOxG(SVr^q@3yvLzyj7VMur*Y;g3bW*`T5fJZB^7>Fnr5_3 z8t7oDC}(4!@0}f!&#AVeAF7AuY||jVOAFhDl7WIAT*N_t}ysX&AM60aVf+I9= zH8-nLV!TxXe2V=6c}b)s;h9A`W!pD`za&NnI_ipq$OFzL+8g1dO)W~avKbPYjniEA zBGAMUHQUFKV+mDdsrIL;L5#|Rd{O8WVdP@qy$Eq!*j7+eKXWj#@s!R6<;2RSorGn? zloWTE9l$h?xPaTvLnJ!4ZO?uz4C*if#-V_urlGz8?3|GZ6|lI~i;|dTY;*IWF+eG7 zaY}@Ql%nMDOnM!t>=E)W49Adzj*~OH@|*;@c@DBuYtgLT-F0{W1)HM3=9ZVEhQiKc z=UvD3id7KrY)<}-5BH(`t}8!F;f!|pdS2Iu7=P*Gzbn(vN`DD|V<`Gk2!D4m;kp*% z92j*SLn@SrHe(s{Or;RKV+SG?IVZp&9v6NzVbiEdDd=q-eMUw+xR+Md;6_QV|H7$$ z;G$~?z}7WG9fmkLKuU+hzG>h(VLxOT)1%e7x9=WkG7SCuJb?u|F6+fG*d*sjyW7Kz zHDXmKiqa2EDX#S5qM3A{ow2=0!kn(cGG|+CD~Js;o(7T-{qy-`i;vZ|oTF2aC!&-b zIDgi6cSn0Ut8JV=q2M7+V;l^vwUAVn>>SQd&yV)46%Vj%>^!`GKJL0*R~wc-KR;Ow zjWxpaCJ|wv0W7IA13Z2I{{2jWpuuiM;aKIumLySS?Q5;SWWNT1Ia|}8Pj=)b@`vpw zD?7zN0R#n^)`b0y{8QGf$+S0B&s1T`v^j5v*e8u~#99f@QZK$dvYTM=QfBWMF71*5 zEZoX~7{U6j?C9A!hWN?(L* zp$`3t&tBpJx%S>6WakAh=CBtLH!6?NZAJo~5GWRui(u2Ct{{zA{3i7gVq~3zq(9A| zv@VE3^o8U;=$IDr8s+8&ptDd`t+(3;kJGkkx7#6=Y_Q^ph6MGjc9J_bN|h>%A$|gD z$t!{i=uOd9Xs*C?K2otO>jSj+nao9J7%#Ilp+h;Wq6i|Ddqti!tE-aH5dO-Da9Rqu!WSevdIhPt9DO&p!Xc z3d(eyGUE|c3kKZV!Z0ihM~qiQ2e56!gNh)4_-PW?JQ+@c0VZ2;n32SJm{x|zgc{9K zI&+}!=Me3%!yu@F0T-&1GH>BM-SzGJK;W?HwgOUJ(aImC)1)+l&DxuR` z0PohUqf`nO^0>hW3DF90e1*E6JX;_ktEq7TJrRm{j_{DTm+0+`M(9F^5$31+ah{r{ zL#PF6h;5az`(V2a;o8_$uRI}uvF6(k@pb_{$tOM9ZE&qGud zr8s&dFk>GOp#Rf+DmNN6K$U$c*jrJYmQ}{u0f4aIK`}C9Ke&W;5TzaHVKfM&f-#xO7-7&>9*Qp%QVVg*40gnxP zz1r3+^aB@=w*%K-FAf5GXwLJdX3bLbirp%K~5DR)!_V;&AR6 z^R&|T;PzT0K;ngCEi65U=ox_HXm+vK?LAHc70XgDl2VYD0h*6t(^$wvBlg|iDrEyu zSR*^o7VqoDZmqU1yYBe$N3VCgu7iCx`iec=Wb(S@X^@6~vdbHDiu4^f^Qb_0SLl}A zfZfcMJX>Rv>FN|3Ovp7sxKda4Vr2yzOe7i*wAx$VcRhi{Xp1SGMl|}uDT=Tr5AvLo zkEJbUHD{>y$Cu01_A-69GdvbyjA?5)w23*t2`x}y;e|mVLPjD?EBT01H!O#^LT z0PTXBwObW*`4R-)s|v-Jl`!GMlS(Wok_s!OLhh#$)6>zmfYUr3@9yj>HLPD2#rEz3 z={#BH&e1y$T)fVt6m2LL8aqhaLQL3T*r}_VmR2}3eeBc~$E{m|7pwlDtX7%ZLE9kQ zDzufcjT72Dap8DcF}^Asf&Ei%ACf>;x>>(sK-#$OxP&nMR;5I!;|7@|JN0MO9FyaDiHjum7W8(W*{M@;Ids_N9<|Zrm=HyKA+`9 zJ`9ugI!tXw>SJBC+rA&j7_}i|A%ntDlHoaNQDVPs+n|I!dKH040-qQARnptTN)Ao> zW*DPvcX3Q+iqHe|^XPOVz9Mz|L6Om3-OJ2+<=>;s7A0*@k)26l9J`PKwG-M(J2Pdw z()L!N5DII;M@Yw$y-1cehk7%8?sGhL4iC_8#Az%eZR;4 zWX28*mnA<1pnN7cGQEUB%QRGrk_j@8n=^Hn{beW*Wf~0)&$!?8n*(Z6@{^LQNYzrr zQh!{(`zOz3{rp=Umt-5Iu2?&_e$xfW6Jxk8{&pGG@=w97 zFNN^8{>u=~!*g}I6vR!KLWPo91(oDp0rBQoAy%w7Wgx|lE*`72!@8-bX*i#+c8fMm zE6sM+EHm~Nu50*bp&mglgBsd1?B0P$!W<-yf7qU-st+j6Zfw%%{e3z*mK&Yeh@K^J3Gh=SlefRlZ#~!XdVPFcPqVi}hK+{nU`xrHH}|$QKRiBIEmYFpO_)gOeM7B( z5>u40C$(_jUMGSo<<7YeB=uFuN5?=-G*CR?0^eeuu~kN3v_+I zb5O91&gYIIj%`^s2?0c$u;ZroK8FtSXmo+WSc}DqoDrW<;JUC6%y>RF5;dvX?RH99 z25s2E-PF@sA%3ZtX~4bk8Om%go|zB;JaQ(=WadCw8n+@mH1w`h?$RSoq9Vh{7dOV} z%@=+nSfWPQ`LsfNxjgO<2U$rl@FDk4F{&!yfCEE@z9&Ghm}R}_)Md_#T&hqpIBQZoU8Zr9Hk6#q>KeHgXZ6iG@29?&BJ^dWMk~ z`*-x0=zh8Qf06wET)96h(k~sX-IiZpV&Y37{8hW7wpNut`s?t6PvV;$JC{WYxi?29 zV$?KUkuTt3qbkOrTSngVwn&Y$A*PBp)^U?6%k!BI7IYxwhsA-2X6s6p^U6RVQ$H|O z9G$d4^&t=mRrV|57Zxpfh$#ijeX?3aw~<8wF9m~umRUkB^?jx5B9TFk8;Q}fh)ab@ z2}1p*WgP992U|ulQ-mo-)wL`R&@()$gE!K(#efhnNW_4xv^DO0+3)w`h+!#xztA#W ztKxXrQ4*Kbk5V9py)|jr#ds9n2IQRJYK+i9WQzdRAOcvQDI^qe09A}T>MK$@DwnA zfkKAP=ID^eJGj9?7jGCH!z8Qvg@kI{WMDE3V8-MPC_J%1%G8t*^G+z6>A`3xuv{ks z8l4u^6;X>bFQ~`~wAl!T*B}RqiOIU2NgZeutn#m^oHji|Hf$qTVf%vPOe~$6)G-q# z6%j$!t%zJLMRD3+&G6gwD3dZ+>t2 z-amZb-+xh5kH3@YxfqNVsYWsezK+aL{FCJWd*^@Tia&SZM=t%?#h<$PIUH@0a;MTu zA^ffR8pi79Z^P*tU-huw<-j3_ys-s>uotrLw%E>9IY;=LMJgt#2EF|ADb8hV_h!bS z<%a9~Eewx=31GXA5gOoP33&so<$+B(Xt2iVBE6PjppHs*j6+<*n>~z%SE|+i!^l}Y zw^c7P&RH81Ex>#ZWWPo5mEbHy-QBRL9>(i+czk@IC=f%xg$xmC9dTet7=~gG#*3+3KIHhi##3>e@q~KiDs<#9FaD91d1B3uqq)Av{pu*Y)XgW;(p< z+jh8K2%#55(65pD6Sn1wLhc=#M1opR2KE>_83rrnvz(e0p@61mwnUMh3`GWh+s5_+E$s9Lpu>u)O}|LYAhYl73oSH& zY53|n#O;B`DKu>Lb@sG%i)3V2U0ODrs)CwX%9~f&W~&Z6Y+`-e9_2!H3miV3P8Hb5 zJN1pZX#tbPZ;tH(ucCuGl@R$OWDM$+V*3Hxr5rpY>;;qSv`AT~?FDFJaFfp}I_)($ ztkbM9V5H! z&1cmHyVG`w;5KDCh;2>`vf6fsMGKQYsAC~9l~p0xSYDp^k^5DT$AP#5OMJLvV2sbf zAoeJMX}|nIl!K70Gk9#2PISBV@BZog!^b}k#l?M{q5(^vzTxbj_@6ufgB}oA*GE!^ndR186x?TVf zZBtu0P6TJRLbd4o0N;%z)pA;tf3dA8h-}I86|{KN3x&sV)d+3m)s~I5r>Do6Fx39< z0(~|@A0&cYAESE@vlm4%FkcBTy-RJ?)-#5SIDoP$8ifC?b`HQ=q=4aixl$Ewbr#6A z1zk2Vm_Et^BPchK$h9DSR4flWH}jT^WbX~UAZ@b?11en_ee3(UL5Y~6y zPNYdD1H%Thj7hrzaY?LZPjq;pDwJ?vsyhfeR$Vgakf#~cTqi>XH5;H$U9%%C$;In> z>s_zxr8?$9OPOv3KhANBuPrvsUy_E^Wh@AR9Vf)q%AuF^5;td2?PkEI#J|^=)1^XY ze&r$A$%?T(qtEh7juYs?U>ZT2R3}%NVHUcq;165>@VSkPU zuxKC3`Eu_1&h92U_%&|HWtq<8Jf(pVMilMcinU-Xnb+vV07Y{^i;`Td)Y5a$8jzQP zw~CBLc+ttoH&P?-IZ$OTa8N2K!zV>M$`*ZP=4}G*4KXh0~aRPoEBfW`e!cuwR1mp>rY(#3%C3TsfsDS6vE%sG2vQ%{@@=k9szBh z!#~tQ^mrCs?v$a1w7L4;sGC@}pj@9P&b?Yh+3_B_nJ!}9Ml8U)EO5{WffQsC@G^|d z;z28jiEOeqEBdY*cjNQvN_qau(pV=1Z(9|#!fV^uiW}Ur;cbc^5l@n5GgICqFMq5W z_N-_Ls?kR+BvTcuN+va|RM>?RTSS@Q%nyKG{ZKx5CIfl@*za=> z;+cxY3bZCEM~cAmQ=FckFjysWB9cZ5IZWoJ3EeV>WFk9i+YaC(E~@BO%tyzy>v}6U zwlOeJ1tH<;T26xPn9G8a`KBr|i4h*<^R)O%G!_N5I+Bk@g-h5Uhh-yC`T~0jC|>BO z$60QHIqu5fBzVpldYA1dOuac;R*VtTd^YUmvTWgY1*!(f1sl~AJF3C11hj_xo`rUZ z+fIlE0Bhb1@!vOx)teWZI5otFxjXAN$QX0X5FjQ%)$M8d7s9{FOC%S`$yTPSYVHu` znk{IQ8P4-KCN4hQN9a$KXc!!i^b#+v5RT(e=JqL*ePcdgN54b#+v(=So(7*2)fNf| z;XFw>pkCyZ6#0Yb-GE)J*kDPTXcN0?%1C0`nz^760MTvH8J>39iu^^@T;-_%aH=4t zs#`w!7`-1Lv>0{L&v3%z`h;*Dv!}an0=Y>o97AUV^(*A&(akKP9LQoLSIo zBSNlKR^T+)tqW()V(nU6acs|zrXjN3N8a@7FkG_FvcVu?3)@5=4m-;7(DS4v=9~-p zniLCX&z$%|s9?)Ho$W8CYX>LHBO(>&l=g-wsF47)n3knao}~!hZ+1!mahr}1Eh&}P zYD?`vBSg(E3*>m93tc;g;@a8Fj64rrA0nBs4*>Bs$ooU#bNHK8b6K>tUAT^uWgN7w;!(8$EOovaoX*7_AItVscL$JQvATG zw31isHuE`nsRO=q`(S0VX4zx`vtx}!!0=}`6ah0c2@Ih9$5?nmY;SII252jB!l`D- zs{}VOUMUtiLn=sthw8}YDoFp%i;FA4xVxOMc>6JtLy9?{CT&GgF^V}mXy3+k+OzQ| zrHt7o*`a|p6}AjTY=hjQ2Vyfp6W&ru#W;8jVT)lNMHU5$Ey@Ayg{7)MjaA_T>xH8J zm>a6FTW_jnm296|kG5&$T}B7fta>Dani}}k)9=MD3GvtxZ`hPOAK-{78ZgL|&e58I zA-<@Tc0H3sw6c{7x{{}fgi1b1Pxxn5sZN>vVb>-LU{s_%yVzu^R`Kqeb9Z?7Td@`W zEvkq~_`P(?&beHe zo85?Lig0LRtJG~)QZ6qa8t3rVEje(2${vM=QE-L}gRF$*0htH>+~1a7q9?)&)IA3q zM)3M-n39y(N5@rjI0o)^T^ktf3TqCAKP2Q0fn`z{tPFHh(OfD8EW)9*-wxN&o@Te( z&9RY)EnGc(qP8G9^7lA`%CkcR09UmzX(0fiQ6&p@b_tPw#C|dih7@7>zpl^Vh@w=N zm+KH$TacU6>B2XO9-S4{5>K1X#Y z%BYt_$7a=0mt`zrBNk$5vI>O458r_KcOw13bLE9fI4G-m53#Vvpyi_(Yy$UVfL z>MMFREPdOdEev=)JCbB6kyP?Cn7WH>!pjZ!!E72?>L}?9O!jKH=lDHhuT7}n#UoJ_ zM8F6<`3eI1uw+YUN2NrP8swHjwJFmxRYw~;Td=AI~L1g zwfD9xHCWS)z2NC|va5H8z~*`yk=SCF@D>K?m4GFJGzXbpG9kfDsUC5ikHhUaoj_+5 zxbyVcA_g;i&rZ8v*;J>d!Gq6Fg#)AO(>A$)F9ggI=o3$QZ&FiN?!Ss46eUO>AKoTM z7SPo%8d5GP-Nu9Htv2=ed;iIE-MxRQg8$*VXoc#|r91Eb(7S(}{Bv@@jPB1;`OlpH zKZ^7V7Y85LmqPgKr9)935jo|^MUR{k&Q3Ul_w?KY$((AyZH$5&kJ0MXC&-zQYn%L* zR3_oQ;u3AW*l{5W$qtP<78N&aqSdm!=-u6&ZQjOUM&hQxr$2(j`@H?VC zh3!ZX;$csD8P~dMYzuL^OsDH$ALaRS0c)lx=lj%j9hP{j;%9iC?k?Bk@z{0t36HJ8 z({T;Sz$7#qOeJyZAtP$haG#TG!y%TExAU@YBUM@ssYWFx(&DssEc<&PTh-MPN+;PP|1+I7{~wyUmY`miT?;xTwO zk1d1fG%|8>oQp>s4{_)c7v9-k%FE_4 zWaz5w``ux08~VZOB2x>(vLe^?ig3S$z0%2^r>O0>jV&OMmC;3!fjhE!W5*3mFh~Jm z8dqRib3la;BeHF&@#1*H%1K=nWbQ4smA<7Q0X}G-4l8|^D9l2{fjl79vZLDoII=22 z@SctaVBSTTR~5rJc6GtgOGtNzVha1dH=qU;c;S_E+q77W5UGPo1{!3QohN55G(7wg zT7?BBnYNz*fGZgV1)3(+ix^XKJ%chV`2m7R%FkoT_4u}qis`E)|R6eOc)PXX10 zE3cR7`T4}44CDo#&*SUYue-iCbgNnFG_KE27ZfZIdlTI9x~@ZJlzm&vg;@ee3a^z) zf~~_ij`T1`wanf_hGb~c3W_Qn81y&lQQP0-=`0DzR*PY|ZXa7+PefxkF+*awUVybv z)C^D1u;f)_C(L#_2-}0yeSNwb{T(S_r)6ol~fMj_yix z#?3Cv=X17(5dJ)pH;}WUsCi*tSK+IE_`N?qHM{q}Nw%WDO<(rm@51u;!t#gC|C1EX z$$yr@KTqkO`t;9zveUZ$zpjWch48E4%M_=J8)rsc7ru1)#w>K@-#Bh=3t(|d8^}i0 z!EZGpuOXoRZzQ}Vh6Feu^1>F5iMg*Q{v9ko&>QODN`{<1``-kwFZj7|q0G^42}D5D zfK(K_-R|-E#0@@sIP@qBM6a|+_GwPbG?Md;VNVen2BWL=!)&FlD`w7F0YevboRvcE z=!l&6@3w)yB4uK_xn~ItE~`wN6Vl0He&VXLt;z4~k%xV6xWx|C&SwK3Y7j52oC8P% zNZ@fM5ja_o}NdUVwcNm zwdCR9@pQSgO-tkE`FuXxo4dQe+wU+#dVD&)d-rx2U{!4uVw&Hc&eyi>kby-)tF7_p zjeKD55HSzFh5?0C(&h^eUWmqpUESuWl4&3-dz#-vk?fpLVDS$)_8+^zJStE!gx zm5fpYqXo^%33*+P{*c?wZQnp?Qk<8`4!y)-WJzM)lKG7pM4B`>FB6Qp;jX~zeW8yW z^p8b0o}u9Z`s|LXUs#+6UtuGP7Os82z{njpb)~o{Tt6Yn#f{E;Ic%4N9GZ`UYfA4O z-Rp9evz^)U`g^Ve92{R(&|kfG+W6JBsIQ#+zH|Rw^8Y%yf9Bl(>fK*D_e@FRe^{#j zQV72qP*}id+bt7m?lJj4NHV&dVX0VG-L zw3%6>#{Lo{!&C&jYzzt=^Chqr6ge;t$#1r9;;2ivS?t$UrS@Hv(KsK))LA-=cq>jL zOfnM?DiR@JlRcK~AFpU0LfUA@zuWhR{oWn{yKoqER#SllU}d$d8-=DUmU1-C7PyO} z9}DBwM26&=rp1Z>K?9sQFYnP^u8TntJ6D%0ya;yv;g_GjwOV+*ySE=czJEBKF5um) z1_fXl`uueI;){0=4^Q^#+}+<>%{V=ukH>rL59rCGP)6I5#IRO8&O{QVGU6F3;&j?S zjLjrw$ObEt)@ph4-Q|=2^wVbd?q&J>N-LtX<>s3{{bT3; zOXt3D?#IsmFu5Oj_wSPXOYeTpy7>P@iZ6xmt2z6~W5U(WeY#U(wMH*)lNEocnlaC{ zF!KpCE%YL^r9~O1;vfL#lzC-mM<^=)UG#2_FOWRvLFB_A-@_sU)>#Y?2n^`4<>Ru( z2_rpD3c)I{wQ@j$U4scCc`yT_?QLa}16DQ~g)$HA(6nSL4iZPH6ZXDpD*Rte0B5pL z5`)CsCTgGsgBy@5v4nY9qrq!!MrM#)VVI4|W^dugE8IjQU=TEi(=Zm~^Bs1*{Txm% zX+6TRJN9jJyJm%}hWKR;iop!mWY8oXL{e0u(@s;ejaGJH4=mtlbbgpF!Dx92l79x_0& z+OV?p8g^{E+F&`Ux$d}cY|jA_Q%VB@q*CobU=gwQgD1=O74qqVx9Aqyo@j!zl8OkA zA@<(m&Ei0V&Kf`J;IF|ZA4ZdSTVEZ9-szdH&EPy;#0PJmIy)LV!ZP-b4 z27kf-M7U##tw~8@3kX)FwDZDnivE!E%rpa4!^Uq}{noFLK{obDxTmbVeZY7!trbf=V~+b8o>_hnq+uyCxk-3%tXzd%EUhTD#u{eyP(+wH;6eKk{lT?8~Axf;sgy5=~(aJ zU~A_bEV!f&c<|b@o!@-_eRKSU|1I;^{;gS%3!2v)ocn#3{%a31+SU7a$^Ty|{0r~? zrAzC=gVO$omM{`(Vp) z!O0OQ6F^gFmd;EdWa|?2nu*c&Al%h8YxpvA`XYrje?NOTW=$c=ss)B!#4f8Qp$y7l z{tRj}G-E35u>Zzu7_=*Qh^==FbyVjcYuGSZ^_n|hHYr%OD$f_&ybpbUy`G25*-FLz z{k@%~xJ>pvQWfe1t8p@M4DwFnGC-m^4~SYCIxCq~Z7+Ym4)5PTS}m+#WeGvv6L8H7m-(8abB=zCX{y*+!dVLsak9goLh7%tqM zEX-u=b_Y8K>g^2QtSULcyRwjA-x5{^HGrcRsS@4lLT+a$31!*sc1_IjNp{T^paf5v zk>B6l*^UIAUhI;vcQLGni>}FbgUmQXLs)_zAZZ8{5^W1-Qmh@SJ|@#}%=O~AB$02u z0Jmo&)h=22@p8K$6JS@8bYnkV=patpn!v7gV9FBbsk|;rUverhlbR^-2ny3WDSm=o z0ow|%n&>N_gCC(&s#+-KSHHKk`**g_e5rze3l*^-Ozd2G?bG*t{BP_|cj1>Q{CRT! zlUx5c?|$LaOCkI!zHATs@Z{%}#Y|&{Lq!oZg(rU4=M zp4Rtv+m;3Qq>$1Vy1T(rK@AI+%h?`p7-(i|mSk?kZqL)jikhu-R_{(v!#ZBE-$%*; zwBYv4({M%)GtCKpG!QD#^&2^V%2G3cRn|?xRBl_8t?((A&XYrt*?TKR%xx9v}C+Lsi4G z#!l+HxAuc4z7$RtLYBrw$r6{#u#yV2z=@_(&9*{#8PODdzQ*ZiU)aL?olm~ytgg0u zJI}VCIG;|o&NW?YE90AwUc>k__#-BrK#EJW&$TM+jsv1{B~=8x+*L9Y z9KB?c4<+VfZ_dF~Y$a<0*Gh;*_ViR};wVLneR|k&Sa}<#IL%$#7l6nUn%IsMWpX#v zP_@^&oSzXO6$(2OwD?)r8xP}TpI~ht*EAbggBh!p*y}Jh+!f8dYKyv&Koe!YDCc=> zo6bP+^>TqR!ud@5h1v?h!H{uT+cv8yOAuW1d;s@=K}opumNlJ^(EP01o$X>GSzIu* zceK*Qg}p8l$j>DF;69Tzfu`%hOQPV&;dIKykX`{Njcrt@;pxeaHVdK(a|W8inN}Ju zpc}&IGflDK^GmO63JBLs@?9ob;e(#a0RoK#=yMAORLp(}B8c-NX3(*10nZ2ztE`ncbfzAz{^(M4Kj)C*V!W7js=jN9E#nxbvO4LnU?0Q!Yeer^95rl^TO zw(ytB#lE+{WU${POL3`2BGdUaqhg~tmLan?)T7(FfNb+R`7Gz;1V$CYXwu%yMHEdcpC7n?#w~;Du zP0_#M)0wj3e)E5k@2t<3B$1}=;LBt$(8&0q6}?azbuz$~wB9bx@563$U zGi%#2)bixBy6EWDcQ_noCQU+~I89KDqUVW2u!|F+!NKjjZnG`apuy35AxWpPZ6G3n zm&*hjx!T}}qJ|VMk0)G~?Lu?P#6ZX)M&x-Rns}uEQM4e0%CqY%x9;c}fw^(HSGJrP zcm`47Nb2h%BhlOnCTNeWzah^P<&p{kzrn$ezn{9}dmrYPmGjs8WyS9!pZ+Vi{6n|C z6vD6KYmMREBGZ=ooB0gj}H$&|K%^99#4igAD=ErV4v2*{&2j%Uy%jlu9xTH0{<6{T&<8v#DWHx zyB$0zvN+p|W0D0*&H4HHqmN!=k%yaFY?}6P=|t=eMgyQwGggrgj&npYPGthly0R8B_j5UYFNgV|FS#b#($XY})-BFc z>H$*)gLhl(ANratwkasXB9R`C#~j8~)VwnmRw(~v>}2n%LL;%;x4SD1Uz+SGmyV-rS0*q(shuhlT0K{~6h zc24?TcRdf>t02Qy<0$)^ySroC*5h~$Ql8XM8SeLeT>~D{a{x*>rToCPr^Qxdx9bc8 zFY_4S4+mTQ#YtCL$mUW?I29PheSqJO?;Tr!1(~1m#=sKqhE&Iu39ue|O8UwWKFXi7 zqUs%TbqdG5j5h=QxEFGRvs(hC8tTp3YZ^Ef=~IObZE&11&Asr^E8y22YG-Sn2iqyl z%$K%`Yv-V<8r#XBbAXOQveSz!4zGIZ6wRpWE@MEK$;L?*xKf`Y*j<|+bgd;#NYsX8 zza}*sG|awjzvw-H+s5-L{08#u?U!V=HFb{*n_(O}+nnuZ;w}S+IfE2h#J#y-w};cb zMwyjEg<0fyL8?~ds+QY7PUhpvPF5cC<@I-;+T*9*zrexYICS{(^_7eXm+<+!^8NGj zcRsT9HbWW?E%LnH>+V_-NhI_|%%vmU9Mx4dQ*P>TD&a@~T%}gV>%g>Z7HVW*4OrPS z@3xJzRlBs}Z){bqp-ihwWFAyM``KSVpUzcP>_Z!o0n`RRJfPP#V<!elLrsCosvf?9qo&XdxMuUJ)iceuO z&^VaJ#kO~mhpb_9wkE|iC^#u;#LS{uFrb1)+XZU~ED;!s2xSXpd@!e8W!1ovMoy!w zQL^YFkm`BM%e_VZ>P0D#L$%XOBVKq$sx#W}{JAhLXrFcr434m^m|?;5`6|7?0NKr_ zO|~>Q4lO^Lid5B-W=uC}AFjs_F`4S_Zh6tNYf0i~!umQ;BZ>PYDX+R z0$J885p(eJ@^%Wd2SYoMxs~NZR^;YHeM@%2q%?{k-R=rZ1##xnX0yW$EI&!@D(&0( z%^#%x{=Jp>m-X_c5dN-?31ji;+vbaBjM}Rvtuq%uxm|oHbDrHR&IuD$Qy)4`8Cgou z5ID5Q))za|g}hLl-nv_avh8V<nBD5B+(YKE9dOfiwsMZcbb3A=_6MsF zZR^M&ZAE{C6Ag+VLVDqWxhzdn*{XTmxAy5ETFKt4EuaXkRSbRc(VN%y0U^u}-eiSn z9riK3e|X;aohx>GW80dhZE6@&gISD&;MIr#UKL_hT|mIfR|Or&w7c>wZ$8EkX}S`k3z|?oiowv``mxo8_>j3z^K+0#gt29$!r%>{C67*!bPbw$FlI5Od4dpzJ-a zQ7~!tUO6{uaWlJ$1#)T_PrI(;Fq}`P-QHH8dqY=!-OosU0^_A?>%K)adEYhmiq~=B z2n1XETDolV;9)D(0N&UUUoRKw?x76M5$IyjU|CpbHifokx#Xbb1RzeFwSWJOMzD2HLvTsvdj428?!_(-j6zXm|$K?t5F{@qEzO zdAMu&(C?4n!Ou&JEW=1;8&CM`?9<=x(Ecni4n!qkdu|>lZy$s`&#r5*f+vLl*+s$v z0Nmlz*5!+;cQSq^04CWOI|$VbvYh74y1Cc@mLu3Ixhuo(5HsR}=3V8c&=AeU=fPO6 zL-gW?E^D`-Ze*IzyN60mJXeEK;CMYu6Kun_I%>b z=sDM%9+efgLE#b+1ezqsm08skoKRoSs?5wkc*13%EZV2tXto=Pnrh~h+s&zvQ^f#2 zt$jX84yDsP(Ypfnq>63w**kyZE}@8W%YqGE(jh2#o+~pS=wuGPC8C%)M-s7h|5{aKU0NthT)_vWc?$ z8kGrLRlOQ8TYjfV6DW~o)o?^w1Ks6-d2X=QxdX+qZ{3^o#6JA$X|8 z1FF;M+5P~5Kz_e&zv~oslTyn7riofgz%fH@boUSTV(;HQk+>R<`@`{YN3(Kz*QGdI z#tVhvO&JU|S~+ZAy?T6j`~qG9;Fa9%cO@J|nRyrE>3n{E9+z>cc6|*mkkr&clUhSo zoq@#$aAPT~>|Kxx5v_1qUA@@)zuWJ?+JJlboz-irwQbh`=^B_=VrT$*?8hGb8c5Lv zVp5Lo^Gx4Ga+BL41{1r$Sk-;D=fm{bJ}veKk>^0CY4u1YQK3OO&CQX~cD3&s9NZGkM-Rw!lufoL!Gxj;ps#%_?kKkKi>r^6JjzHZ? zbwdbI7KRYEbjqi8bYsa9q+ z;+gY^oIZx0ihNOT@fOyw`o$N+{U^`u;oS=y{0*hd{EuA^H>xdg{N?Lc*i@A9lW)!c z=pXnuKn&O?I(g7%R)fM!DNa(vMBahQ0W0T71b9o!tT3B5xjZTqP!PkL-^&`)s=%bQ zCoK?#Z~)ml@;s&g83b*2#{Z;VF4f3^Roe-2WBf!A{bk5UN z{ZDAo>dIbexL)XS>KVdOido67X(Q=kYkT#s#jt8o#<|s6TXhYP z?e|vx3p)ra@b=<%WXHP$k^l?8Z14JZng#?+7Pk8LoYBI;4DCr8s~ARvZQL}`TLd_-Z#oijYYE}78W*4x zsW~9Rs&a<17o2ufyjtjN?vyH2vY)9*-bUA~)JtdI)#aW$cp^b!fVh=F6&p_V!uAcp z{Fb0}ds_O+g4aa0?^eY9Mv68BglJ4zX+l&gsJHcgozx(UIvu=nZo^JhU+n{(2-05l zFQa5#6*)X4lXsjV$|PG!UJ<)R-Y;+{D7RifLU9{mZ>EpJ7&QNpf>bq_n zrZq?ZEbZlLYfWh=%~mzr;2#e=ES`GHdlTX9-F=Qa=ixl z;H0&;nu6$nnWBH}-yt$vCbU>X?CO$Hg$r04^X#=eBXHK0@Y-JAzkh4157;B2RtAxH z8KfqHR%#IFI*ek^PQ$o8nBc%CKp6ytQT8dzH~E9%C!8u9RI2YK9@bIpnH zCSRenG<7iz7X#FGwhf5eOLv{UW7_**NnHwx_ggzd-43)}=r_>TM(z?ckLisdqr?Mm0I{gyU`m4(e8xDDMJ9bj8V+qCCm+^n)jW}w7bX_*_jy|; zHH*O3=9V6maFl_Ms;Q_UET}nwmn};EVWcf>V)LLy#9(#?udmm$4^@&MAsmMcT-ITm zG;7P4$LiZ2QrHwxQgn$$npHv;n9FI34hDxok_XgTh}``~DU) z_<{((2HjET$HleH@+W^7_Z_AP?2AgYtc^TX%T8<&nRkddNUKpt;-ZXy#UodWTBMy~ z)*fh~38dOf+Y6=~Ht<++dJM+aVZ6>$wk@^t0I@QQA(RimA4u_*wybH}$EOF>l5qyF zk}K>=6J*0T_Ellox-9cjHkfs%>P~^@!}$y@&sB}rvct2F#eQOt)-bCb zLIHt<0YqWwSO&lEn!c@;Y1ly=5#&o}#im2nM`b6*9&W4d{;<0{9QwY!zduYf^02_t z1a?F)2#_dUvq{Y@`rW>eGM(>2V!ObBU&X1a%K?Pq+(wnKg3%ebF&koi(W6t9N>IuZ zC?}*em%@hG*H8NL4?Wh5oVd4gn`}{}$RsmTzY%agXgN~$JkvMq$0kj70~wpjmsiCl zY{aE*dm20Eg6~lOFEe}lQL#$d5M0jOyx^6!w3S2RLe8OWA81z{qlQ;TQslJZs3a!V zeNG=}EU_|a1=nj-5E6EQOm$+sbX4{Wo!N!Qh3RmXz|uD0j{J$a$VnHGA&a!RX* zBZe$+N|MmJm|ectMIpY*IGexo_t%fV{|msum#-H@_;ujR_;>Hizxem=;pD&Z$``ey zc(l(>+X9qi%uVt`@LSg_%1Mnj&<1Xt^uk&8M1L7>Z$m{FUy=vxwkwiv+J3qWSktR= ze>_lQp3R5lU!^6I{W?+uhHK0zHi;}aB%7bC2<@x@`Sax(7x-!Qebx3mtJ^2WLD+J; zE>mY^vnn4SA8nC2?std7!QkC>xEjo}6=S$w?Af}u>V18_TrQ`xK{518p`?~4jv+#* z8Vsr(_FYld(C1w*=kwX#l&>o*hj99+s)vV1`+>b&2T%p0D)@1y=IL~ji_J6*_Ha8e z!;VE!wMg5rwP$J!d!fqO|CWd2zJf4extvkRVW#6#LtI#ZSwpN`YHo!>VHGY$gSKT; z)n#ap`&J-4#d+8F_UEXer5lHH5n8LMa>`rh>ofs}SzWhE7vvm^0$I&-KqGm$>~;r3 zu5q4Ae0+u`TNzbH>RRLT!_CdXCFP3G!szc3Y=>)8yMmF58N4)gXcfY_vCoEo8-*+(`01 z5JtudRwDQ-7Ci0Iz2XMtm3X?G!B|<60tG$0s3j2$Wm^=AaeEF=(V}Qb$F$dOMHd0j z9NzhYP{>uND+5@zVdsbiYU_gmv|ab+qc=>XMHRO{rUOfh?1+OwzQ&NPm1jdyj0_^f z(xX-x@G^+F>smC9y;mI3ez&85Bmnw=J5*wYwbE2q2dhqydDQkiU2g|l*y>p#SQxM` z(v-xB2)}^!y6<U52az=N~Ln92HRCjY64?T~fLX*I_^u zltHM%ha96uaktA3DP(`pOhLF2kL629jaQ-OqvH^C(lmN>8vSOyVpOu9(t&vyVhgpZ z55YcUuDm%;-EW-Bjk=OG8${%VTA&HtUAg|{D^>-xWbK?LtIaU9t3d^ViJc?Chb`7X zZ)@xc2nT20NVhhrl@)9#;2z7EE*euA?cbL54{ zT@I}Wx7wha-A_6Lt76p9rH$-Hc%pDfVn94@+u$;9O7^aj@+UdYNEM@K5?q#$0QHCU zqwk&jSD(Qf_~q-R5PnU*uo&%j@ejTeUmqhR1KA&W8^ssdZ;8ejDtEz$ElEeaRWj*Q z-iBlSDl%bV+rj5iO$qLQ#mH}&~K7;`wyOV@RjSVU;ikQ-G( zbnnpBJl>%S24WPHm?{`BvM1MuAktsi4raJsZRP6i6}g{4^x$p><_l!-5q&~C zOV{hQZs0g$^`ok5UCJn3w~-*K%HneM4Q}IFvufK_aN%SRU3&X~Xqq}vvKO=wRAMv- z5U#+kg$27(C{>QQ%DF8*`;>~oX6FeLYm~}GNxYg1^1ZH(Rgi5FYAEQD%PHV8%_Ety zY{DQebQ^4DzEW8=?mK^fdGq^!RaU2$0pUv_{Q8tZ?9YqKknRraAAAy9c23;J(2F-A zJ(b8BqLf*Ir74YEgEvZoA_v?DSuVX5HB?0IU}dO*}|u#hYrL5qDMPsx)B2lmgRnb#1N6Cz!m_58R&90mDJ4#$P{+1RlE+K zVJ(!1fx`3s@o>C59yNL~z{iweZ0Yasj%bdJqZJG*61H&K6B>-ON86HeMgT%>#aH2_ zR%VD$!G~1PvAyi&a@E^}3=u~K{-c%Nwyxj2x@+sgacCMS*f4x%M`V9yWn|wQtV9#I zVWqWq0q22nU^Maq?@vlC?vDrZiJ}2ni~xf>9&mUyB8OqGOi0=-%Lrp=1lP~>(rlQ{ z;VbS2;G(H`LY&NnUSi5)<-Bd4o&O+i%Lpu2>ynppsO-4J{K7agsFunCys$e0^$pVI@4`l z8w4%0j&RN0c#oueyTnlH;tI!fej{s7yWZ^R4DXV93Mne12Nw+~+4k`A*ULjqA=5m$ zv`@LQtE$I^)@VFT2lhN=z+FSnaZ$8M&JxEuN$su=KtoFdj+=LG${bW$`Z;bmQtsk# zj>~(&E-FU?8%KeX@Xv{Hh70XR6@HjEJR5D7k;lI6=WqPr{r=5Qe0YI_FNN^yHYprO zXNBjFem{NlE|l9Cf;aX|u0+Z~3JUaIlbW0wvy-3Zv_#S3Ij2}idCj@J-O1qGL$@0h zKk8`UsdF(@+ecsgNa#RWIt`4J&xS#NCe5 z7w9Ss-qqfb+Wl^)#8VncuXgG#*Q-^fT@T~RyW<{02N$g>+PC+|9lEbH z&!YIx+0PAqe)Q^C`qjtzfy@0g+3sQd7658h!y@&PppTM=A_0Y608r2~v{KdwE^9DwEy?jfxJpfNrU*f@ ziu$}}b%7ofnB!cNj755p_uKPuK1(Hlhfzad5R&}f9mN}@so(v~RCrJG`mIYUR3b=!Z=)5wfC+Jw+wk>g0uKnQqUF4KHH zpUD?Rd(%Gsl_c-;Ff_>OD?m@5M=ODL7e7Bg+d{V^RdT-rzr$X$s%RdGzLNp3Xo&Q5 z8qen|0aZL0Htb+RZFLU9yCvH{Vl0Gw+pBG&4FLjIR`uQC4$}3fC#$v|NUN7rnQ;o0 zY`|IwFwmhzy1GmEcY98_mR)Zs%{Nwqo1VZLWzW{=Z-47MkB?6jCPIPm@%`cX>D{YW z25lR7>Ab#sdU{5p(6Sm{#5T~kYGIAjP*4Hgz)!9tzdul_F_A9ly6({T&2=2ci@l3z zxVLPOO!V;dR6|YJkv1tmRd|(&X-R9LwX7Rt2)d$f>k(et>GA2&KG&A)N3soJhAuBN zh8e?bdZkW4wwGX_z<&mt0z}$!uK=Jn&!xXbRfMwg66!&cz)I+|(Vr%a(Q}HHvdO2K z@3LG8Eh$3=)Rt+?ATa=oXWMI%maCdU!7wVrf1rtkCjhQ6J5hBFJxMGKYTGGFO{zbo$TG_V;5C$Y? zFqGb)1`s!!-yD{!@m^-n%JfC9G}y<)MP@Pd@yQ-HiK0lj!^V8?l81)-X<8Z2ySYbY zz2lY!tS6c3{kdu%H^-uvua`pjb)(E&20x6Lj$?Zr$%OGM{iHQDWSb>0H3FOCb%B!_ zza=}3^1>qX*(oNZhAbd?z+*VFDy%RES>*A(TrX@}Z6Kk-rg%nBdDI#`Tn8}1p=Qv? z5I48|DHPwS!Dw)q`(0-*dA%aq9Y=*uFJ5d1Bl+JtVfXOx-fBOZvmp2^_jmXEzS;F3UoTg1YvXJ{&K>vm zSPW3-x$inVx$rJ6krw1ISX$^oQ!X<|7Pb(RC=)~17zAC$OTkvD{nwta-C3~!E61MN zFeS`I>T;ZC;YCs^0KO4YpXh!XKky;#02JtIhl=V|27q#mA#*09R#3lLaxvv&p~+mN zaTu_N9HnvCC>RLIV}@Qx8Iwt(4!gJ|t(hA|_c)E&a+hBpUR|yi!{~M(B>LiFm#nCIOwCh_#YgQg;gA&TxO;1*S>Q}Gsx~V;%E_L0! zd-vA2E3d=F zO31gq^-V|GB*chTW5SMXkp*4KP+Tq-xVlwfMwASZhCygmQ*${U4*fh8gsS0i+#ip< zRaSc=-~QH{>*YLLLCQED_6Fj{VZKB3!MSU#7ETP$fvzsb%Fazw)!j$;=jZ3^<MzGRx6J^M;hWsx-#tcU_A%X~4VcJ5g0Zkoo#)Ux>}>w$a>-<-v=#GoQ(R=5aQ>U;2Sm_CAzXlo@w3l9yT5;B zX9Hr*X>8gC8>j5qjr3+%;;|1Rt)$;bF$O(z;4}$LaC?b4Ux|Kfcn+peMGYi1RBX zTor{O#dq)CB?PW|=;tPQu+^2Enqg`)SPiS2u4|EdK3)%oAS(Dh+B!T#eTDXF)r8%? zC%4u?FP~_v?4hi;-Gjl+UEjWb{mS0>)6-*9S8HRNhS>MrJdW#P%P!cA=kvof4))Sa zFuvMzwY$D5LRU8ib`h|*M*cPirf-97bwtWw2M-gxq-_%>f`DTn+46#tc7h~K ztFq?75a>~yVBi&IyUS|em1j8~2s3n+J%94q*w@t)@JF6Oyq%-xzoo6oeSOb!M9Bc}}MQ!7gDvQ=6cpUX+n zUR|jf=t+0Sxc}(6Y9Ct=rQw!I2uMe%GX0#iM|YeEaqq9e&n?u=Gi}^{Ey^O~cgjKalyi`{uSIQB z-uxhuk_k?#M(&mt00TjG3TXeymUS&A01V9r#K9|*cUsh2Bh z88fM*%lUi~^V6zq-?i|jsmQ%?X`rRJ{ol6lk(WpHNC9&k{7^Z6oCIfYOqsSq?N021 zcMW%ynWf81>ls(M#$=mNi6!umts%K-N}b#|Z?c3B(S4f{=oTNTAV8GKETG%?A_{CG zB{gkuPz0$h$u*@5|khPTG5zX7S0}hDn zXq#ae;SNJ)CRit6@J1=I5`R;-L4OxyzPUmqLkVMfme15Mc9=GXO(Rz4}!KmzdIbkGLgCajeZiUby ztC}lm3-)IvA{~-}Ihcmixec~$m6ab@kvSxxk9JJE9RldBZuMP_=UHaz1YB+9^YlDT z*InNfo;Iq^o&*OVC$ni9&QA|D4XkB3$=((BADNuL)J?enreKl=Sw#yG2;_-Pjai>< z1vqLdc$V~Y67U15C$Jq!n5}}2i|h0Iru+SO=?%C+8 z_#RyDtg`<`6{!?&a9;Jc3#l~;%RO{MVa&gy8ZTvGATef4;nwFAozia!e_60f(ysC; z4bQo}S<7)y$=`as@yo5zJS?QM8c9!Z)~3!q*kz^=M}=q6U}ce0ggwqKAA8cK>*je? zj-QAu8Whs-EYbQ5yD~AyAfQEMsbrr9;w~GQ#a6j9sLe4N*j{4KS8icTUhcg~9np}T zoM>6GYW^v2nWV{|tY$d8moaG)(KOf9jcEhqtR!LbX4%>$otBQiDuZ^JzxnRz?%O|q z30-<=jegAwVFX|1=b!t}PvLIA7GB$P_FW{%Rc0^SVl+Z?*(pjJ{IHo|DXBhX%P6I6 zL)*jzJ^FU1&2Z6Wtv6YDCGv4#>bPQQCZPjW)?UA0(VC(0Li8XExE+ixuji3ejg@=+ zqHFuh=^C*3f}d%xJpbS9y=#*s$#os*uZYOZs_Msd_jLCR25%6gxRhzv)@)aw_RsJ4 zU2)AsQ6LG5B7k{M_jFffW<UWCjL<#!OabJhJ@yx%WKIr~R&B zvV1z9gM&lN`E>LX#FAHgXzANdzX8g&{!jb1-@j>pe*OCEwp_!BCPZrZkZ7Oca5(H5 zOgf^>-W_57IqVMumpCXmBIb_i(W4v442pwf60v`-)xxj7{Ho!?M~@yg5ZM4_gPQ}8 z;!c3Nom)GPcK0I|wZ%U;G=S2sJ0?HwuE6prxv%CS4ZF4uKe)MS3wb+Z_5B_^xS7}X ze<$69K^no7PRHYly9$MYW%GmZ6 z^0QVBB^@cDf@2xLe;KC1pe2VfqJlX&fpeX}WaMxhr?l> z*PELgTFAL!X5&o+`V8;^6De1<0I)gDnAs@s^i#4^GqRGHob&f?Bsy=ku+omTajgk}putGv$gD;)D&LMUo zeS0NgAF6x1tur-)R^f6v&ufJkbo6BBQu~)YMUo6W@#|6y^wH28;7PuNK8P9?@gzpi zNdc@xWjBD*MjmO9Gd5-ZZIdyL)gqaAz1<(qu}$JdNtGu!)U;@XhE2K%rIH#m*%Bj| zBLZ7(jy9sTX>2P&Ku?ycsbY9lyb6mv)_@gN^z{6X=MR7UUqZU`m%iww5MH{YZeHDP zSAY85^56j39djoroyanLQWCc1<0zU}BOm9xL9!ZNE7~>Q;(NI^Nk1?1d*(55lPb{@ z-@Zsr!KO{B-LMo{*%XIYEfanCgcPR+i*`)GMJxmIjNZI?-7rY21QVha{5+o_5o_RP zOw`+r$c{(%DzuINo7=Yp2ZW|7DfuXWa_nHhiw{x@J4yt<;K5CDH31=w;*I@d8b@nr~&H+imMR z*N`WWuDXC0fGhNkj-Uu%lDvMBl4X@=hG=D`g$dG_`Hp(!L?5Zb0ZMXaOL#hYmXGak*VtU2m)Ud%#@fExdoQ zJp1vh>nFczn}N$JcqxRJgF>kGn{l(xKmMdLJB^#{OaYl`dimRVzX#V$(A&1$6uMwe zQna9@3owwTP@!9u3Sd?llX|fjTzd_DN>kn3yDeT0*|6+Bg;5cO6%NsuZ_w*GDIk&Q zwe5Yuhyllv3qW3X@DW8IZF>Qzp;hGv=+hBU9u7NZEV!A0g4b79umOWRJU{b;I8J) zc8ryP7BloQXK~B0++vDeUY6+Xr~2LFa!=PGwdO>{>|;4CWBC`l|W$Zmj_BnH+J$mDOE!>iT!ULOy zY0ArbygOZA-9)*?bJFR1sZ&=#b3XQesrHk(D2uKBU~Z^RVH=ge8vgj)tKy$~{SKRn zsWoOYJY~>(h?{SoAS-`UU7cV2bbbEQ1_y61aPTrCyc|wZ>;AJZ|C(4wHhHs0Yzp5r);aqBenQNTKl)xCuqQ1-OSY4jmj;O-&a}D z;l#YHUHFJf!?1C#_U*q*z^aWNSgENlnKvg<{k<0JW@Y+l%F~<_n6!f*=>$c`-QArM z0jsTv7KxxpEN)Lc;*J{RLK0Rb1qS!ul0BTa+{6`og+d(DDgfbQl~RzR_+fKb(Wj6; zx`p2=)yT;i!8!3f<3(Xa|uZE%2H?sI2vxfYzRSPgs3d(R6l;QY*rthBTjN{5~J{ z=Out+Uh;mwZ|fADWmedG4ntdjSk+GA@1WN-!-tdDaoA1cX+BR=d*yZICR*_q=$A(5 z-x)%?CMUWbdXEl+7B2BYGaI-*5?`W{!VIAORJ0_6`3xBYsU4OfAQB&&(dYyYVM>#n zS={MV4~GQz=Um!iy)FTrUI0u0Pz?*a8(9=zCPwidQK`WQmD4m=Zk!TKQ6ipAwNR~2 z$FPoe!!h%`u=A(5bPH!z^oZ#KBNyttaa>Hypz6~xpWm9U@3LX4`KEZY(e~C(2Ob2D z#dMQ}%^I9aowZn9LqSeUJ+1fvX{)218{{X|dJp`nW@5N$w`))F?c2Aa7T>;k!|CrR z1>Y`#GqtSH0{JPlYDlM^mF`98w_C6~$d*yg51@9o-43R|Av3^_juEkKq}nN7Umf7w z#)pls?tsq3OfNU3Lq{-thZ(X?&&q`}4RR9?eE^StWyCQjj+ye9Sl6gOqGKwaj{Irt zZrV`6{#vW4by!L4HbnS11T{Yg?Iw4ww`)!_8FT44Y7=YN%_Ms zf7{Sojm=OA(r84C^OZ8W*nXptRzu4%^pVPvU1&9Jfk^yD2A~+c97?%9oR07YJJ09# zLAz;!P4hUaPB4=jQ3?AUJllrqP+8}5TSJehwu< zLK8~IHe>^o5HK8%ju5t82w+{qKkZeDYeE@VmF*#i6m$U3XDB(OS7=py$@4^pO8_AS zx$O*O2x_G^z{X7rT+kR8oB8Jf6|N%4mp;^kkPYnChO{DKco9(B25Cb0Fx<)P5>s2< z;L-7nOeJS6Ry+U*@W;rJomUK@1aP@G>oh{#-rVf;oRkcqm&2?sL5x;NFxO-b1rl!; zae}=0pDJa9%f8@0aBABCS*Z=elvK}$-a!W?MUeV2xY=kK<-8YrAE~ z;|W%!`}RGA-zOMM6<4QJI>qt@gweHgmP8o_t52ctB1gPT#7a@0C*XLV_>64`dRp%%^xbT(xUk z*mZ;?QE_81b|36MNNCFM5E(FKJvXooNhrK#*roIVHm`vy*L9f(BrlI`$JM?nhwDRo ztGnafa$exa1_e-6(y}kw7J|(rB2qBlnk;iycs0>x4kHC~ETuC51%;d?6GSuO3hiMw zRjSm49L2^0i(h<{E5;u!?C)l(^3_+xS9SjArGNCDU%88w%!Mp-IeyKoa9QIwZ`$Iq z{^XPTc#mCc0Rdc7l5L>qC8Ch=jO#QA(}k%o>U&g^gwvI7SR~xD!=vE{bH`^)n}S+; zwj$Ff8Kz~(nx_<6{#2Njah0U^aU_!hqTmH|$3PT?#eZP-?zkHnH9l(D+@UTa%pFwG z0UXX&G{kmki{@@OwsnO0JARKCrp(~=QFAqD9^gwuqiCg=2cu)G-S?7%QmPJo)dWs6 zGYAda3JU*{sDKNt7`{s4~3PqsyMXB+`$@G8VHzYFT%%Fvt>fK<@Jerh%Z5z*xKcw^t4r7bVTEWDruKoG+;KTZz|K)i7>7t%a!|faLSSJQ{9V!D@@N z=cbVws4;CNTGl{kW)(tt)L<0=oST6{FbEt16b`7aLon*VkS3n#!UQKQJu6#|5zNxG zI0V^OFc{qR^;KI3+SXkNl*K0+D|$9 zR?dKVIP}^f^0%k{Q4c#lX1zs~I_qlSF~uc$=V|zthW)A`HucJ_JW0ieo)8|gQbQr5L&^Iy>m@*^&x9h<u#P~}nx-`COB{kvoQ_O8siZJslk2(}6sL#A3FX(2U+zl+Wc4Y6R7?4U52 z(uaIOUNluk{`}oEvVGE)920zPMy2 z#AM@Qha`GcITbbnXN5E|(5J0dqkJIMR|J~_EjthGzru`V*I8kQmc}R^3v=-T#+htm z^2Eq^3DyfWE{ZrEiZ9?#%2!kOs@1mSU8Gfqr@xf+7ycga*c2q-m z5TaU~eg(?LvnB`=u*{&W5Cm0~gC7{T>R2L=Hgo+G$ zk_=~1kRMyfMWGd1{b2-}=W1tX^=aiCmwHCrzlkvs{MrVEHPO4(BI{k~kWaBtbyGcd zagbmsKO zuOI&S7jd|2H|#PU{KG_p=~4*a*RJE+xAD_2^7)7EYF7hAUoyl}rRx^Pn&PWSxXnD+ zcO#n3LnKITy>ez$ykeL<9U>*Nm$MdNT{WSi0+?XyLW64Vi?Q~Ew(PL!W~p{TBM&O5 z+kUy2zCq->1$4n-7*{!f15?H-lPO3RNxMgyeC3)hp|s^8>~>?rOki~mQQpmLh-pl< zaMqPH9Hm354P-RmqVhy?1)TEA<(7@ZWa9h46|fq}tK ztDMJEpj8bTZbgx_r~#{7w48AoeRzktiFGwA&!C905=*s=^3W}<*-|d9M-n>o+BO@o;k`*E_W*Wrfzx5`9`Y*PKu7Q3QFUk;ub6ikPz8JYrnT=0>rcCfOtv zG`dag;9VC5vdW3uqDF0DR15JTt&kY?i6}}k#4L0P9=3R{NFNLRR{?jbLCpDskKKnq z`c;~~?I{!fvex~<%?F*EE`{*DRKc9Xt8a(D`bGZioATg_UQZjFT}4lJB5Ede?Y!T1 z)I|hur&<^dN?2GAd6uix!I5GqOV7c8Ul@euBZY8^Ij+I@{wN+sX8`qpr3G7kOFKm; z3V}){EOSl~v?RML=t;vAY_93vsivQ|Vn)9hCbmnrE}V*Xnu@zh2on}pTSB}6qZkm! zBk_QW8Qorjt!BIe8*O&(yZ-Vdd9KS$8boxK1;UIwyV5MUCJsoa9f9%VMYGi`QcYT~ zo6vR&Yqc9vl<976WLQ6u{X_g`wq$VHG%lm1CBR4&4@7!OW&MO=B&SSQ1U-G}D9qI* z!%?Mea#HoRd)J`xM@uj@?9dw+(vbO_q`6>(g=af4yGX!POch{>hP2FHy)|rX;G_^&st0^vPRnj8%RQu zYs(tPBqto@U`)FSJ%csFXtZp@bkB0P^8Y|>!?}D+>GpfL+m(-==8v8@jLNFgE-hvU=Q@QY z23y3PLTXi#f!@H^*cNlq+cUWyk>H@#?j8WygdsWS-pNnzX+e^id!CY11Xbq99Wf~R z$aQy2;$CowvPlzqgk7X93>5bYF$S{n!W^DeOMO?AF_kN)2=k4E;*5O58=wMWksv}I zTCkZNxWEOVNDH9{ROF>rHU(kmMX`o{1B*!m!Q(Za@cd7g3XvUc^o-!bp)!ubP9~Ki?(a2QnhbA|GLq^`d+0t zx;}7cX6|wJXcQforTh_v29~Yf+N#)}@saeoImM+*XG|}o_7wh7WzG!c4qA0Xf^7|W zGrdLfW~u{4A0?S-vD`tX7HbTw(W8oQM2}VVcilbApZss97k~Wo!{e`7QM_a={eibe zhYKQnU&nb4U%v8ReCy8HU3+E2h~J^9zLz0JM}htybXkmR*j-2&=%jWhx2|h|sTgM> z>;1&V+D~mW4Zdh1SJH>XRRHGQbpB$bT(Zr(%&DwSGHHgM+Ky4WXSshz8k(U$2_|0I zWOfv8t@2A#yhc!umc1&P^F!@GH8b6}Og4}a$4fFx57gtF$db(&K3wwF6W-KyR4-)l zDp`a)t*a^HD+}$c4TTilAvGQ)7gXy@tVB2bvc?o4>?gXo|D*1)#Qct=EFMp&y zAzQSTuY(^T>!ege2GVkbli{?!XiQzl-baR|h$sdHlucTP?+7{-u=0_ixkMGNSX;Q5 z>cd%$2g@||W_1q>L<1u9Ko!Og7somBf{#XLb#u(=JrC_UmLiAiET1!~;N?gthgHnz zz{#RCqcdXKD{ACkqMb6ZADu|CPhth-qbBzC3Ynm6wr+0SnkJI8Ym?yMHLQi5dRQ{& zzP=U0^9SGkMVekyws<-IB^1NU@m`Mh`*S*n*KgxF=jz_c4s{#r+xd+g^ABeYMt?JJ zWuS>5J_q4r+&15Mfr#VNFp3t)J^Ve&v6+yE4~PWeSUq9D)*%;>U(as!TKc8wCS#?@ z4bmtToR?|b6*89==3VqXyqy*5S$BZcPl?Z>dH_Ay0QxGV*4=><6GXNy8^EjG^`ouB z4T~Zk9i&!x&A>*cErl5F;DlQ-XfH(7hp%W*)D(xI%0!C0UDS%N_jgoWsPN=G16)R= zL3JVwXqkP92LqET$*E8+ztuD3vkwShk|AfLgIY3bsOvL1>x8>*-d6agdVQ~p#T4P} z-y+v(06P|KRZh^PJ5>6-?!LBCw1NZZ!LZO`M#wFRV;$Ni>0AYU@cEvcO+1Wf(a(A) z@)k#}h(zw3iD9Ux8ieL{AdQH_O&_7Wd`JA_wu-Kr-Q#j=AO9gPu|YTpJdmM*r;ctX z>RK-(vKF#kNTwSR%mTPWffa-X2@Ee&`Yt{rs}S=WRm_Tjy%1{b`J<&fa!2-MxZhCY zB2WdA^ZizaL56e;!qAsx6u(ijCIBxR_7&&-qw?~n$C%z;s^Fy%UXFj_(SC>Tj^Xof z{1>n5lWRAcyh{1JmIQFWu-M2@!sKfD;GC6oXFNBrL<(<~Y_;P!FYbo#tEvm~t29Jq z%cxd=<@LCzrHm_24Qw8@3M7%DD@q}cl-a>Kn<_3(9keZ}zzQj>3w&E>F|)Qsqpct# zqkzkO`_~eLXnJE4HP9xPv(kb%Q$!GfPg3b6)v96|M;3{-?aOVbCBkZ3uvcX5L0b~k zzKelm3GgKXXN}4!Z?|GuW#LjltZ`vscAX-7*#=BYhL?!x<=GtrKSaK0Fu$(y<90pt z$G9+L$80rk3xB}Ssb&Ui$OW_}>s#8ELM|{CH}By#mu+cguAYtk3{CdrdV`O`WNccJ z7izr&ofTGY)#lB~2pa|{ZMO5TOuOX0MXVB%nB>l;@*jxxL!TO23VTe_Mo4KQk&SY?UcxY}up=n)k>Iul zw_&C8ta|wjVd(~STML_t5Vr*r)0{lWJr){yM_(pkM2-Wi2w816MB)xUy|iD2AuiMP z`Pq+`w7VM*rw1=S3+Z%O)h>nba{SW~;oT|z{paDoe&#-TR)>)#3c51=4vJb~cKG$3 zpK{&%o<+l3v^Jf9xdusfZX5;fzXMyB7~{osUe7WLc}Ev4n#kti7%G?dZfJ-|7aSG5C#1;4vzmUUTB2@cQx1CZM*FK`{6c#2|zOzFTOvt&f&DA`E0u!QlI446z)W`}*wMXaxC1{3^B zlShiqZc#U+u)`XR4CF~oHNzcEJv9qt#K?Usri!*3fce(p`uy^z>(%2|*Uvr&HHI#3 z?95z_OCh`*zrmRB^RL3+f9h|47_I{d``Vd82W%su&;ylvX|sh!N^pw=U;#7#5wiBX z6w%QQN?A_$O|IR{+l!vZCA)4dR(f+P>%i6s76y=7Gs2#dS7E>#!3PAES0oO4*?DMg zm&tBm5#xEZ!if^xaExy?NZmrz5as<=+Iw)7uQc~+SR^QzH}q_vjijm$WRPV{&WR4C zP8EwzSABr$SzL>d0z16XlxtqpRx4D}=p@rpU}Z(I;18L$ z@HHzhqBpUzYEt5DuFqw`m{Rc+RhgGPoLC zKAzE2mJ>SYyrr9+R)|wcnsuv__Mi7XxOZlyo!J!3ugzRp0>3Spd7PT?LE6&rUq*CS zZC{eWH*BBMz6p*^^7b*bM4TH`-*u@43 z+EjXr2v(U?Pq^N@o|j>F_u?m~tEb=GJpCe0Z`z0ZOZ~eP!preLbF>x3z4|u&?Jwdl zJ}-}UZ3(Ov$x0uNuI+W+tbdI)qkl*~X3=kd`yb93z4o@%oYE?1kMPSRVI4_8nC;`8 z8mwSOpecq8#A#7doUa)D=H*7?|56htBpUcZnU$II7m;l{jsqOFNo0^Qn@s)pyItbS zuAye6G*Ie5x_Ds~NzRxf&ZOmuc@&r)wf`$D6}nEN51cx8luT3AW7?3mw_L033`*_5 zah2buQ%sVR4$n#pMSPe|vO7oTakeo#b43{oo1D({16FyQ1*Xf&)&6ZzT{i=pRcZODjpb|X$pJSXb5Mrh?r8sUal!fm@nv*XrgZltl5){$l0}*f}WHZHAS~} zE=zmeoYizCBJn1=4tQrI8i4YWIPsG?K>IfNGkUq8cfpxYY6p^&QdCv-`c)`des}{K z;DDP|98}@a_Gl>Gtji7m9B%wO?%6>wtpZ~^8+iIB(=a2sM?&%NycG!JG zfy9LxdMSjL>~q*{?e+$)P_9jqH>sT69!1}&cus}YRSkg-@u6p|CDK&SSL zhA$j8k-ia0<52c5aR%e}h|;itVj*Ms2%SBFTvkZegS(&Pcy<3&QI!0kg;HqAN>lL4 z>@8M4SsB*s3>+8B1LQ3LS%%$0S9>_CGorug@Tznp=*3x{AF`_83EnlC9DLGa6Cm4} zceMpy1*cWGYsZ6#NI?eBd>#O8#vBo7!x@56$q->(EUjOc(6x-N^GGqAlDUCo-iQ-2 zV}ZKSLC{6D`ITpnH`o(H>V3ddAg%=l&BxPeng$VBof_lF?&xjnSy z;loD_uC^y0*xEjyX5+ z_WQuUv&rieG-w{$WszaG2KH^Y;L~|H+&z5p`MCSaUHoGHr5_g^;rl(>ve15kZ;!A) z`QXYO#)=)f1(B;&_fDO#wYx7mT}~&eixx#&;?1fps23&9zbF`Fz>|4y?<4t`^zxL7 z$-PqTIIFKTZv$n9E@;@3C)kw8?Gvjl2U2T4Sw&=EC2T)3!wAijn{I$E_$^jg^G*|z zQ-+@ysYrPr`-gqv*e`gavoaoUQ_jh^#S(}t%h0Ao=2pUTP7z$}!&YbQRv5>WE&si` z4RCrXE7}ju>r(Frv=ZUvoEg`p(2rE3-Pr zg?}J>w$BC8Ri=-&g(Of0Zl-+P$VA0(L^GI0YzXW^^Zu~kjT7g;EANtA%Ng1T!-K@^ zrP!+A@p$w|Q`(=7rv+2hfhozD@|JEZqbf*p5>rK31z94aE>eyM)#leKHjW0a;_zfw zI%kU9PIemPn1jaB! zL*<5kTY|D2i_;vL#9xhAS8lM{0~xMP-~G3zmp}b-|KO{T&X;xTUpgAR=m_8Q(U$VB zUx)wjv+=x?-EjK9{?t8rNDLInh^zN*iCbr|H|4mc4e~lcR)SZwYO9C}IOXmZb2#$# zC@a*s2!(=>p{$)T==lnxoU*-&pzxxJpkho#qBx(=u7W9=#>8v~OICsbB=e-ibeF>! zGM=~~f>0wDx1b_K>H#4nYl#iFrqtebXs?@lVQ43s>F$^~STe?}<~}eR&sZ!66W-19 z%=2lhW#Wn`;Xbt}v@eMX)DK?dQfcPx!KX#wfgN z$hVyGx&=}4qWr+@ns`}x?lvyfsI{uenp7T-L2oxh^Rm*3wdkQ)e2FbzU3sCw1xl40Bo4t-V&Z%1}tGQye zl5tlt&ENUc^NXLTBbqNA(MxOea{O*;;oIBrv%eeu{MqtHFY00McB9#GS?Z^)l77<~ z8eyi&ZB5oJ-X>PTW`*1bo3B+h*1t3f+1%Gc=dZM$%~Rb$_A5bYXWm;omC_T>)j##B3g#+ zN+rHpRV22$ye#wijQH0W1nAh_%w%=Y52PBoV;YkYNwyduV60gB&SEojL3k`Erltb) zy#?Am$;2`ZZo}yu0YVQ~r7y&Cniv?1CTHjg%hjY810oviQ}2#+XyFL=Rlb?L&>LYWMCSlUt< zH(6?(z?L~TO+hxX-HL~=T);U35cAzcwC!^Wij*^TJcS?ZNTt%Teu37NXXK(R#o=>1{W zFxKtuTL3RIP9xlH7@Sj`EmT5MXj?gR5^<VLz-o!So_4Nrh)mW+ zy+F_%*0V^CD@YP_;VO{Ef$o7V#$QU zh@>e&6+qi;uj!+zGUvBLOqibX9@42x6b^DSk%}kuBeflQC2jX`USdeyTh%<_^c`q7 zo{dekl&KFz{L`|S`kpgcWWEzT4;7X%l$gi%=LY`bemt!6dUtn5l2a>vtq>-(riWpi zm$|{>yW6`!64tz&pyQ+g5J9J@`)S+ONmtBh4;bJzfPQ9HgLf8^ROy_{WR0@2FCjq% zo$-nyH?kBbkj*e5x^u)JB0_?s`q4$d0A8K|Y7}e(1hQ(`?WAw7zQZ^r-%GOZdDfCY zb^`$ICwuFiqMB5vtJC-Y-QA-XpT^;MS*iZ@v_>z5@b_?N74f&H_|>=JSKowr%@6mP z^TNm)Snna>yP(ips)Wr2Ju`W;Ofnc5!&IXbs*KxQ&q5_bj|sS59X-W1De5rNu`M9L z4|Gn7<-6Z0>x=6){DN`L#D6x`Tj{F;6&gW?e8@OGl3t%?GxOYDzTfS@clTkQ7uNxq z^WAdB98>7O1@Z(~JLF&Z+5;CB3J+KwD!G8|v>i!4ev?vCNFF8M?)=P3Mo$oZUgkIh z^c{ij$v|?RfS~Ook6w?$y`!vHRrlyY9j&u!lR8_Z5fGo17wL@7Lsv^}U!wWX zv59AIVN!36+>lWZkbytP5AV6P25`?uC6r{6{80&X{LI~)2>~KZYH9emxalO zxxwFx_IsF5r}j)v^Bo^OzX=SeCm|PRovec^Sz3YL6Ge+)JJmpBLUMhS)>D)0q;1PK zFV`HIgVoBMNKt0m?Z(A`J?7m#`bS4J`o6<6mUOR@{IzPgZFg0!9v;8*XK$W<@9&1` z+lzhZWkz^8e&>h~bCl!B{rs2dv#;IPufkQV5B|hGx-K@MGp=G3N!`q^tFzb%vwVy) zX6ueEOoTJIHNRbsSOyN&&x0-f&$>E(C49!vT>dEO8;ZWFpWU_ zP`1vH>PikpL=x<>GLs=lrT|cdbf+%;_R1GW^n8_Z%#uk;EVRC?-H8+}Jy3g0x5+YB zRmGWF#SC|qLrxPjo#C+JM%D6n(wS5&B?yth3fi0CyK=3UiK;gky6HnqHEIYR%#nFs zr)gZf`hb(gxQR4c5!6N|qap_5mqJkhRM$SP9%o@pl1fw*=$wa)<>Bq=ZF|pYH^ma< z&brL!F^xIT!?-)o=k}euy18oq@b>t29Cu#%Lu^Mw8%hRJ+pJLe9?*A1;{XcCR=Ow( z%*63))c_gRK9)5oV^8BX^9$W}XvHu!8ymc$c6*lgMZ~uSy@#Au}L+cOJ@x_#I!S@u=PM~3wk`t0pm^ite`m;!< z6q2#K)OX4~Xs{|ed}}{OA_k;AK!ROlqODh25U;~Zr)URLLu2R#;9@l=gIOG&Qj#h41~Oz^0?&W-<dS5UHj|0e`?b|xo@+jE|u?U8H!w9o% z^EXjGhm#J3;k?0C+aObusF#^oaPx-ALOEjDEJAF$x8zLH13D-HIBGw^u-S0WJ|!Gv z4>OyGfY(k2NDeW1D7XNH(=?fY#E3j3@e)YWuzM)Q+&S@J$VDP8M`?k~bCUZDlKkX? znm9JD?CwsH8IV*pgsjjEssLZVdc|>R7>cF6gPd`hu4(la=Bzn!d&iPF0**7gY*B{Ab!8!g zCM3i!f)wfvzC$pT0H8{vUg@+bYiW~gkSpqdnUJoxt_z8NmgQvWzLa9j>kI?7DM2BQ z92~4IP%+!!%yb8iC9s{&&`MhH>)l;jg^#DZY22;r=!%9C>RBSGJ)077D1-Me;6 z+vU_#i7sYK=EE~eya~{NxCXMPM7_jXHF<0pF=PDL3ZSayTB^L)S4REO&aVCK>gwun zH8nI(suo&~;~3AUQ@e$y(`mOqlnPOOYX3B*_9oMAFQhf&AH#LCq#}oIjDE@eX{Sy9 znhC=~U^!5KU9PjAQ>%Urr@Czr>_i`xZAAORVCZZ?j7BW-Mx*sclHpDSJB(7@eIkpT za;bq7dNxHxmL68BS|5zViyNq57WCT-rvdG!*EF3!{*yP4KKgXrf0JmRc3G7EH5bFn z@%yQQ%dXZBe0&kY%ivory!-pzo9FXkDsk5t<{)#@4?hwNpr~-RXR0=>p1I3`nVe?r zF`AO$G)J9s(obw#h1Xq+`){Y{=Tj$evwqJk?xOj(R)PvCz zLhm$cRSa8#BAU7pSID+u@P_KL1^%)Y6@+Uz2@qAz;L%mkGL_p4v!5I_C(12advbxC z$0ho>4+T^*u#Dn(Cu?H=RH}LU)pa$bvsQ5J`GwWY!90Wd^siv7DqmIz%Sp>5VI+Cu zsG74hcr)OYjLZx14Hgr8dk}3osbvC5d+Zhw3{FiNPv^No!uB5L^U_K@#rRW&Y9+Ou z$vEw+n{ID!olm>NNWX79cIZ~~N^1s_=B%*A;djM>GrG=;Ct z2=g+F+)FrEbn(S6V6m`S&y)+bDNMsEu^)te8162;C+f5XVQw_4r`{^dt={ZU(KZOw z?2Gj5m4mI3Ata4!^>;7ulKd7xjh)v*2-4e<(teoMOwrR{ErIIO0u?ZmZR_QVZ4k;!<;)pu83G@TqxaikK z%Z~xFfh2aNW!ZSTkYsS;naZb7T;D|>-Oh}<0%@d|84QWj`3U+Yb%_l=F!xClq0(T= z@$NK^qgsFDQ>-IH&wW?FZKx765^%_AU>r992sALJhS*khGc~Aoq z{Z`COj%BrLSQax1 zhx1Y}t*rJ=)d+Oek!W@+JusGW4>wAjg-MN&##3r){fnq?zI zg5S>lG|`~Ec8wrhKqV*yI}}=YOeKkx7rCH8FX7DGM;1w;yXGisg{S0|U(vo`g@mbY zl&3i}C&3aEdifcsomxo^Dt+in;VXA^>hZU)?RYp{J-vPU$ye7;Kl5R}tVoyE=minJ zcPu#7&JOIlyD##fH`Mfkxju>%N(EKZBj#L z;+c1-Ehm_Wpo}g>d2Awo*2s`QsQ2jfE#4`*a3sO|)sc2sT+dbKm`uV92)yK(#g$iu zMhgy*3@wi{Js_n}sI4Nlm25YSnWozV7>SkP20?85hBCmUK!J#LHA*2XF4r>|)i}TX zCC^Q=N!(I>;#Dr|*}+dm267oe3QS7!1Yu!>c}NG2s99|>o%4CxAwZ+8+NxU`w5kag z5xOw+Xvg~mj9Sz&2C`XZb6ROPw!H~6Y)w1ZXMW-ep(5^f7{ayB)pRXL6vx%u(gs0U z^6(#J?-b$?-Y?RQDlU$ghu*I>G86)y0NCvySUG|}+O{*Pz31FE7UR^QIlILEw9MjC zCEBDZScE%=yx(25ce^{DAh+00bwEd9UY1si8(Q9tV><( zCX=9nu6qsQL-My8Bc^M^lq^TBuje%QUbNaioc1rh!p zjxxFBW9L2$@kxqLhwyv|&tkY~o09DF?9Q_rua@b04vCaEdKR~JC#o|^x;(?agEx?7 zY&-Adz8@qt6uE+ZQAL#Y5O(rM2%$FSBhC6(R zCm>5*QRPH)2pZkd@mWS+_1;p(ux2q=k|Nj_dpIp>CvjR3LD!(AO->nK;FPK;_#%j0 zyZxJBWi*qTm5yei$JP&Uz#)*OlvC)?g4{_Zt!5-ZFoU8c({8?F)Kv!yT_XJz*jkf!5Lla5 zY@A07^;i*$kEp>+e=2g~MmNxla>)T)jN>%U=UM)rhu!XQxO)5Mje|ePV3`s$_d?O8 z5ooJvyJ>$uop!@$g8EY18(8Q(Q`c)>QBA^6Vdd#~CXw1y9h&kQ*XgHxCva-dAN zX8xenMv$Q`zbg+*Kd6d>&ul;T5Dcf0mGL>-G9Te-1{Y9`E_Fo)bE*b%;@3D0P}M?) zR>ZU*bz@L^XYF9XA z@1_YAQC(0N4)DpKF$tOc_~5}q8EV5%jvJAi!!@nWvW&&4(-#>^j3O60Zf^bn~;K|U9a?8ltK`g zEqOA?G2UXi48p%-%}SQ6@3!rc?yyX5@#}bfzIysinqGT<>4;vA3nKhIRKc2`2lpby zXS?|H5T9N7$CJBmzo)ZXPq_ca5rywst30cPDn+^{_Vhb$aw%Bvv1-Lwk5T?%L1_@h zS4*OG7HnZWek=<%?K4D3Q_m`-ahlJ0X>b(M1&w3g-riECAjvuFrKuE^keJZ|< z;s69DjBBCo(Eg#H+}6&RM(G-hdCHNh!jDsL&h;$yQnu_TGa^+*=r#tp@b%+ih0vY8bZ&6 z&CNOaHuZ*N|tcocb*i)*e0f1J) z1t+HyX1-B)&e}>}n$li>bvVE&;I#Bkl~?pZ;#4P7COERzPSd2kg>f40?%s~W)b;FD zZs8i%(d!aCk@I!=aT;&cR`MTLmIE$a=9)*|=Otca+sy()4h+oXkLqN1Wp z)}f58rPE}YC!XBxCr+|5>4(|(Q2Wwjrvg1p%_)YYjR+7ALr$?t_0*hY6J|MvYH~)- zBR8Zm^kySxU{)n6r@TF!odaPXq-c*E_c}UWbljX@hSB>9GG zb9J7VG$0931fFrk7A7&4mm$%WQ%-n#4=|MJomD59sXTb98T;w0$UdsF9ptobhJK;f z|K6u2O<22*;`FWSei2HS6ymJfqZ!*p%Aag1QP%^dd%4~`(zlSypnF)9wd7a@Z=e6< z^ACUWpRb<$Dh_WhnG%=dQV8Gg(bl`REPqt}cSC#`!iUK}iS9ut`&ON9{rt+mQw3QF zi!XCnj%huw)6F~{+KS|3w9y||1ZC>6FK7Kxd9smSLu64|A}Jte065nyxq0yjgtSriZiHAZt5ryTM>w5M>spKrh^CyWxr>ww)PD=V za^x9DWDq(Fb&8?=-`m?;_++m6{>F6@sjYVHWg`gvtq_v-QLK;HeONT5TS?s1)`3IQ z?QpV2Q6fjj$eg(%F(X^gtvA&4_f~TbR|ja}bD{YtmwpZktw`wyH0E?Mh2!)^I1fFHfF*!@R7iV5`DDyU^)txjwpjB?WS@@UH`yTmd zHZq&pt<)#xdt0labiFuiBSl%RDwSyK)=OZ+!VR z0w&Z#j6({t;~JhwV+o280N~PQoa<5UvAtYfNQsvhX@+>KFH`g;f@A-+ zC5s>lmd$)eU?ArB)UGR_+Ckc7X6-uT@^DyI=6UtqC)`^}@!f023|LGy&=y=o*jrwO zu+cIv*Vi`^FLD;O<@Qm|h#FAz};Xa4!3f zT0nY)z&y)8&TXNB1SBxCrV>rJS~L|&G9)&E8L2!@R>t)~@up~Xi48*kK{0Zen#da& zd%&&Evr=z|S0)QrR)Da|X9&@rn}gzBu4gOzw?j%>@Ne59w0(&XB!eUP&U6!_??6KS z18mzCJ_Iy!LziH3C|Km)sfx0B*s5utz`7Ky)JU$|oC`4OOQ?;H*{yBj$*#QN{;c%m zXQ4Is&RYcozLv@vtQxA4a&k(uG+{{D6&V4_axQ5ess{+l?J^#YAH00^?E9a8@X628 z_)2rZ4QgJF%W)}$zw-lXrt4b1GS8xiROT4bV6&P5*$Gi337` ziSE2CFy5q4?|eQD+Cm;uG)HW_1zf6~Aw}rqTG>q)VM4B+=M}8o1qFiOIku9zHUz00K`I8ELKc%n zOwc_X9Y%h*6~FAvV!Pd5F?=|7vl0&FL%Sm>u;$@&GPe<$0@8D?s?;m%7QhH!1#U71 zC~exm8O8yr5A>o_3lV;>Rl`5p;9-1=gx>w(Y6d$OV#SLQL{TS489Ra{rpJ@3REb^7_0scKov zpH7KfhBrN#)5<|s=UGuL?ViPu$E(wm?|u68lfOvQHw1!x7;rf*$FDmsi14?3v|rq{ zE$mO6|6WMnP2uA)JP!V<_?QFMg-8DU;+>ZY>cEP7SU7iR&6)^}rcZ4AAArU8`sWIMBE)=y|9 zbut>a2m;AD!q$|NfF072D!gc61-dS{&sf>=#6zyzz`A#H?Fz%%2QW*MiR7~Nbx9wU z4>((=R+MQlAFPrN&xcDsr7FUX}+b;7*GnOsO{9j*Tx zluC$?o+k8;-`>8>2u>PK=QDiI5lNqm(i-&L=}(WHN~5E+|81AnKqvPl>uxu;JH`i> z#29jn<t4{)4(>`igFu6z7jf;1S ztG`jgSu2F|?f#pe-|jy+!g>t`ir||#774Dx4NoXL5BudH0tE05){$%iMDMJRde1zs z#)>R*khXnM^fD*4by=M%$k-}_03Fp2(903r$?3z(nA4e^r;`r=x?CB$y4pz(-qP+j zN6ofqt`(%4H6vMK9ELaF-i^R#gj2Pr8)TCxXoy!)K{GtNBhywb%BS?f6zV`jdG~ug zF=A+}VmGyR&9!W>)q4YyTgliAgvv8{cdcfP!}>HR{^oT7CvX)tO`q=PS(fYsp953 z8EkgNO+nA8qkcp|>d~r5mI7Zum?KPHwT54u))ks(CKh!FB>@*}+(uQhQ zIrB&h!9YCbw+DHX22NX7OH@GWFU$@E5Y3PifUltX^XEgsj)A;hx0EINF*=bh!H(Dp|@Gz3D?3p}#7+|XV-i^JiX&P4d=6`ZX0ZBK~xplmSGOO^HB zh3=yh4BknM8ZAM?Afh6isuxzb7Kcbwye;q>^fUm-&Ny6U+%**V`tazB$1nemX-emN zz5JyLUXDv4d_O=C9iN@^AGqPe=w6KO!{ncOe>J;sb~XRczI|JRmgCjee^uk|)v%ud z%OZgbHL^QkOHiRJ&OU~1N(j~Ma)=rh7>vWzY@GT_fFk2Ep7b8qozS1Q$aJ~l7bZF1? zaJXupe?Fb2X;fZ_A}pyo1?Z<8O)KoNRkeX>@yK|hB@kFt>jKvWUsrj~kFu z@BhbP_mvMP+J|0xp_k)Q2;XZJq|jli;ca zZ1UuZIkr}!wA>sIf_I|!DJN<+^9xc7_Iz~NqC_$ei`xQJSqm+C2(K-Otyf?U*ezTn zSLNqLT60^hsxyG-I~*2+P!2jy>glapTT34&Vnvt1ck&UA+qt^Uud-N^)TAlVBi$Q@`Nq7_+| z8Bw}{n$S?xx0Z;dD;xmXK_)_CMjgkgq3QO-r*WqyZJYwN6ALU)NgpbVA8MaD#9>A4 z%ra6N(?AtqZQBLE+fj+J>bpXa+EA~X*M-*_m(xOXgLyZNNUm>aGPWPudE1S-WnF_x zb?^yY@+^!j$&C$n-4=mTWqnFDXX)fFzi9;3w^y^ZZsURKyN@V|G-!;Id<&!ZdAj=c z@yCBN9)9Kh`LZHij^FIKkY#@3mL&#q$p72B@YusU`{m$TA$%U);S|c9ufLsbspST$ zrl#pU>@)WFf!!w2Qp8UYe(7QW=Xxlw*90wSnF%UKs*|L7*1E<2u_p;Oro72q=dbhK zJvsT^m)dI-Xi@CGPqWnh@>ZEhcC>hV8lT>Gz*r`#yyEDKT?{;;X6V~k3-Zvth$NYi z&J&VGX8Yv0=>mbL%-dRw3t|x$I|&7Mj*~oRLFc+ThLUwxq*3p|#ZlcnlgW80@hLH& z(m)?)jo2d6Y?YB1C#}hMqd401*iEKqkME{#VB*k6+cJ{6Mqb+qHL1m_tloUEF`tdC zsoeJ!u@K zb_$Twn5$};&x^hUtp<~b6HNgk8dn$1qZiHoFqp}(a!t0eAb;mw>um9s^!+ja(5-69 zE5WohCX?uHx((1TyQ{kwKm7X1cYbwv^b5GAU4Y=__zltEg)H-%9c@QHR6n?|bMD5u zN5Op<@z;~$#*@!SSAT22lfJ{Yu-qQL`kBwm`TE7T*Du_70|=p9Qs6C-%8l4)Xovwn z+<;q`(mJX^kxstboz!l@)x{K^T8LuV_2xrHUo!F**`Y!>qSV&n2r@ri)mKeRaa(L{ zwF2z6UM{*|s7eW~SElgM4yXHy0i&3)nY8n@Cm4V$UfacT zLd?&F(@+3h-;i0}_5_m2qmj6*bc^+@UbtR%6xH@EV#@{u636*?T3abf8pwuuJ}Dw< z+U)?=ma|et8cJ)QJfcxaLu+zDWQUH36(?6AewG_S?OUNYTI*<)HiKL<#kG5sC+ZR84^Wf=M51#z}xc|!eW%C`p9GByQ2;T<~EN%b2tM1zQ2i`vk zfPnhzgW?Wn=jPwh&J@4uc^>b+8c(l=wzf`(l13ksrLB3bC(5G{&s-p*6XKm|2aFm6 zQ9K|H=_XP=h=E78It|q-D^@X@gYW!1!!Bq!qD8xQ5l1(nE~5N#MyJMQu#ROls*R~Q z+&eBL<^=$TJ-IE&VzfY70D(1koKPgOkUB$ zAq8@{PMU?4Ov2bNuZ#30#@#SzWhytJy{=%>n9FRqgG~^%=O~&f>K%xpBFQz_rl7ej z+ETF!9js1a3&QOSO%GUQYqw7qbBCmuBz*%E{Z!Ly2q%l}_N*ys@G|~cX^xj1=$@cuz?tq^|b-9zf-$5o**zq?+nYy0l9rrYJJfb9PI$q&xuu*NZliA9KQ zw~P=~+DCiq*_GN&H)ViKltIlKvw$LX7E15~1z*lAUiqeu0x^1k1Zf(8y<~tASNdg# zVAz7LU0I$t-eQN?>+4osWdB*6@I9ljaHp^OYq-i12iEEojelLN1V)vJ@5M(!H8Fa4WDV1C@i!MmXufBTYa`8B@NH&BLy-wiD|3nH_h5B!IoW z()Ar?cN*zpIV33vEw&JGj6M#uRHavG%qv+TG-8qMWp5X&KS}pI2k&xtv>~~|WkaFu z!H#nL0m}v+2+%W-)B>-caU3XqNB>i7%!T3zS?)2>7|a@s{)Q;O9(9{T9Kay;epzSO z(Y5oMXYU8WwqZ(=NV1}1D2v+7f~$>9rhLZ>)7y507JW?6j7Ekr(lB|Ru#!_VoVi@; z>9b?q)sRjjLG7*ZC0di8Q+)gE`)?mS`|{zlUyS=NFR{9pX@dT?!>E*5Fq6YHGG!A<1P6T(^@(W6~CwzM?e9>3$v$Q89E8gPeR)w6TRNDd^n1*TmsuOuWmIpwotv3J6G9fc)Cq z&db8A3kTOvX#|OR<#6o~&4>gBi*w@Ok+U)r{ws)ZV2iYsPl!W!9!QX1nUDp8N(DZR1Z)JgdRlrxFpk#ZB2#RG&e3&0IW_n) zYrJ3`@HXd=EC=R>Dxzv`C@xcap;C<|r}&h*10?y0 z1}RPm(j){6ZaBXop)^sk2ge;034y>x}2cucVz{j8<_lk6zxSu@2Mi$I3 z+0`A|jw03hd}`%$z;}Q&CEZVF`5toC4kc9DvFbafJxuM<5UPjTOlTTvo&}!P44B<+ zJ0BrP`*tw7n7s$SIjeBHtH+l=`r`SI{&IKt3lkg$m$ySi0k3^`YZ9{6(IH+*Sf}*FgmYX7#tG55KU$t!x}pK1h}eMoq&sYWgLe9R4{QisnQbqy$4PGefL?>YS5t zz1-|$s`a z!vFz2p=hQUlawaZ#=kb~M(BAak#v@h>=9@5axYM#$28WY$^Q#LcM zqUx?LYb1!P@}J zdS$`bCVV>*>4Wu6RjTAaDQD*n!Y#R64l+_Hx;}CAvJ~LZEAdl=H7#^WB7_b+3;}zE z7yy5*vp_8&n<5)G<5p+vp1$j_YqLNGfo<|{eUJUIk*BI7hRHd>5PRspHburX3K}9I zvuCJp@2@{oNv{ZsD9WYf+d7VB(>%}PIJG~W&u18AQ*H}t-`B{0hn{dlE4Rea z3YM~OU(cBieL!ktD+{cWY9+Ss^4q81{o>`1fBxwCUr)e-mo@5g{EpDzg)H-roXYK8 z{B9k8;NnXc9zZ+s&!=S@PT%g&e=)xO%;(cLAN+Wo9#o$I-K3f*ZAEUUi}*U)r~!!B z%5SC4_?w`TD1guw{QzUN5;d?xI}3B0y0Whb+cRVrNdM*ewk~&@ZF@&=141sPYJJMx zoBV(0Ewg|tHAajEUyE$tw2IeGHDr%2bRTUr;AAS+erjkxJb+YEZ9k`up;v{~`djN} z{gT)7VnTaAi1+%bz2`h1!)k4|a9sJi0&kQ54nWxJKtJ6pg@$>Gb6l6mT4U6D>2f>rIi|NC zeCM;LpZxiB_~kqC5trj~ToB>!>lj@96QBNVNS`=={m-zS41f~W<8XQ%<1~+lw%l4i zA&D!r;4l&mTw@xB(85HrpR~%#`e9Xr;F>Id6SP@!eXR*xn<_If(>i;J(^S8T%@@7a zGO#Bk%>eOK`RonB<>-A zm$VM0D!jsLeyLDbp$$dJXH_qa?}(n2bx_SBfQ8YF)4VlkYw6Gr&Z}sswqh*E1~)aB zitYCM+q)z1=#V5}UnTb7J%pE<$y{&}iXO^kF(VbDq4dlN;zQ<12=`?FXiaCpZH9H;^{fZQK|C#R%!vx7;t(Bi+V+NTIw zau!fw{ZF5Qk^#MXxn1^O-4+++?Ru-3=txGFz+oZfLy=S#sgc(i7gJ>Gqw-0eiZ>Yu z$C4bLm5}5LP$a=y;US=DB*QhT>j50B`d%d$oZV#w0_4UZGA(^`U|i4lcH1;=Y$qGr zwr$(C(WuGBY;4;$+SuM0jcxng&-eH4-*@NE!93@==ggcjii+^hhqyXHULF{0$dk49 z5tf~TvoIR^uweL+JtGk8lMGOGxx6G|$Q`HI!g0l2a%#5JUL>TYTI#O4LtT`0E|l@K z`(m?RW!pgz>@TqN@PZ%OX-O3DaIN?;-(MQQej(W1B+58kjQr6f`|nFo*{fmq=i`)L zd_DE&p}&yxmecmf+qqH-WW#BT)-Ykb!tQVeN|z;Je*WczbWG9@L6OLu<4+s{(l8_R z2Hf8pZ+dEY@sQ-nX{SDI)t?wJYsh-gI)h^?`RaPcJUqrb=E#PedR%|#<;3mv56d9i z0vGzN3Z>eE*LHq1OWnR*4r^ow>Y%B+`6nu=g@r$OhB~Ly2ly^w~TNfCq zD0mN;gz&?K@>&dh`=)NvDG)KFyM3)buRW}zo<_6JDc;2z(zEW>H&dfSN6Nv}_+xFN zSpBzTQZ-&_f^>7!iAKJ~_{t*-1ZNh8@TfR(i(73zG1Dju`A@fbm+*GLwqQgx2tAPn zm?(%pRG#d2FEB(l2ugO?E3ra*AI(~B@Ri>;FP*j=0b~z@(@+_F37my~^wa$p_l>+5`)n;}Elv{STSt9_;uc>h^d4=OrY8z$XdeGu;99S_-GFgE1?3_h@ zbIJovFot&oH^){J5Nqumn(nbF<60)ga;u>sF*KxwzMjcyX!(LJmOA9%ugbXc7+j?b zOK9oi274GS^#^x9j8=Q|FW8w(Mk&&7f!ex$1(t!yX_GJMF=7HMlPU97(Tj8QsF&7^ z4&LfN6p<^Y5+)kYsXum~y>6&1(y37YBIOixYwR9+Yur?KR1Kb@txZp5#hd)mX=Dz< z-Zfa&_}Z$*tg_eJr--Momp+r%ls#4bV>>*J3u;dg z|OUMe;oyk1(*qpgZW%OP;g!n$l(Ci?jvy*lS~6vobYf|n zlPM;DMrmRG5RWul9HFHwnW`1mx~ST`B=}a_zh7hEB;b6%-;X= zpq!&lrdCgq<4Nt?`Eu1E6J92wIF28DYB3iqY{BHn&%gNqut2mtx3fZK*gNL5|EaDL z=0rr29isd0`JQw#p;K+^J)zb)FH%nrZ{c0%e{~b3_;K{v-tjzb-Sy?~xU(>@_tDhp zLR9lp|2{xv14Q)HoJ79i_t)iV(Esypa+k>8@5xGfNeNL!sq7xT5nuW2*sVk;@}gLO z+c-jiA|pIvbGdRwn#E9>xp%XzbZCjv=9g{cOKG#67;;s7M56=^3fcme^&~u&eBI01 z&Dc?Q+B7Xr;qQw=AJxP@tOEugNEc!sjgK6JzM7IGB}&vmJ|qU3>#&S2tHp`U_0A$d zYfGt4ZdJN2_K<}P!zcIQMC`COfw-sIdC;x50Abwu{;Ts&8@$n!b&Z6iGrNv%x#><~ z61H*+*Ifus!D<1rFomPRcCpar7x*TRD0qT}%6-Z5;BnSpy$CirL+RtbI){1Ar#kBt z2Y67Ma0>(Ch{fvcsO_@cBQ!~TgzLFhKt1!C{bwwv}ZhT+RfY=hWW8ZAMO`>oC^2r}6>cDq{0e=NCUmQ| zxs0>{@9(oVoH}NyUN8I1?Y=xq*yU-6_Da_-p`U?@C6=1OPXKVU6wXdaSx%l!Z!!Ly%3-%O zv!H5B5`K(Ti7_1s4*Z4)@T%Uv z6vxkEw9S#H;iDcCoR2h;F7HLNLUd*I&i|KYAdpfGGReEsl6koyKw%{glWB3A8Gt$+ z0iu?&bTiSh6qGG}GrYmgPqyns{7M4TPELh5i$By&8jW1F#E95z20mRZpOJuno90OV zX&TNQI1xSEB+-t9xu&Xt2YE_4Z>3Dkq8jlK;#g5ZH}=kUuFT25yxQIRxyVS_SS!X` ztKjR9d+r3_W0ecvB>!MUcOZjHJ!?-komQ&oe@+x^-mnm4mYp;Epl-;kWu|7+(aMCmie7jc5;r(4*XxpW`4nFb&)7>%Mn4hVeDTG#(fQjqR3Pej}oej;cOn!$JN_R zD3&kd?IS68f6qPGKC`6bE0!o8X!dUQzSkxmYo6~BCI!7jwyE@Oi+NE{5zV+4OtXAu zS>4Y0BXZwoA_cg1rwgQADz`3j&WrOFx&Rb8sGAmxmjTeK@Ir;evY{agW{=-XSm$o# z>avt$z}*NN=-=_M4w7z9a(w4D#Hq{MrplqBGE%p`X)^s7HM1|2w1@}hn`%ouc*$&I zR3=B%aLUv!d_!|>xEHmDfKyDOkzB&BN7KC^KjAPhBh0e;3U}QqtiQj|BCiu~)q$&8 zvAU&pYlANtgVQ}a6pqT8NnI9taUv1N3^QcZU`;li&pxhMfybSanXHmcVpCS|0wZM3 zH3BP3Bp;VQ&pH$lrISdyuDp&4mDaA+XU5_jZp}14Dw(K8xl)H<1G_R<|M8K}Q9s+2 z{OaI*@JsVcKt#zQcvngnIl>g)0>&4=`%pVHI5>r3Z*FMX-g;1DVera<@tQ!kBf(ks z{dkR%k!M`+*!S#DdWQ_#?mN1JUNI(( z-F+Gp6PV*g6A7U84QK1kl!jT_Qw=n{=(4pM#R?i+$fEd?GzEel2YB+%FWh@ZsuJ)%LJ_Ac3O=?|4mex&VKY2FMUU6?oZW#o&}v7 z!z>GDp9d{ScI7N7=GzRvYyz9@c+B!*-U(Ik7_vY?U(`OaB7rNBsXKMOS}ya0*N^E8XKN53k8PgdR8YGR z)Qm;5CSD{>Yz8mYjH_2uT4Bp>)0t;rg)qf@r>xV}d4WfwH+tjhuBn=gCmfar7`@!B z3nc2G6K$S3LwkraVMI{cxSNzY=a{2alo?7gxgd3!F1o{TjjOMZykhqitr7F_4^&YU zFY9PS19lmUEU;N<(!LDe=ny#Qc{j9p2YgO3mNwRATnNXbGI4)$XTeNMl5mmwaNY(? zHhmuOo$LG_*gbGUi6uHG*yv>G^l?1g{dvmy;vgDZTr}o#FZ26G=s|YUYB0J~d6Ohf zQ8neP1DltknL>FhzDX}7;yajqPsQ$eeuq62rOH#vJ9Cq&mhcORri#e;j6EyhyTf;F zo@r56e6l=r?(X}(1zLxf7pn;6@Rk-{Y*){2kG_zKnR9X!<$zvLVgAN9wju=p0n1oT5#{6JW7 z_0d(RgW|_YlcO+W;o|st`_<@)y@lhov;uNmPA4g$(PfJJTIF`5v;4;IQL-qL{U$T5 z*pyIKm}cL}@~IemC#DER^lL#K8*ftkZ=_9Vv z@=bWoWwGz7cl0}0PCa3wLwukjP+TNrL)(d1K;u~vd8MAxZ<^(tpE+lXSsfxU2iUv8}2QKcnqPdb8f|8}vZr?%Po0(Bxz zc*rtY;aQ4+tDqnSU$x*dG*D`0w8C(zL1O(xpw8-m5w}`)9ywPBOyWqWZJ~?Xm5a#6 zdWC5$6dB~g;S)}@zu1d*#TlUh%TD}~4my)s^bOt~zUWYNQGVS11O&>1(!8Zpi$iE6 ze#q*mmXC!l#^7reBgYgut}NDo0`<-L8i#`&(G+KIxBp?l)Z%A$|14{%31S%9wzlA| z?La{8@vYnj-p7SyHR4+T&rE2;6w#B!(M5nWj;!Lwun!Bb#2ib#v-IFK{oWXUi_~D#!n?rK>a4xYN`<#l+Lb%n z-nPhz&14?6`d5Iv{Al1QoXkwIV3r0b9~ZT5HEFxPe_H6hi9D|7w*(sw%C;TE#w0S^ z57{6ucR?88F$+0T7W6J%Ww+C4Ih3m75j?Ix0AiV@8kjGjJqp{D#*E9m0y?$&vZA;q zDu;XFA!JdJm`!(xvv%?O6^~_A`ceo#m@qs|ByjcTy!$@$`GrX2wydU~3miEP505v= zKEyG1*$mSN(v7)RpI-v(34FaB4-;#b9PSI8=Vx<=7NL3P`hVaBox0rNBb$S}oi%(z zqcd;5SS=g(nCX+ob1D#Bfcc@NlsZwImY;y)))tL>E%wpoBKst!a+l_-R+THec( z9$bY0ZhvtYTLCaDK~Yw~=Vv680c3d*u1GKao8z&177Oa!_S=?amVryVwaq)D554Ohi~u0emeRwZ&9H?kx7xHy+Is+>7gaT zY+ukP_xy%jOz(Xq{~Y9S&nePlXsIuKz+mf(hlUqTKQF~~o-KKSkdPUW@T>I3f>Aln z5TAD_DvH7yFe2&9w#Y^(c&s$B8ed^VNy@0lh3fKkP^?Q{)If}WWdSs?7hBb`RvleA zN4#LG;Y%kIolqk&2Z+U3R;;kQJwtD;tqQ_-3(hd2M8>$Mqt$SoLmE|i>kN}uc}L2mdDNS?E`en*ebH^GdJ z!NljyT@TaQNv*72GhMaVxPH}`;4rw}^2`F4yAZuUsF-FHlw<5ks#%(*_*}a6%3M$# zf(WhZXlTl`iL3;LLEnPdycVLgSd&+ z#%h^fz1xFPOzL__QhPzt380(AhVSGF21mmxvhhQVb&>hZnD>|OH)BC^rs#zXaHAYD zb=H+BiF_xFYGP&Y>M=_*v1R^>{Z90cV?Vqb1R#KrJFCb*n=dYlQuJp_Qe6OYdV6q4?`Ap5KxuwzBkW=`4VlnU1t2ZaoVs1pc>{uo*<&rO@h);U&k<-urVw7Dg_7 zX2IrV{jmOsD_>&~H(J?@xz|*Ex~KQ{tE0_}+>h4>yTfe|65=6m+^)yggqd=PkyXSE zZFspG;UkfIy4nguW)*#Z9zqMa3t6y?vZjI(+ynNLwk-!$uFbv1{lAtSWp*z%hoHs5 zL?dlXwMVl18Aeak_DxzTDY0K5;-Llm!X4)>S3g4S{;%eHOD%J-5^m~~=^%NW#eWnU zJV`J=&q_GTYOZ+3dLpa@?yQY1ELS7!V0~pQTD}HXV{#=?wwSAqJ17Z$Nw|e#msr9# z=7pcJ&j*sh;J`GzOvZCWupcP)$Ew(m(zKsUH|S7Vi5aC7XA+~r!*yuWbc|_GBwY>9 zv+L48{=9I#X!VF~t>y1|o?6o7?M0eIJJ{{;-gN?dkEmS4=2!S0*Bn##Y4uFESp>z$ zoK2zwUZh%IC$2R=zLjWW0698OK#A9%47dYh*rsLTcnwW?k#cOfu%>gdNgCrVx-jFF z${%B7W-mQvc+^)WHbXJhDIdg@t7B(mDzh@kDax`Ciy0vQK4~l1d~v$~ z)o=iUzape2KYc>Ew3<&&B%C$)VMFlY_Ve+0aZ|XYa0BnL#^BX&9zV2>cslAorPxk{ z3yi$M7vTBVyr9;VZ(q56UK#ZEd^){eZ10`)GexP;vf;dNKjkW&zexY7c}_MPd88}+ zLgFhu?{n1FniQr;oMqSTl zBb_ck<*!7FO;UpN`}uEe*%VidUumPQPkrBYGG@z1e`y!T>2kqZu%#sbRX2luRbtV^ z&Bku|%_b3S`|_PEG~`MNewF`B!?X?$vJ57|aTS+nA z6Re2Sxe|QZMV0`ezM#&jpa4^MDeEIbf=eP_^&_VqZ_1YW((|F+ z-*u>YEpXydq3XD8(QNiup*@n#`3LFW6;XGgvIP>3VQtWpG&Jj>n6)Hl%pQcT$r3`} zgP7b%R52BXU_?Qlrg-yr=etS{RhS@AI$bgFE$!awW6}j89NIAH5mBqBW!Z4yn*5w0 zE%in=KY5eWl{gABTHHaam=+BP3q~tS=*i2-4$1Bb>A_GuAbt5;*YXDW0p1mO%0nE8 z9dVSXG8A=S49-Fhs;4-@neHHT)|^&{&0PY`eSSHWr#j8w!2?j?dH3tuPec8c&&L)& z0Mv@XH!y2t{6o@J>_yLc$@~sHs))Pjot8}pBJLLa>}q@1t6Xo2dfxQiJ?}rB3ihB+ zF|{N~u_(aIHskpcV;JU|uG3`RV7B7o=78tiQ}x_xqKBIIkqC))%{*bxeXdr# zhj~^M5F#7-v6&?=o)6^a8PBUKD${{gWm~Q(Az#rkw+g6Wr%68wR@-pkiA82v3e@?I zmDy>HU?^2iBq*7yYtoXZj5dwUTdhu3s=>K<3W}s zcQd!VO!`P&9L^l_a*C|_G-YIy+1E#=al3?^ zttEaF`KdE9|AmwSV*SxK=Xu|p=g7u?)Jz2>WX7o|}u1fkD4YGt>oMi3I@6OyO3MH!UF4nUziJ`e^?iA)xIv>XxdkN=BjL00c zcW8HIIVtz%4Ob%124y+xD<(;DgRwUV8$!&P9Hnye^|Go9z*`S>ea7t$6H)6y-zEm> z49nJwLSo#U^N2Nk=O%1WakAcE>=B8kdABvune}Nd6+qYn1{hBLhp{@Na=SQEx%!J8 z1Psnl#~;{Ee3hU z6$Pb{%EBJq{SrNb8s@fXwlAs^xf4?`@pUj)dgq(=g)K+y-y&wP?J&8%5~8B9n78vm zsgS9bYdW%{{{-bD+~sfd|53kmxbYs36_Fner(D3$gwHDz*h#r2Q=|{mkv|~~?q)yi zfHzHuG5ZzT<@*noHW#21*lA;AT4%=cCQ;onIi@alzeA&Gz+p`qRV4~Yj1NjWos(BF zbxY=f*$t}_{#6$~AF^2XZgw_Nw>Omfnr+Y5{c!OfbM9DU@L7#*HcmVvC=5i{wlvjaTa3xG5kw}5=YKO z?c#NpqG{0)7)~OI-MG+Vz2>nW=i;i0nFk{yk&0=B4Rz;m(jPIX|p+2Pr5AkZ*#a>7NU{TV2 zn6fX8;m^GAEsxsn7sqpnNgdXQXs3igA+nX{B#OpZ19?>AqrVm>G=<>mdR?i8t$6)? z{Z{}9t=5>XHvAwj)~Yxs!fB@S=XsdznxW%mz^uoX3Ce=35cmTygpY9kdJ^9sog&w4 z(+QI=@sSwzlrHohx;IDgW#eEf%+SE^Q@InzyWyz(d{r#Lj{%PfAGOIE*JeqIDSuBP z&voP~Dz+%dp`!k08pi4u=Lz3pv++A1QRKV zbWRDz^tcQTJW+SVQlBksO%3QTndor0(${Mh@9@#z5lnws0VLWx^s?` zqU6qVWd4$0{QiUL!Q5Gb49zYQ!3&*9uLX8)s0_+UI$KbkE9huQiC?*qMoI*6!Ik!+ z@Z7EK{qOeb!M2%CQ~PVDhYOLu(&rf$82Kq;myVAd90YdmJx8^#CZ6MW07b?hlb?Q- zf*lRcyQdz9YfLzE_!nmMh{MVq&V2!*`GJ_>*Vr6XQomzn58Mu*NjK>*%cj~hCak8F zeM04nCD>(yH;Grzl*A+LUWDwr)adgRAdQuvUQASxF-_oQ${Os^8WvUhqRLd46R{DmJ^eN8d0j26|N8BN(K7CnX|1ih)&CrCH2}H9FC((v19uRkftZz8 zr)(A{S4~iwpHX>V+%*xCSx=XyOWas{5fnG0)``i&%X}6_hL#?MOgI`@62qy>;wmf( zTPna6G2zJ=*FW%;<4g5Sv|UgIKPMNv)sIe_$759`m-iM}$DCb|F^n>|W0|c^xkvTggpD~P$bfqh$*jEw&0N*B0R#AF`)Ov=Rw5u zDxH6Aa?6lbZfmqxVuDm%QvXUA6p$DqygE%VTNNEij|clv~bq z{$2+7o6VgnjY_Yxh3CT z!h>4he3|+*y8|oI!gj|hnJTiA_`umJU`)a)G&5!$!asNxkRB=4P@T81fS_^g8~Skd zvN0IrNfgEeMCPm!$Z*_*IF$a!w?mxuh95gF0^B+6&9N5b>|8i=Z5ws#t~0uH+3lG6bsxD^QIhsBWQtXeX}gFlFhj4Yf0qGj3*|7{58u2q zS;giJU2|~AC!y>TpOJ5e8Bxd)gsC-#N)pXsAbDVvKr=DdP--LbxS4v@9<7#YWgD+9 z4d?6KAUAr*fOh4&rS}c1_k>J#}wJsz%6vIdc@-oom zNfyjW=>8@JJS19YOs?JF%jic)f-cwb9eM&BOnNC^>>|u z``nMC<`jw+c?+p?YGr~Yl2>E=?q?O>m^*e0bM=!$+VyNv7A9L zu9SOA>t;RDzO=U*+|A(Lh2dFgeLF_)AGMZ=_s=gel#Sh62y2otz|F|lrJ3=LqVMw@ zt5*x!1ZATQZ*1i-zuIkL?T6NIKOGpdI?~f;kYj8RGl)5j&^ApY78*$Hcv%aoD^_{s zuwDnhT-K+t1t^QWj+h|QgK1pZG_!w0s8u@w zvbCV`U$JnbSJ(SLO`>W+naLujLE+Z-L6d~}qIl6C@)I}eoNu$yVnfI_RQZxs%LDk8 z)+>3lSaT)|&IC4X-?d3Hk%NqDhcbT3j$+D!9_e;1IqakBZ98XLbQDyg-)BT~YSNLM zEJsD!^U?IpLXB6L|x^7CeMz3-#jLsP*wwodc@ z_PeJgKZ7W0uzV4Rt5n&UzS8X_klaDrA|W{qp)FEm>Na*~ znGx+>yB$XL6wb^lZMv)^AD1g{;fy}+!ls*S6y4Xyn-Mjpm*>$Xg66-j_$be(1Fyw; zNeVmD=-V_fUSHx`><0&MLmBsTbO#7WYf6i55k`~Hlh|{#247f|@O`8U;y^wWwpoKu zrk#*%NK`hvUEqRvA#X0BLHoNrOuyT^Kd<+|xIiguZvSj&XV3vz`HD8-+T);G3(^wI!s-i#O|=hi)*fu{qoMbmYaa#c5QrZa&P0 z^7E$5@E=d{ z&Lruz46SyP4-NETY7JkupF}~}{`Gebsc`@On1BLEB$ChgGaEJeH(Lz0z4k(dsD|V0 zOi>9nXE;csTCddipk&~*pN#J$1A4vWx`zO9I6aU5GVa2mUvTbFx}Yo7$+$?Me|a3_{~k1vxpV#(FyUsOH>5q|?EGh(!nWpixazd< zft4e~uO-6x;R^b{Pfl%cQVRa=R!TGb8v$0(V@kio>pX`_`N8E$Zz@@O4Dkx}%(fH2 z7tytZ2<;Ovm%$m0f0zR$naf@zh{O7!e!+(2+#J+N+@tVcEyuQxVqOsSQ?ww02p1m< z=Gpt5w>F;e#*y8NX$3UKSwI(mbK9`&d&7W_IGia0M3|lYBS`u}7AfD`@BEtzxdG$3fR_N5t%E9ma$`tuE%>8(UsgD_d z6$)|ajIAAi&zlUt;?G*SVf`>SyWKV-rX_H~JMk%W=c(*rh94WHgOzh+#!_7Y(dfMU z#0o-`C@|af=?CN&1jl1#e+aBB zR~3rhLzV7-90k0YGU=Q=Q4Vjng^Ovy{hbu@{^d0xMBeng$G4<}C#Yq4j# z!($Wc)d0M3m@7$70aa035x;*dEqDb%Quf_gZ#p~$#|jvAfaYJALyj0b`i((^-{7co zo($KR(g87jY~{R%jw#9N^oR@7oEd5`qJr5yoe@O zN=@`Ecp<3Eh68T;!t&(rY+_P4-`3JLm!KF`^FK*3!MzK68%uwlWg-zd_ET>4^g=YY%FZ`ke_QoJ8D6XDHDH!ue!# zO82~6N6*ZyX@q#F{HFy8%VXM-hJuPBeo~ zKgZ4+&9Qu768o=xBA6~&K3&n^;HMe`Uhrcz1B=7stUzU#B0NFoBEXCVnhmO{qc}* znSbzY*xS%$lXA%>kb}|k`?tl8obA$;G zR>(ju%-)q?zW{*I|0`C|+|6j#i=B-3Go~OBzn}u9=zHgUe*ZQ9zi}4CVH{TyEqcaY z9x!zQ&wlc6h0ztcgDB0~a_8P*SO4mM6ZyEqn*P7B{kg5= zXVae37t731dZeT;!E@sRFW%G;czSi|2g!LGxJx*Qng@|=FfRQ!x=Xzg2BZ!4{G7vr zjR=zo`_^Le9NO_+p?HjH!T(9dgjqBeLrF7J{LfT_@GNNy}Ft)M>B(Y!x|C2a~<(~>|+OpoFjY4;N6*X4R(*Yl593q%CfrGsj zdYUAr+VOW)bk1{fcw8(SxOrC_5k{#06KAf@B=p`xf`+7XsqBB#@1hpp*1Cm-cZrlb zlBXRk@Q}RK60S)BrXLr-NrR!;v+sZU6m3!KQ$eIa58xA!hSpXPV+Ev`@o@bE=wS~j z!5Mgn-$wX2%N1WW{Kw?Lb|-@`K$nvr%R;q`q~lS@oGM&XapeezM z5YHdPk6}oKTL1sj3fXBbSIIN~KxY}I67yPc5*!r)QY$MO!-S9zMzIasaLrSpjM>C2 zCK;gffH$XrBV5RU!XCJOl4Obqaf?i4Bd+yY@FUwen}oxIx4~T1ryUY6?0<^MN;Ii^ zU=dCEmFHtG`z6GwFe#FlClh{gMC7I$Cv4a4-+z|EVFAegP(?T*uj%MZOd4jKD0>9@ z{GT1l%FYJ8t(y=GCMYUASAhN3xZD5p30aZzz#v4|WYmoL2QTUA+cI$6{hx~V{|OtE z!zV-bvU-k!1FVMl2oHE;y$%>?3A)^-QzDsE2bP?1k~GiSQw_m}sjP+(m6ShZ1ok^) zvXy-U-?RnScrRtWf@i)suP%W|G7lSjcQgARlq^rav(0aFsWSM6LK-d;-2VkLCmpK@ zKRGanV8H9xs(q-u7I;eoO=>(EC_7XT56@1-(=ae=Gs|$o zdG}Fc#|QlW$Kcdp#NicZ;~GQZu)cs7_IX#bLr`x2HU_xCIBY&|p$4J~9Y(%Wz7#CT zAf`9?8_qs4FFOyu0eH1x<-nz;cD>wlpWGJA{C__x8)xyBE0eTs%BZ=f`Q%pw3XuLE z#*8Z=-i&F6yo_5-Cr#cIlqgjx5ifN*ap7;lZ{mXC5rSYb3a7dnHH%Z60I?PfN}S<{ zl?abz<(3OkU4X2*%Y|GY serverCtrl.serverUp(server, done)); + + after( done => serverCtrl.serverDown(server, done)); + + afterEach( done => cleanDB(done)); + + describe('testing POST /api/artist/:artistID/gallery', () => { + + describe('with a valid body', () => { + + before(done => mockArtist.call(this, done)); + + it('should return a gallery profile and a status 200', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send(exampleGallery) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempArtist.username); + expect(res.body.name).to.equal(exampleGallery.name); + expect(res.body.desc).to.equal(exampleGallery.desc); + expect(res.body.category).to.equal(exampleGallery.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with a bad user token', () => { + + before(done => mockArtist.call(this, done)); + + it('should return a 401 error unauthorized user', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send(exampleGallery) + .set({ + Authorization: `Bearer ${this.tempToken}bad`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with bad/invalid artist id', () => { + + before(done => mockArtist.call(this, done)); + + it('should return status 404 for invalid id', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}bad/gallery`) + .send({ + name: exampleGallery.name, + username: this.tempArtist.username, + desc: exampleGallery.desc, + category: exampleGallery.category, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with no name', () => { + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send({ + username: this.tempArtist.username, + desc: exampleGallery.desc, + category: exampleGallery.category, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no desc', () => { + + before(done => mockArtist.call(this, done)); + + it('should status 400 bad request', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send({ + name: exampleGallery.name, + username: this.tempArtist.username, + category: exampleGallery.category, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no category', () => { + + before(done => mockArtist.call(this, done)); + + it('should return an gallery profile and a status 400', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send({ + name: exampleGallery.name, + desc: exampleGallery.desc, + username: this.tempArtist.username, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with invalid date--string', () => { + + before(done => mockArtist.call(this, done)); + + it('should return an gallery profile and a status 400', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send({ + name: exampleGallery.name, + desc: exampleGallery.desc, + username: exampleGallery.username, + category: exampleGallery.category, + created: 'striiiing', + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with an invalid body', () => { + + before(done => mockArtist.call(this, done)); + + it('should a status 400 bad request', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send('badbody') + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with a bad authorization header', () => { + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send(exampleGallery) + .set({ + Authorization: 'bad request', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no authorization header', () => { + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send(exampleGallery) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with bearer header with no token', () => { + + before(done => mockArtist.call(this, done)); + + it('should return status 400 bad request', (done) => { + + request.post(`${url}/api/artist/${this.tempArtist._id}/gallery`) + .send(exampleGallery) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('testing GET to /api/gallery/:galleryID', () => { + + describe('with valid token and id', () =>{ + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(this.tempGallery.name); + expect(res.body.desc).to.equal(this.tempGallery.desc); + expect(res.body.category).to.equal(this.tempGallery.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('testing populate gallery listings', () => { + + before(done => mockMultipleListings.call(this, 10, done)); + + it('should return a gallery with populated listing array', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.listings.length).to.equal(10); + done(); + }); + }); + }); + + describe('testing populate gallery listings with valid id and bad token request', function(){ + + before(done => mockMultipleListings.call(this, 10, done)); + + it('should return a 400 error bad request', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('testing populate gallery listings with invalid id and valid token', function(){ + + before(done => mockMultipleListings.call(this, 10, done)); + + it('should return a 404 error for invalid id', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with valid token and invalid id', () => { + + before(done => mockGallery.call(this, done)); + + it('should return status 404 not found', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with bad token request and valid id', () => { + + before(done => mockGallery.call(this, done)); + + it('should return status 400 bad request', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('testing PUT to /api/gallery/:galleryID', () => { + + describe('updating name property with valid token and id, ', () => { + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + let updateData = {name: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(updateData.name); + expect(res.body.desc).to.equal(this.tempGallery.desc); + expect(res.body.category).to.equal(this.tempGallery.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating desc property with valid token and id, ', () => { + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + let updateData = {desc: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(this.tempGallery.name); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(this.tempGallery.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating category property with valid token and id, ', () => { + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + let updateData = {category: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(this.tempGallery.name); + expect(res.body.desc).to.equal(this.tempGallery.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating name and category properties with valid token and id, ', () => { + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + let updateData = {name: 'bob', category: 'bob2'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(updateData.name); + expect(res.body.desc).to.equal(this.tempGallery.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating name and desc properties with valid token and id, ', () => { + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + let updateData = {name: 'bob', desc: 'bob2'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(updateData.name); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(this.tempGallery.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating desc and category properties with valid token and id, ', () => { + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + let updateData = {desc: 'bob', category: 'bob2'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(this.tempGallery.name); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating name, desc and category properties with valid token and id, ', () => { + + before(done => mockGallery.call(this, done)); + + it('should return a gallery', done => { + let updateData = {name: 'bob', desc: 'bob2', category: 'bob3'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.name).to.equal(updateData.name); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and invalid id', () => { + + before(done => mockGallery.call(this, done)); + + it('should status 404 not found', done => { + let updateData = {name: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}bad`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with bad token request and valid id', () => { + + before(done => mockGallery.call(this, done)); + + it('should return status 400 bad request', done => { + let updateData = {name: 'bob'}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong user', () => { + + before(done => mockGallery.call(this, done)); + before(done => mockUser.call(this, done)); + + it('should status 401 unauthorized', done => { + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + + describe('updated with empty name', () => { + + before(done => mockGallery.call(this, done)); + + it('should status 400 bad request', done => { + let updateData = {name: ''}; + request.put(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('testing DELETE to /api/artist/:artistID/gallery/:galleryID', () => { + + describe('with valid token and id', () => { + + before(done => mockGallery.call(this, done)); + it('should delete a gallery', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with valid token and invalid id', () => { + + before(done => mockGallery.call(this, done)); + + it('should status 404 not found', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with bad token request and valid id', () => { + + before(done => mockGallery.call(this, done)); + + it('should status 400 bad request', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong user', () => { + + before(done => mockGallery.call(this, done)); + before(done => mockUser.call(this, done)); + + it('should status 401 unauthorized', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with valid token and id', () => { + + before( done => mockManyPhotos.call(this, 5, done)); + + it('should delete a gallery and all associated listings and photos', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + }); +}); diff --git a/lab-claudia/test/lib/artist-mock.js b/lab-claudia/test/lib/artist-mock.js new file mode 100644 index 0000000..136225b --- /dev/null +++ b/lab-claudia/test/lib/artist-mock.js @@ -0,0 +1,29 @@ +'use strict'; + +const debug = require('debug')('artc:artist-mock'); +const userMock = require('./user-mock.js'); +const Artist = require('../../model/artist.js'); + +module.exports = function(done){ + debug('create artist mock'); + let exampleArtist = { + firstname: 'george', + lastname: 'bush', + city: 'houston', + zip: '85749', + about: 'former president of the united states of america, avid painter', + phone: '7328851234', + }; + userMock.call(this, err => { + if(err) + return done(err); + exampleArtist.userID = this.tempUser._id.toString(); + exampleArtist.username = this.tempUser.username; + exampleArtist.email = this.tempUser.email; + new Artist(exampleArtist).save() + .then( artist => { + this.tempArtist = artist; + done(); + }); + }); +}; diff --git a/lab-claudia/test/lib/aws-mock.js b/lab-claudia/test/lib/aws-mock.js new file mode 100644 index 0000000..1471ec7 --- /dev/null +++ b/lab-claudia/test/lib/aws-mock.js @@ -0,0 +1,33 @@ +'use strict'; + +const AWSMock = require('aws-sdk-mock'); + +module.exports = exports = {}; + +exports.uploadMock = { + ETag: '"ddad2cbff1bd8023ed7cd42e5059600a"', + Location: 'https://artc-staging-assets.s3.amazonaws.com/03a7990e1adf9fa8944df58dfc81cc5b.jpg', + key: '03a7990e1adf9fa8944df58dfc81cc5b.jpg', + Key: '03a7990e1adf9fa8944df58dfc81cc5b.jpg', + Bucket: 'artc-staging-assets', +}; + +AWSMock.mock('S3', 'upload', function(params, callback){ + if(params.ACL !== 'public-read') + return callback(new Error('ACL must be public read')); + if(params.Bucket !== 'artc-staging-assets') + return callback(new Error('Bucket must be artc-staging-assets')); + if(!params.Key) + return callback(new Error('requres Key')); + if(!params.Body) + return callback(new Error('requires body')); + callback(null, exports.uploadMock); +}); + +AWSMock.mock('S3', 'deleteObject', function(params, callback){ + if(params.Bucket !== 'artc-staging-assets') + return callback(new Error('Bucket must be artc-staging-assets')); + if(!params.Key) + return callback(new Error('requires Key')); + callback(null , {hello: 'sup'}); +}); diff --git a/lab-claudia/test/lib/clean-db.js b/lab-claudia/test/lib/clean-db.js new file mode 100644 index 0000000..6b1f7c5 --- /dev/null +++ b/lab-claudia/test/lib/clean-db.js @@ -0,0 +1,23 @@ +'use strict'; + +const debug = require('debug')('artc:clean-db'); + +const User = require('../../model/user.js'); +const Photo = require('../../model/photo.js'); +const Artist = require('../../model/artist.js'); +const Gallery = require('../../model/gallery.js'); +const Listing = require('../../model/listing.js'); + + +module.exports = function(done){ + debug('clean up database'); + Promise.all([ + Photo.remove({}), + User.remove({}), + Artist.remove({}), + Gallery.remove({}), + Listing.remove({}), + ]) + .then( () => done()) + .catch(done); +}; diff --git a/lab-claudia/test/lib/gallery-mock.js b/lab-claudia/test/lib/gallery-mock.js new file mode 100644 index 0000000..acc64a4 --- /dev/null +++ b/lab-claudia/test/lib/gallery-mock.js @@ -0,0 +1,26 @@ +'use strict'; + +const debug = require('debug')('artc:gallery-mock'); +const artistMock = require('./artist-mock.js'); +const Gallery = require('../../model/gallery.js'); + +module.exports = function(done){ + debug('create mock gallery'); + let exampleGallery = { + name: 'george bush, finest collection', + desc: 'the best portraits george has ever produced', + category: 'portraits', + }; + artistMock.call(this, err => { + if (err) + return done(err); + exampleGallery.username = this.tempUser.username; + exampleGallery.userID = this.tempUser._id.toString(); + exampleGallery.artistID = this.tempArtist._id.toString(); + new Gallery(exampleGallery).save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }); + }); +}; diff --git a/lab-claudia/test/lib/listing-mock.js b/lab-claudia/test/lib/listing-mock.js new file mode 100644 index 0000000..c344093 --- /dev/null +++ b/lab-claudia/test/lib/listing-mock.js @@ -0,0 +1,28 @@ +'use strict'; + +const debug = require('debug')('artc:listing-mock'); +const galleryMock = require('./gallery-mock.js'); +const Listing = require('../../model/listing.js'); + +module.exports = function(done){ + debug('create mock listing'); + let exampleListing = { + title: 'a cat', + desc: 'george does cat portraits', + category: 'portraits', + }; + + galleryMock.call(this, err => { + if (err) + return done(err); + exampleListing.username = this.tempUser.username; + exampleListing.userID = this.tempUser._id.toString(); + exampleListing.artistID = this.tempArtist._id.toString(); + exampleListing.galleryID = this.tempGallery._id.toString(); + new Listing(exampleListing).save() + .then( listing => { + this.tempListing = listing; + done(); + }); + }); +}; diff --git a/lab-claudia/test/lib/mock-many-artists.js b/lab-claudia/test/lib/mock-many-artists.js new file mode 100644 index 0000000..7688b7c --- /dev/null +++ b/lab-claudia/test/lib/mock-many-artists.js @@ -0,0 +1,48 @@ +'use strict'; + +const debug = require('debug')('artc:artist-mock'); +const userMock = require('./user-mock.js'); +const Artist = require('../../model/artist.js'); +const lorem = require('lorem-ipsum'); + +module.exports = function(count, done){ + userMock.call(this, err => { + debug('mock multiple artists'); + if(err) return done(err); + let artistMocks = []; + let userID = this.tempUser._id.toString(); + let email = this.tempUser.email; + let username = this.tempUser.username; + for(var i=0; i { + artists.forEach( artist => { + let artistID = artist._id.toString(); + this.tempUser.artists.push(artistID); + }); + this.tempArtists = artists; + return this.tempArtist.save(); + }) + .then(() => done()) + .catch(done); + }); +}; + +function mockArtist(userID, username, email) { + let firstname = lorem({count:2, units: 'word'}); + let lastname = lorem({count:2, units: 'word'}); + let city = lorem({count:2, units: 'word'}); + let zip = lorem({count:2, units: 'word'}); + let exampleArtist = { + firstname, + lastname, + city, + zip, + userID, + username, + email, + }; + return new Artist(exampleArtist).save(); +} diff --git a/lab-claudia/test/lib/mock-many-photos.js b/lab-claudia/test/lib/mock-many-photos.js new file mode 100644 index 0000000..62aa732 --- /dev/null +++ b/lab-claudia/test/lib/mock-many-photos.js @@ -0,0 +1,48 @@ +'use strict'; + +const debug = require('debug')('artc:mock-many-photos'); +const Photo = require('../../model/photo.js'); +const listingMock = require('./listing-mock.js'); +const lorem = require('lorem-ipsum'); + +module.exports = function(count, done){ + debug('mock multiple photos'); + listingMock.call(this, err => { + if (err) return done(err); + let photoMocks = []; + let userID = this.tempUser._id.toString(); + let artistID = this.tempArtist._id.toString(); + let galleryID = this.tempGallery._id.toString(); + let listingID = this.tempListing._id.toString(); + let username = this.tempUser.username; + for(var i=0; i { + this.tempPhotos = photos; + done(); + }) + .catch(done); + }); +}; + +function mockPic(userID, artistID, galleryID, listingID, username){ + let name = lorem({count:2, units: 'word'}); + let alt = lorem({count:2, units: 'word'}); + let objectKey = lorem({count:4, units: 'word'}).split(' ').join(''); + let uri = lorem({count:4, units: 'word'}).split('').join('-'); + let imageURI = `https://${uri}/${objectKey}`; + let examplePhoto = { + name, + alt, + objectKey, + imageURI, + username, + userID, + artistID, + galleryID, + listingID, + }; + return new Photo(examplePhoto).save(); +} diff --git a/lab-claudia/test/lib/photo-mock.js b/lab-claudia/test/lib/photo-mock.js new file mode 100644 index 0000000..5410cb8 --- /dev/null +++ b/lab-claudia/test/lib/photo-mock.js @@ -0,0 +1,33 @@ +'use strict'; + +const debug = require('debug')('artc:photo-mock'); + +const awsMocks = require('./aws-mock.js'); +const listingMock = require('./listing-mock.js'); + +const Photo = require('../../model/photo.js'); + +module.exports = function(done){ + debug('creating mock photo'); + + let examplePhotoData = { + name: 'example name', + alt: 'useful photo', + imageURI: awsMocks.uploadMock.Location, + objectKey: awsMocks.uploadMock.Key, + }; + + listingMock.call(this, err => { + if (err) return done(err); + examplePhotoData.username = this.tempUser.username; + examplePhotoData.userID = this.tempUser._id.toString(); + examplePhotoData.artistID = this.tempArtist._id.toString(); + examplePhotoData.galleryID = this.tempGallery._id.toString(); + new Photo(examplePhotoData).save() + .then( photo => { + this.tempPhoto = photo; + done(); + }) + .catch(done); + }); +}; diff --git a/lab-claudia/test/lib/populate-artist-galleries-mock.js b/lab-claudia/test/lib/populate-artist-galleries-mock.js new file mode 100644 index 0000000..7cad249 --- /dev/null +++ b/lab-claudia/test/lib/populate-artist-galleries-mock.js @@ -0,0 +1,39 @@ +'use strict'; + +const debug = require('debug')('artc-populate-artist-mocks'); +const artistMock = require('./artist-mock.js'); +const Gallery = require('../../model/gallery.js'); +const lorem = require('lorem-ipsum'); + +module.exports = function(count, done){ + debug('mocking multiple galleries'); + artistMock.call(this, err => { + if (err) return done(err); + let galleriesMock = []; + let userID = this.tempUser._id.toString(); + let artistID = this.tempArtist._id.toString(); + let username = this.tempUser.username; + for(var i=0; i { + galleries.forEach(gallery => { + let galleryID = gallery._id.toString(); + this.tempArtist.galleries.push(galleryID); + }); + this.tempGalleries = galleries; + return this.tempArtist.save(); + }) + .then(() => done()) + .catch(done); + }); +}; + +function mockGallery(userID, artistID, username){ + let name = lorem({count: 2, units: 'word'}); + let desc = lorem({count: 2, units: 'sentence'}); + let category = lorem({count: 2, units: 'word'}); + let exampleGallery = { name, desc, category, userID, artistID, username }; + return new Gallery(exampleGallery).save(); +} diff --git a/lab-claudia/test/lib/populate-gallery-listings-mock.js b/lab-claudia/test/lib/populate-gallery-listings-mock.js new file mode 100644 index 0000000..27201a2 --- /dev/null +++ b/lab-claudia/test/lib/populate-gallery-listings-mock.js @@ -0,0 +1,40 @@ +'use strict'; + +const debug = require('debug')('artc:populate-gallery-listings'); +const galleryMock = require('./gallery-mock.js'); +const Listing = require('../../model/listing.js'); +const lorem = require('lorem-ipsum'); + +module.exports = function(count, done){ + debug('creating multiple listings'); + galleryMock.call(this, err => { + if (err) return done(err); + let listingsMock = []; + let userID = this.tempUser._id.toString(); + let artistID = this.tempArtist._id.toString(); + let galleryID = this.tempGallery._id.toString(); + let username = this.tempUser.username; + for(var i=0; i { + listings.forEach( listing => { + let listingID = listing._id.toString(); + this.tempGallery.listings.push(listingID); + }); + this.tempListings = listings; + return this.tempGallery.save(); + }) + .then(() => done()) + .catch(done); + }); +}; + +function mockListing(userID, artistID, galleryID, username){ + let title = lorem({count: 2, units: 'word'}); + let desc = lorem({count: 2, units: 'sentence'}); + let category = lorem({count: 1, units: 'word'}); + let exampleListing = { title, desc, category, userID, artistID, galleryID, username}; + return new Listing(exampleListing).save(); +} diff --git a/lab-claudia/test/lib/server-control.js b/lab-claudia/test/lib/server-control.js new file mode 100644 index 0000000..0295923 --- /dev/null +++ b/lab-claudia/test/lib/server-control.js @@ -0,0 +1,30 @@ +'use strict'; + +const debug = require('debug')('artc:server-control'); + +module.exports = exports = {}; + +exports.serverUp = function(server, done) { + if (!server.isRunning){ + server.listen(process.env.PORT, () => { + server.isRunning = true; + debug('server up'); + done(); + }); + return; + } + done(); +}; + +exports.serverDown = function(server, done) { + if (server.isRunning) { + server.close(err => { + if (err) return done(err); + server.isRunning = false; + debug('server down'); + done(); + }); + return; + } + done(); +}; diff --git a/lab-claudia/test/lib/test-env.js b/lab-claudia/test/lib/test-env.js new file mode 100644 index 0000000..917f9f3 --- /dev/null +++ b/lab-claudia/test/lib/test-env.js @@ -0,0 +1,8 @@ +'use strict'; + +process.env.MONGODB_URI = 'mongodb://localhost/artctesting'; +process.env.PORT = 3000; +process.env.NODE_ENV = 'testing'; +process.env.APP_SECRET = 'super cool fake secret for tests'; +process.env.AWS_ACCESS_KEY_ID = 'FAKEKEYFORAWS'; +process.env.AWS_SECRET_ACCESS_KEY = 'FAKEACESSKEY'; diff --git a/lab-claudia/test/lib/user-mock.js b/lab-claudia/test/lib/user-mock.js new file mode 100644 index 0000000..b53d45c --- /dev/null +++ b/lab-claudia/test/lib/user-mock.js @@ -0,0 +1,30 @@ +'use strict'; + +const debug = require('debug')('artc:user-mock'); +const User = require('../../model/user.js'); +const lorem = require('lorem-ipsum'); + +module.exports = function(done) { + debug('create mock user'); + let username = lorem({count: 2, units: 'word'}).split(' ').join('-'); + let password = lorem({count: 2, units: 'word'}).split(' ').join('-'); + let email = lorem({count: 2, units: 'word'}).split(' ').join('-'); + let exampleUser = { + username, + password, + email: `${email}@art.fancyartist`, + }; + this.tempPassword = password; + new User(exampleUser) + .generatePasswordHash(exampleUser.password) + .then( user => user.save()) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); +}; diff --git a/lab-claudia/test/listing-router-test.js b/lab-claudia/test/listing-router-test.js new file mode 100644 index 0000000..3f199c0 --- /dev/null +++ b/lab-claudia/test/listing-router-test.js @@ -0,0 +1,651 @@ +'use strict'; + +require('./lib/test-env.js'); +require('./lib/aws-mock.js'); + +// npm modules +const expect = require('chai').expect; +const request = require('superagent'); +const Promise = require('bluebird'); +const mongoose = require('mongoose'); + +// app modules +const serverCtrl = require('./lib/server-control.js'); +const cleanDB = require('./lib/clean-db.js'); +const mockGallery = require('./lib/gallery-mock.js'); +const mockListing = require('./lib/listing-mock.js'); +const mockUser = require('./lib/user-mock.js'); +const mockManyPhotos = require('./lib/mock-many-photos.js'); + +mongoose.Promise = Promise; + +// module constants +const server = require('../server.js'); +const url = `http://localhost:${process.env.PORT}`; + +const exampleListing = { + title: 'a cat', + desc: 'george does cat portraits', + category: 'portraits', +}; + +describe('testing listing-router', function(){ + + before(done => serverCtrl.serverUp(server, done)); + after(done => serverCtrl.serverDown(server, done)); + afterEach(done => cleanDB(done)); + + describe('testing POST /api/signup', () => { + + describe('with a valid body', () =>{ + + before(done => mockGallery.call(this, done)); + + it('should return a listing and status 200', done => { + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send(exampleListing) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.title).to.equal(exampleListing.title); + expect(res.body.desc).to.equal(exampleListing.desc); + expect(res.body.category).to.equal(exampleListing.category); + expect(res.body.username).to.equal(this.tempUser.username); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with no title', () => { + + before(done => mockGallery.call(this, done)); + + it('should status 400 bad request', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send({ + username: this.tempGallery.username, + desc: exampleListing.desc, + category: exampleListing.category, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no desc', () => { + + before(done => mockGallery.call(this, done)); + + it('should status 400 bad request', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send({ + title: exampleListing.name, + username: this.tempGallery.username, + category: exampleListing.category, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no category', () => { + + before(done => mockGallery.call(this, done)); + + it('should return an gallery profile and a status 400', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send({ + title: exampleListing.name, + desc: exampleListing.desc, + username: this.tempGallery.username, + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with invalid date--string', () => { + + before(done => mockGallery.call(this, done)); + + it('should return an gallery profile and a status 400', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send({ + title: exampleListing.name, + desc: exampleListing.desc, + username: exampleListing.username, + category: exampleListing.category, + created: 'striiiing', + }) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with an invalid body', () => { + + before(done => mockGallery.call(this, done)); + + it('should a status 400 bad request', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send('badbody') + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with a bad authorization header', () => { + + before(done => mockGallery.call(this, done)); + + it('should return status 400 bad request', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send(exampleListing) + .set({ + Authorization: 'bad request', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with no authorization header', () => { + + before(done => mockGallery.call(this, done)); + + it('should status 400 bad request', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send(exampleListing) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with bearer header with no token', () => { + + before(done => mockGallery.call(this, done)); + + it('should return status 400 bad request', (done) => { + + request.post(`${url}/api/gallery/${this.tempGallery._id}/listing`) + .send(exampleListing) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + }); + + describe('testing GET to /api/listing/:listingID', () => { + + describe('with valid token and id', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + request.get(`${url}/api/listing/${this.tempListing._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempListing.username); + expect(res.body.title).to.equal(this.tempListing.title); + expect(res.body.desc).to.equal(this.tempListing.desc); + expect(res.body.category).to.equal(this.tempListing.category); + expect(res.body.userID).to.equal(this.tempListing.userID.toString()); + expect(res.body.artistID).to.equal(this.tempListing.artistID.toString()); + expect(res.body.galleryID).to.equal(this.tempListing.galleryID.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + describe('with valid token and invalid id', () => { + + before(done => mockListing.call(this, done)); + + it('should status 404 not found', done => { + request.get(`${url}/api/listing/${this.tempListing._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with bearer header with no token', () => { + + before(done => mockListing.call(this, done)); + + it('should return status 400 bad request', done => { + request.get(`${url}/api/listing/${this.tempListing._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('testing PUT to /api/listing/:listingID', () => { + + describe('updating title property with valid token and id, ', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + let updateData = {title: 'bob'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempListing.username); + expect(res.body.title).to.equal(updateData.title); + expect(res.body.desc).to.equal(this.tempListing.desc); + expect(res.body.category).to.equal(this.tempListing.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating desc property with valid token and id, ', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + let updateData = {desc: 'bob'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempListing.username); + expect(res.body.title).to.equal(this.tempListing.title); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(this.tempListing.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating category property with valid token and id, ', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + let updateData = {category: 'bob'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempListing.username); + expect(res.body.title).to.equal(this.tempListing.title); + expect(res.body.desc).to.equal(this.tempListing.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating title and category properties with valid token and id, ', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + let updateData = {title: 'bob', category: 'bob2'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempListing.username); + expect(res.body.title).to.equal(updateData.title); + expect(res.body.desc).to.equal(this.tempListing.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating title and desc properties with valid token and id, ', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + let updateData = {title: 'bob', desc: 'bob2'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempListing.username); + expect(res.body.title).to.equal(updateData.title); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(this.tempListing.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating desc and category properties with valid token and id, ', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + let updateData = {desc: 'bob', category: 'bob2'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempListing.username); + expect(res.body.title).to.equal(this.tempListing.title); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('updating title, desc and category properties with valid token and id, ', () => { + + before(done => mockListing.call(this, done)); + + it('should return a listing', done => { + let updateData = {title: 'bob', desc: 'bob2', category: 'bob3'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(200); + expect(res.body.username).to.equal(this.tempGallery.username); + expect(res.body.title).to.equal(updateData.title); + expect(res.body.desc).to.equal(updateData.desc); + expect(res.body.category).to.equal(updateData.category); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.body.artistID).to.equal(this.tempArtist._id.toString()); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + let date = new Date(res.body.created).toString(); + expect(date).to.not.equal('Invalid Date'); + done(); + }); + }); + }); + + describe('with valid token and invalid id', () => { + + before(done => mockListing.call(this, done)); + + it('should status 404 not found', done => { + let updateData = {name: 'bob'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}bad`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with bearer header and no token', () => { + + before(done => mockListing.call(this, done)); + + it('should return status 400 bad request', done => { + let updateData = {title: 'bob'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong user', () => { + + before(done => mockListing.call(this, done)); + before(done => mockUser.call(this, done)); + + it('should status 401 unauthorized', done => { + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('updated with empty name', function(){ + + before(done => mockListing.call(this, done)); + + it('should status 400 bad request', done => { + let updateData = {title: ''}; + request.put(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .send(updateData) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('testing DELETE to /api/listing/:listingID', () => { + + describe('with valid token and id', () => { + + before(done => mockListing.call(this, done)); + + it('should delete a listing', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) + return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with valid token and invalid id', () => { + + before(done => mockListing.call(this, done)); + + it('should status 404 not found', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}bad`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with bearer header and no token', () => { + + before(done => mockListing.call(this, done)); + + it('should return status 400 bad request', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .set({ + Authorization: 'Bearer ', + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with wrong user', () => { + + before(done => mockListing.call(this, done)); + before(done => mockUser.call(this, done)); + + it('should return status 401 unauthorized', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with valid token and id', () => { + + before( done => mockManyPhotos.call(this, 5, done)); + + it('should delete a listing and all associated photos and references', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/listing/${this.tempListing._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}`, + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + }); +}); diff --git a/lab-claudia/test/page-router-test.js b/lab-claudia/test/page-router-test.js new file mode 100644 index 0000000..a7a1b9b --- /dev/null +++ b/lab-claudia/test/page-router-test.js @@ -0,0 +1,112 @@ +'use strict'; + +// testing for page queries on all relevant models + +require('./lib/test-env.js'); +require('./lib/aws-mock.js'); + +// npm modules +const expect = require('chai').expect; +const request = require('superagent'); +const mongoose = require('mongoose'); +const Promise = require('bluebird'); + +// app modules +const serverCtrl = require('./lib/server-control'); +const cleanDB = require('./lib/clean-db'); +//const mockUser = require('./lib/user-mock'); +//const mockArtist = require('./lib/artist-mock'); +const mockManyGalleries = require('./lib/populate-artist-galleries-mock.js'); +const mockManyListings = require('./lib/populate-gallery-listings-mock.js'); + +mongoose.Promise = Promise; + +// app constants +const server = require('../server.js'); +const url = `http://localhost:${process.env.PORT}`; + +// const exampleGallery = { +// name: 'Happy Stuff', +// desc: 'this is the best album', +// category: 'fun', +// }; + +//TESTS NEEDED + +describe('testing page-router', function(){ + //start/stop server and clean database + before(done => serverCtrl.serverUp(server, done)); + after(done => serverCtrl.serverDown(server, done)); + afterEach(done => cleanDB(done)); + + + describe('testing /api/listing', function() { + describe('with pagenation' , function() { + before(done => mockManyListings.call(this, 100, done)); + it('should return 50 listings', done => { + request.get(`${url}/api/listing`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.length).to.equal(50); + done(); + }); + }); + }); + + describe('with /api/listing?page=5' , function() { + before(done => mockManyListings.call(this, 100, done)); + it('should return 50 listings', done => { + request.get(`${url}/api/listing?page=5`) + .end((err, res) => { + if (err) return done(err); + for (let i=0; i< res.body.length; i++){ + expect(res.body.listings[i]._id.toString()).to.equal(this.tempListings[i + 10 ]._id.toString()); + expect(res.body.listings[i].artistID.toString()).to.equal(this.tempListings[i + 10 ].artistID.toString()); + expect(res.body.listings[i].galleryID.toString()).to.equal(this.tempListings[i + 10 ].galleryID.toString()); + expect(res.body.listings[i].category).to.equal(this.tempListings[i + 10].category); + expect(res.body.listings[i].name).to.equal(this.tempListings[i + 10].name); + expect(res.body.listings[i].desc).to.equal(this.tempListings[i + 10].desc); + } + expect(res.status).to.equal(200); + done(); + }); + }); + }); + }); //end testing listing pagenation + + describe('testing /api/gallery', function() { + describe('with pagenation' , function() { + before(done => mockManyGalleries.call(this, 100, done)); + it('should return 50 galleries', done => { + request.get(`${url}/api/gallery`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.length).to.equal(50); + done(); + }); + }); + }); + + describe('with /api/gallery?page=5' , function() { + before(done => mockManyGalleries.call(this, 100, done)); + it('should return 50 listings', done => { + request.get(`${url}/api/gallery?page=5`) + .end((err, res) => { + if (err) return done(err); + for (let i=0; i< res.body.length; i++){ + expect(res.body.galleries[i]._id.toString()).to.equal(this.tempGalleries[i + 10 ]._id.toString()); + expect(res.body.galleries[i].artistID.toString()).to.equal(this.tempGalleries[i + 10 ].artistID.toString()); + expect(res.body.galleries[i].category).to.equal(this.tempGalleries[i + 10].category); + expect(res.body.galleries[i].desc).to.equal(this.tempGalleries[i + 10].desc); + } + expect(res.status).to.equal(200); + done(); + }); + }); + }); + + + }); // end testing /api/gallery +}); // end first describe block diff --git a/lab-claudia/test/photo-router-test.js b/lab-claudia/test/photo-router-test.js new file mode 100644 index 0000000..9abe280 --- /dev/null +++ b/lab-claudia/test/photo-router-test.js @@ -0,0 +1,474 @@ +'use strict'; + +require('./lib/test-env.js'); +const awsMocks = require('./lib/aws-mock.js'); + +// NPM MODULES +const expect = require('chai').expect; +const request = require('superagent'); + +// APP MODULES +const artistMock = require('./lib/artist-mock.js'); +const photoMock = require('./lib/photo-mock.js'); +const galleryMock = require('./lib/gallery-mock.js'); +const listingMock = require('./lib/listing-mock.js'); +const cleanDB = require('./lib/clean-db.js'); +const serverControl = require('./lib/server-control.js'); + +// APP MODULES +const server = require('../server.js'); +const url = `http://localhost:${process.env.PORT}`; + +const examplePhoto = { + name: 'goose', + alt: 'good times', + image: `${__dirname}/data/dog.jpg`, +}; + +describe('testing photo router', function() { + + before(done => serverControl.serverUp(server, done)); + after(done => serverControl.serverDown(server, done)); + afterEach(done => cleanDB(done)); + + describe('testing POST routes - /api/artist/:artistID/photo', function() { + describe('with valid token and data', function() { + + before(done => artistMock.call(this, done)); + + it ('should return a photo', done => { + request.post(`${url}/api/artist/${this.tempArtist._id}/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(examplePhoto.name); + expect(res.body.alt).to.equal(examplePhoto.alt); + expect(res.body.imageURI).to.equal(awsMocks.uploadMock.Location); + expect(res.body.objectKey).to.equal(awsMocks.uploadMock.Key); + done(); + }); + }); + }); + + describe('with no image', function() { + + before(done => artistMock.call(this, done)); + + it('should respond with status 400', done => { + request.post(`${url}/api/artist/${this.tempArtist._id}/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with an invalid token', function() { + + before(done => artistMock.call(this, done)); + + it('should respond with status 401', done => { + request.post(`${url}/api/artist/${this.tempArtist._id}/photo`) + .set({Authorization:`Bearer ${this.tempToken}12345`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with an invalid artistID', function() { + + before(done => artistMock.call(this, done)); + + it('should respond with status 404', done => { + request.post(`${url}/api/artist/${this.tempArtist._id}goose/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + }); + + describe('testing DELETE routes - /api/artist/:artistID/photo/:photoID', function() { + describe('with valid token and data', function() { + + before(done => photoMock.call(this, done)); + + it ('should return a photo', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/photo/${this.tempPhoto._id}`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with invalid token/no bearer auth', function() { + + before(done => photoMock.call(this, done)); + + it ('should respond with 401 UnauthorizedError', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/photo/${this.tempPhoto._id}`) + .set({Authorization: 'Bearer bad'}) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('no auth header', function(){ + + before(done => photoMock.call(this, done)); + + it('should respond with status 400 bad request', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/photo/${this.tempPhoto._id}`) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + done(); + }); + }); + }); + + describe('with invalid artistID', function() { + + before(done => photoMock.call(this, done)); + + it ('should return id not found', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}bad/photo/${this.tempPhoto._id}`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with invalid photoID', function() { + + before(done => photoMock.call(this, done)); + + it ('should return id not found', done => { + request.delete(`${url}/api/artist/${this.tempArtist._id}/photo/${this.tempPhoto._id}bad`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + }); + + describe('testing POST /api/gallery/:galleryID/photo', function() { + describe('with valid token and data', function() { + + before(done => galleryMock.call(this, done)); + + it ('should return a photo', done => { + request.post(`${url}/api/gallery/${this.tempGallery._id}/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(examplePhoto.name); + expect(res.body.alt).to.equal(examplePhoto.alt); + expect(res.body.imageURI).to.equal(awsMocks.uploadMock.Location); + expect(res.body.objectKey).to.equal(awsMocks.uploadMock.Key); + done(); + }); + }); + }); + + describe('with no image', function() { + + before(done => galleryMock.call(this, done)); + + it('should respond with status 400', done => { + request.post(`${url}/api/gallery/${this.tempGallery._id}/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with an invalid token', function() { + + before(done => galleryMock.call(this, done)); + + it('should respond with status 401', done => { + request.post(`${url}/api/gallery/${this.tempGallery._id}/photo`) + .set({Authorization:`Bearer ${this.tempToken}12345`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with an invalid galleryID', function() { + + before(done => galleryMock.call(this, done)); + + it('should respond with status 404', done => { + request.post(`${url}/api/artist/${this.tempGallery._id}goose/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + }); + + describe('testing DELETE routes - /api/gallery/:galleryID/photo/:photoID', function() { + describe('with valid token and data', function() { + + before(done => photoMock.call(this, done)); + + it ('should return a photo', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/photo/${this.tempPhoto._id}`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with invalid token/no bearer auth', function() { + + before(done => photoMock.call(this, done)); + + it ('should respond with 401 UnauthorizedError', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/photo/${this.tempPhoto._id}`) + .set({Authorization: 'Bearer bad'}) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with no auth header', function(){ + + before(done => photoMock.call(this, done)); + + it('should respond with status 400 bad request', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/photo/${this.tempPhoto._id}`) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + done(); + }); + }); + }); + + describe('with invalid artistID', function() { + + before(done => photoMock.call(this, done)); + + it ('should return id not found', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}bad/photo/${this.tempPhoto._id}`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with invalid photoID', function() { + + before(done => photoMock.call(this, done)); + + it ('should return id not found', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/photo/${this.tempPhoto._id}bad`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + }); + + describe('testing POST /api/listing/:listingID/photo', function() { + describe('with valid token and data', function() { + + before(done => listingMock.call(this, done)); + + it ('should return a photo', done => { + request.post(`${url}/api/listing/${this.tempListing._id}/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(examplePhoto.name); + expect(res.body.alt).to.equal(examplePhoto.alt); + expect(res.body.imageURI).to.equal(awsMocks.uploadMock.Location); + expect(res.body.objectKey).to.equal(awsMocks.uploadMock.Key); + done(); + }); + }); + }); + + describe('with no image', function() { + + before(done => listingMock.call(this, done)); + + it('should respond with status 400', done => { + request.post(`${url}/api/listing/${this.tempListing._id}/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + + describe('with an invalid token', function() { + + before(done => listingMock.call(this, done)); + + it('should respond with status 401', done => { + request.post(`${url}/api/listing/${this.tempListing._id}/photo`) + .set({Authorization:`Bearer ${this.tempToken}12345`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with an invalid galleryID', function() { + + before(done => listingMock.call(this, done)); + + it('should respond with status 404', done => { + request.post(`${url}/api/listing/${this.tempListing._id}goose/photo`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .field('name', examplePhoto.name) + .field('alt', examplePhoto.alt) + .attach('image', examplePhoto.image) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + }); + + describe('testing DELETE /api/gallery/:galleryID/photo/:photoID', function(){ + describe('with valid token and data', function() { + + before(done => photoMock.call(this, done)); + + it ('should return a photo', done => { + request.delete(`${url}/api/listing/${this.tempListing._id}/photo/${this.tempPhoto._id}`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + + describe('with invalid token/no bearer auth', function() { + + before(done => photoMock.call(this, done)); + + it ('should respond with 401 UnauthorizedError', done => { + request.delete(`${url}/api/listing/${this.tempListing._id}/photo/${this.tempPhoto._id}`) + .set({Authorization: 'Bearer bad'}) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + + describe('with no auth header', function(){ + + before(done => photoMock.call(this, done)); + + it('should respond with status 400 bad request', done => { + request.delete(`${url}/api/listing/${this.tempListing._id}/photo/${this.tempPhoto._id}`) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.text).to.equal('BadRequestError'); + done(); + }); + }); + }); + + describe('with invalid artistID', function() { + + before(done => photoMock.call(this, done)); + + it ('should return id not found', done => { + request.delete(`${url}/api/listing/${this.tempListing._id}bad/photo/${this.tempPhoto._id}`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + describe('with invalid photoID', function() { + + before(done => photoMock.call(this, done)); + + it ('should return id not found', done => { + request.delete(`${url}/api/listing/${this.tempListing._id}/photo/${this.tempPhoto._id}bad`) + .set({Authorization: `Bearer ${this.tempToken}`}) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); + }); +}); diff --git a/lab-claudia/webpack.config.js b/lab-claudia/webpack.config.js new file mode 100644 index 0000000..084f21e --- /dev/null +++ b/lab-claudia/webpack.config.js @@ -0,0 +1,68 @@ +'use strict'; + +require('dotenv').load(); +const webpack = require('webpack'); +const HTMLPlugin = require('html-webpack-plugin'); +const CleanPlugin = require('clean-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const production = process.env.NODE_ENV === 'production'; + +let plugins = [ + new ExtractTextPlugin('bundle.css'), + new HTMLPlugin({template: `${__dirname}/app/index.html`}), + new webpack.DefinePlugin({ + __API_URL__: JSON.stringify(process.env.API_URL), + __DEBUG__: JSON.stringify(!production), + }), +]; + +if (production){ + plugins = plugins.concat([ + new webpack.optimize.UglifyJsPlugin({ + mangle: true, + compress: { + warnings: false, + }, + }), + new CleanPlugin(), + ]); +} + +module.exports = { + entry: `${__dirname}/app/entry.js`, + devtool: production ? false : 'eval', + plugins, + output: { + path: 'build', + filename: 'bundle.js', + }, + sassLoader: { + includePaths: [`${__dirname}/app/scss/lib`], + }, + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel', + }, + { + test: /\.html$/, + loader: 'html', + }, + { + test: /\.(woff|ttf|svg|eot).*/, + loader: 'url?limit=10000&name=font/[name].[ext]', + }, + { + test: /\.(jpg|jpeg|bmp|tiff|gif|png)$/, + loader: 'url?limit=10000&name=image/[hash].[ext]', + }, + { + test: /\.scss$/, + loader: ExtractTextPlugin.extract('style', 'css!resolve-url!sass?sourceMap'), + }, + ], + }, +}; From 4b7877acc159d82247712ae2abee3680436932d1 Mon Sep 17 00:00:00 2001 From: Claudia Date: Tue, 8 Nov 2016 22:16:13 -0800 Subject: [PATCH 2/3] added stuff --- lab-claudia/.client.env | 5 +- lab-claudia/.gitignore | 2 +- lab-claudia/.server.env | 5 +- lab-claudia/app/entry.js | 4 +- lab-claudia/app/view/home/home-controller.js | 42 ++++++++++--- .../app/view/landing/landing-controller.js | 29 ++++++++- lab-claudia/app/view/landing/landing.html | 1 + lab-claudia/gulpfile.js | 24 -------- lab-claudia/lib/google-oauth-middleware.js | 46 +++++++++++++++ lab-claudia/package.json | 37 ++++++------ lab-claudia/route/auth-router.js | 59 +++++++++++++++++++ lab-claudia/server.js | 15 +++-- lab-claudia/webpack.config.js | 9 ++- 13 files changed, 210 insertions(+), 68 deletions(-) delete mode 100644 lab-claudia/gulpfile.js create mode 100644 lab-claudia/lib/google-oauth-middleware.js diff --git a/lab-claudia/.client.env b/lab-claudia/.client.env index 27c66d1..4075450 100644 --- a/lab-claudia/.client.env +++ b/lab-claudia/.client.env @@ -1,5 +1,4 @@ -# example .env NODE_ENV="dev" TITLE="Photo Gallery" -API_URL="http://localhost:3000" -GOOGLE_CLIENT_ID="308322069962-972jlkfpbov9cd88ot0dtdtc2lgfutdl.apps.googleusercontent.com" +__API_URL__="http://localhost:3000" +__GOOGLE_CLIENT_ID__="308322069962-972jlkfpbov9cd88ot0dtdtc2lgfutdl.apps.googleusercontent.com" diff --git a/lab-claudia/.gitignore b/lab-claudia/.gitignore index e588426..e3a1c88 100644 --- a/lab-claudia/.gitignore +++ b/lab-claudia/.gitignore @@ -1,6 +1,6 @@ #Other db -.env +*.env build coverage html-report diff --git a/lab-claudia/.server.env b/lab-claudia/.server.env index 3273e74..595a3cc 100644 --- a/lab-claudia/.server.env +++ b/lab-claudia/.server.env @@ -1,10 +1,11 @@ PORT=3000 NODE_ENV="dev" MONGODB_URI=mongodb://localhost/art-c-dev +API_URL="http://localhost:3000" APP_SECRET='my secret code' -AWS_ACCESS_KEY_ID=AKIAIP2EINCLGLW2GOHQ -AWS_SECRET_ACCESS_KEY=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 +AWS_ACCESS_KEY_ID=AKIAJ6E6ZFEP6BJYUUPQ +AWS_SECRET_ACCESS_KEY=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 AWS_BUCKET='tastytoast-assets' GOOGLE_CLIENT_ID="308322069962-972jlkfpbov9cd88ot0dtdtc2lgfutdl.apps.googleusercontent.com" diff --git a/lab-claudia/app/entry.js b/lab-claudia/app/entry.js index 1612111..769570c 100644 --- a/lab-claudia/app/entry.js +++ b/lab-claudia/app/entry.js @@ -16,12 +16,12 @@ const pascalcase = require('pascalcase'); const ngTouch = require('angular-touch'); // Dependency of bootstrap const ngAnimate = require('angular-animate'); // Dependency of bootstrap const uiRouter = require('angular-ui-router'); -const uiBootstrap = require('angular-ui-bootstrap'); // Need touch and animate to use +//const uiBootstrap = require('angular-ui-bootstrap'); // Need touch and animate to use const ngFileUpload = require('ng-file-upload'); // Create angular module // Add everything we need to module -const demoApp = angular.module('demoApp', [ngTouch, ngAnimate, uiRouter, uiBootstrap, ngFileUpload]); +const demoApp = angular.module('demoApp', [ngTouch, ngAnimate, uiRouter, ngFileUpload]); // WHAT DOES REQUIRE.CONTEXT DO? ////////////////////////////////////// diff --git a/lab-claudia/app/view/home/home-controller.js b/lab-claudia/app/view/home/home-controller.js index 00776ad..1756974 100644 --- a/lab-claudia/app/view/home/home-controller.js +++ b/lab-claudia/app/view/home/home-controller.js @@ -2,17 +2,41 @@ require('./_home.scss'); -module.exports = ['$log', HomeController ]; +module.exports = ['$log', '$rootScope', 'galleryService', HomeController ]; -function HomeController($log){ +function HomeController($log, $rootScope, galleryService){ $log.debug('init homeCtrl'); + this.galleries = []; - let googleAuthBase = 'https://accounts.google.com/o/oauth2/v2/auth'; - let googleAuthResponseType = 'response_type=code'; - let googleAuthClientID = `client_id=${__GOOGLE_CLIENT_ID__}`; - let googleAuthScope = 'scope=profile%20email%20openid'; - let googleAuthRedirectURI = 'redirect_uri=http://localhost:3000/api/auth/oauth_callback'; - let googleAuthAccessType = 'access_type=offline'; + this.fetchGalleries = function(){ + galleryService.fetchGalleries() + // Fetching returns an array of galleries + // Galleries is set as a property on the home controller + .then( galleries => { + this.galleries = galleries; + // The current gallery is the first one in the index + // In the html, the current gallery is passed into + // The thumbnail-container gallery attribute + this.currentGallery = galleries[0]; + $log.log('Succesfully found gallery'); + }); + }; + +//if you pass in function(gallery, unicorn) --> object would have another property + this.galleryDeleteDone = function(gallery){ + $log.debug('init homeCtrl.galleryDeleteDone()'); + if (this.currentGallery._id == gallery._id) { + // If the current gallery matches the galleryid, it is set to null + this.currentGallery = null; + } + }; + + // When page is loaded (controller gets created), call fetchGalleries + this.fetchGalleries(); + + // Any time url changes (locationChangeSuccess), call fetchGalleries + $rootScope.$on('$locationChangeSuccess', () => { + this.fetchGalleries(); + }); - this.googleAuthURL = `${googleAuthBase}?${googleAuthResponseType}&${googleAuthClientID}&${googleAuthScope}&${googleAuthRedirectURI}&${googleAuthAccessType}`; } diff --git a/lab-claudia/app/view/landing/landing-controller.js b/lab-claudia/app/view/landing/landing-controller.js index 93b5a59..9b9cb62 100644 --- a/lab-claudia/app/view/landing/landing-controller.js +++ b/lab-claudia/app/view/landing/landing-controller.js @@ -2,20 +2,43 @@ require('./_landing.scss'); -module.exports = ['$log', '$location', '$rootScope', LandingController]; +module.exports = ['$log', '$location', '$rootScope', '$authService', LandingController]; + +function LandingController($log, $location, $rootScope, authService){ + $log.debug('init landingCtrl'); -function LandingController($log, $location){ let url = $location.url(); this.showSignup = url === '/join#signup' || url === '/join'; + let query = $location.search(); + + if(query.token){ + authService.setToken(query.token) + .then(() => { + $location.path('/#/home'); + }); + } + + $rootScope.$on('locationChangeSuccess', () => { + let query = $location.search(); + console.log('query', query); + if(query.token){ + authService.setToken(query.token) + .then(() => { + $location.path('/#/home'); + }); + } + }); + let googleAuthBase = 'https://accounts.google.com/o/oauth2/v2/auth'; let googleAuthResponseType = 'response_type=code'; let googleAuthClientID = `client_id=${__GOOGLE_CLIENT_ID__}`; let googleAuthScope = 'scope=profile%20email%20openid'; let googleAuthRedirectURI = 'redirect_uri=http://localhost:3000/api/auth/oauth_callback'; let googleAuthAccessType = 'access_type=offline'; + let googleAuthPrompt = 'prompt=consent'; - this.googleAuthURL = `${googleAuthBase}?${googleAuthResponseType}&${googleAuthClientID}&${googleAuthScope}&${googleAuthRedirectURI}&${googleAuthAccessType}`; + this.googleAuthURL = `${googleAuthBase}?${googleAuthResponseType}&${googleAuthClientID}&${googleAuthScope}&${googleAuthRedirectURI}&${googleAuthAccessType}&${googleAuthPrompt}`; } diff --git a/lab-claudia/app/view/landing/landing.html b/lab-claudia/app/view/landing/landing.html index 8fcdff4..900428e 100644 --- a/lab-claudia/app/view/landing/landing.html +++ b/lab-claudia/app/view/landing/landing.html @@ -13,6 +13,7 @@ ng-click="landingCtrl.showSignup=false"> Login + Login with Google diff --git a/lab-claudia/gulpfile.js b/lab-claudia/gulpfile.js deleted file mode 100644 index e40b453..0000000 --- a/lab-claudia/gulpfile.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -const gulp = require('gulp'); -const eslint = require('gulp-eslint'); -const mocha = require('gulp-mocha'); - -gulp.task('lint', function() { - return gulp.src(['**/*.js', '!node_modules/**']) - .pipe(eslint()) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()); -}); - -gulp.task('test', function() { - gulp.src('./test/*-test.js', {read:false}) - .pipe(mocha({reporter:'landing'})); -}); - -gulp.task('default', ['test', 'lint']); - -gulp.task('dev', function() { - gulp.watch(['**/*.js', '!node_modules/**'], - ['lint', 'test']); -}); diff --git a/lab-claudia/lib/google-oauth-middleware.js b/lab-claudia/lib/google-oauth-middleware.js new file mode 100644 index 0000000..93ca180 --- /dev/null +++ b/lab-claudia/lib/google-oauth-middleware.js @@ -0,0 +1,46 @@ +'use strict'; + +const request = require('superagent'); +const debug = require('debug')('art-c:google-oauth-middleware'); + +module.exports = function(req, res, next){ + debug('getting google user info'); + if (req.query.error){ + req.googleError = new Error(req.query.error); + return next(); + } + + let data = { + code: req.query.code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: `${process.env.API_URL}/api/auth/oauth_callback`, + grant_type: 'authorization_code', + }; + + let accessToken, refreshToken, tokenTTL; + request.post('https://www.googleapis.com/oauth2/v4/token') + .type('form') + .send(data) + .then(response => { + accessToken = response.body.access_token; + refreshToken = response.body.refresh_token; + tokenTTL = response.body.expires_in; // how long the accessToken token will work in seconds + return request.get('https://www.googleapis.com/plus/v1/people/me/openIdConnect') + .set('Authorization', `Bearer ${response.body.access_token}`); + }) + .then( response => { + req.googleOAUTH = { + googleID: response.body.sub, + email: response.body.email, + accessToken, + refreshToken, + tokenTTL, + }; + next(); + }) + .catch((err) => { + req.googleError = err; + next(); + }); +}; diff --git a/lab-claudia/package.json b/lab-claudia/package.json index 58c4ab1..8e0c263 100644 --- a/lab-claudia/package.json +++ b/lab-claudia/package.json @@ -1,5 +1,5 @@ { - "name": "art-c-fullstack", + "name": "art-c", "version": "1.0.0", "description": "", "main": "index.js", @@ -12,8 +12,8 @@ "frontend-test": "./node_modules/karma/bin/karma start --single-run", "test-watch": "./node_modules/karma/bin/karma start", "start": "node server.js", - "start-debug": "DEBUG='slug*' npm start", - "start-watch": "DEBUG='slug*' ./node_modules/nodemon/bin/nodemon.js server.js", + "start-debug": "DEBUG='art-c*' npm start", + "start-watch": "DEBUG='art-c*' ./node_modules/nodemon/bin/nodemon.js server.js", "test": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha", "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", "lint": "./node_modules/eslint/bin/eslint.js .", @@ -29,40 +29,41 @@ "angular-touch": "^1.5.8", "angular-ui-bootstrap": "^2.2.0", "angular-ui-router": "^0.3.1", + "aws-sdk": "^2.6.6", "babel-core": "^6.17.0", "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.16.0", + "bcrypt": "^0.8.7", + "bluebird": "^3.4.6", + "body-parser": "^1.15.2", "bootstrap-sass": "^3.3.7", "camelcase": "^3.0.0", "clean-webpack-plugin": "^0.1.13", + "cors": "^2.8.1", "css-loader": "^0.25.0", + "debug": "^2.2.0", + "del": "^2.2.2", "dotenv": "^2.0.0", + "express": "^4.14.0", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.9.0", "font-awesome": "^4.6.3", "html-loader": "^0.4.4", "html-webpack-plugin": "^2.22.0", + "http-errors": "^1.5.0", + "jsonwebtoken": "^7.1.9", + "mongoose": "^4.6.2", + "morgan": "^1.7.0", + "multer": "^1.2.0", "node-sass": "^3.10.1", "pascalcase": "^0.1.1", "resolve-url-loader": "^1.6.0", "sass-loader": "^4.0.2", "style-loader": "^0.13.1", + "superagent": "^2.3.0", "ui-router": "^1.0.0-alpha.3", "url-loader": "^0.5.7", - "webpack": "^1.13.2", - "aws-sdk": "^2.6.6", - "bcrypt": "^0.8.7", - "bluebird": "^3.4.6", - "body-parser": "^1.15.2", - "cors": "^2.8.1", - "debug": "^2.2.0", - "del": "^2.2.2", - "express": "^4.14.0", - "http-errors": "^1.5.0", - "jsonwebtoken": "^7.1.9", - "mongoose": "^4.6.2", - "morgan": "^1.7.0", - "multer": "^1.2.0" + "webpack": "^1.13.2" }, "devDependencies": { "angular-mocks": "^1.5.8", @@ -78,8 +79,8 @@ "karma-webpack": "^1.8.0", "lorem-ipsum": "^1.0.3", "mocha": "^3.1.0", + "ng-file-upload": "^12.2.13", "nodemon": "^1.11.0", - "superagent": "^2.3.0", "webpack-dev-server": "^1.16.2" } } diff --git a/lab-claudia/route/auth-router.js b/lab-claudia/route/auth-router.js index 15c3676..7cdd7fa 100644 --- a/lab-claudia/route/auth-router.js +++ b/lab-claudia/route/auth-router.js @@ -12,6 +12,7 @@ const Listing = require('../model/listing.js'); const Photo = require('../model/photo.js'); const User = require('../model/user.js'); +const googleOAUTH = require('../lib/google-oauth-middleware.js'); const basicAuth = require('../lib/basic-auth-middleware.js'); const bearerAuth = require('../lib/bearer-auth-middleware'); @@ -100,3 +101,61 @@ authRouter.put('/api/user/updatePassword', bearerAuth, jsonParser, function(req, }) .catch(next); }); + + +authRouter.get('/api/auth/oauth_callback', googleOAUTH, function(req, res){ + debug('GET /api/auth/oauth_callback'); + // should have either req.googleError or req.googleOAUTH + console.log('googleError', req.googleError); + console.log('googleOAUTH', req.googleOAUTH); + + // if googleError deal with google Error + if(req.googleError){ + return res.redirect('/'); + } + + // check if user allreay exists + User.findOne({email: req.googleOAUTH.email}) + .then(user => { + if (!user) return Promise.reject(new Error('user not found')); + return user; + }) + .catch(err => { + if (err.message === 'user not found'){ + let userData = { + username: req.googleOAUTH.email, + email: req.googleOAUTH.email, + google: { + googleID: req.googleOAUTH.googleID, + tokenTTL: req.googleOAUTH.tokenTTL, + tokenTimestamp: Date.now(), + refreshToken: req.googleOAUTH.refreshToken, + accessToken: req.googleOAUTH.accessToken, + }, + }; + return new User(userData).save(); + } + return Promise.reject(err); + }) + .then(user => user.generateToken()) + .then(token => { + res.redirect(`/?token=${token}`); + }) + .catch(err => { + console.error(err); + console.log('user not found'); + res.redirect('/'); + }); + //res.send('lulwat'); + //return new User(userData).save(); + //}) + //.then(user => user.generateToken()) + //.then(token => { + //res.send(token); + //}) + //.catch((err) => { + //console.error(err); + //res.send('boo hoo') + //}); + +}); diff --git a/lab-claudia/server.js b/lab-claudia/server.js index 6326bb4..840527f 100644 --- a/lab-claudia/server.js +++ b/lab-claudia/server.js @@ -18,22 +18,26 @@ const photoRouter = require('./route/photo-router.js'); const pageRouter = require('./route/page-router.js'); const errorMiddleware = require('./lib/error-middleware.js'); -// load env variables -dotenv.load(); -// connect to mongo database +// Load server environment variables +dotenv.load({path: `${__dirname}/.server.env`}); + +// Connect to mongo database mongoose.Promise = Promise; mongoose.connect(process.env.MONGODB_URI); -// module constants +// Module constants const PORT = process.env.PORT; const app = express(); // app middleware app.use(cors()); -app.use(morgan('dev')); +let production = process.env.NODE_ENV === 'production'; +let morganFormat = production ? 'common' : 'dev'; +app.use(morgan(morganFormat)); // app routes +app.use(express.static(`${__dirname}/build`)); app.use(authRouter); app.use(artistRouter); app.use(galleryRouter); @@ -45,6 +49,7 @@ app.use(errorMiddleware); // start server const server = module.exports = app.listen(PORT, function() { debug('server started'); + console.log('server up'); }); server.isRunning = true; diff --git a/lab-claudia/webpack.config.js b/lab-claudia/webpack.config.js index 084f21e..000c6b3 100644 --- a/lab-claudia/webpack.config.js +++ b/lab-claudia/webpack.config.js @@ -1,6 +1,11 @@ 'use strict'; -require('dotenv').load(); +require('dotenv').load({path: `${__dirname}/.client.env`}); +if (!process.env.API_URL || !process.env.NODE_ENV || !process.env.TITLE){ + console.error('ERROR: ng-template requires .env file'); + process.exit(1); +} + const webpack = require('webpack'); const HTMLPlugin = require('html-webpack-plugin'); const CleanPlugin = require('clean-webpack-plugin'); @@ -13,6 +18,8 @@ let plugins = [ new HTMLPlugin({template: `${__dirname}/app/index.html`}), new webpack.DefinePlugin({ __API_URL__: JSON.stringify(process.env.API_URL), + __GOOGLE_CLIENT_ID__: JSON.stringify(process.env.GOOGLE_CLIENT_ID), + __TITLE__: JSON.stringify(process.env.TITLE), __DEBUG__: JSON.stringify(!production), }), ]; From 5bde6ca7e2c15fd28566a6941d55799054477c21 Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 9 Nov 2016 10:43:27 -0800 Subject: [PATCH 3/3] fixed some things --- lab-claudia/.client.env | 4 ++-- lab-claudia/app/entry.js | 8 +++++++- lab-claudia/app/view/landing/landing-controller.js | 2 +- lab-claudia/app/view/landing/landing.html | 6 ++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lab-claudia/.client.env b/lab-claudia/.client.env index 4075450..d6fb96d 100644 --- a/lab-claudia/.client.env +++ b/lab-claudia/.client.env @@ -1,4 +1,4 @@ NODE_ENV="dev" TITLE="Photo Gallery" -__API_URL__="http://localhost:3000" -__GOOGLE_CLIENT_ID__="308322069962-972jlkfpbov9cd88ot0dtdtc2lgfutdl.apps.googleusercontent.com" +API_URL="http://localhost:3000" +GOOGLE_CLIENT_ID="308322069962-972jlkfpbov9cd88ot0dtdtc2lgfutdl.apps.googleusercontent.com" diff --git a/lab-claudia/app/entry.js b/lab-claudia/app/entry.js index 769570c..082b19d 100644 --- a/lab-claudia/app/entry.js +++ b/lab-claudia/app/entry.js @@ -21,7 +21,13 @@ const ngFileUpload = require('ng-file-upload'); // Create angular module // Add everything we need to module -const demoApp = angular.module('demoApp', [ngTouch, ngAnimate, uiRouter, ngFileUpload]); +const demoApp = angular.module(camelcase(__TITLE__), [ngTouch, ngAnimate, uiRouter, ngFileUpload]); + +// create angular module +// set up $rootScope globals +demoApp.run(['$rootScope', function($rootScope){ + $rootScope.title = __TITLE__; +}]); // WHAT DOES REQUIRE.CONTEXT DO? ////////////////////////////////////// diff --git a/lab-claudia/app/view/landing/landing-controller.js b/lab-claudia/app/view/landing/landing-controller.js index 9b9cb62..8f03b01 100644 --- a/lab-claudia/app/view/landing/landing-controller.js +++ b/lab-claudia/app/view/landing/landing-controller.js @@ -2,7 +2,7 @@ require('./_landing.scss'); -module.exports = ['$log', '$location', '$rootScope', '$authService', LandingController]; +module.exports = ['$log', '$location', '$rootScope', 'authService', LandingController]; function LandingController($log, $location, $rootScope, authService){ $log.debug('init landingCtrl'); diff --git a/lab-claudia/app/view/landing/landing.html b/lab-claudia/app/view/landing/landing.html index 900428e..da06693 100644 --- a/lab-claudia/app/view/landing/landing.html +++ b/lab-claudia/app/view/landing/landing.html @@ -1,9 +1,6 @@

- Login with Google - -
@@ -13,10 +10,11 @@ ng-click="landingCtrl.showSignup=false"> Login - Login with Google
+ +