From 8ec6411d284110a49575214843ea2690ff520135 Mon Sep 17 00:00:00 2001 From: machikoyasuda Date: Thu, 7 Sep 2023 20:48:09 +0000 Subject: [PATCH] Deployed aadce51 with MkDocs version: 1.4.2 --- .nojekyll | 0 .pages | 10 + 404.html | 1154 +++ assets/images/favicon.png | Bin 0 -> 1870 bytes assets/javascripts/bundle.51d95adb.min.js | 29 + assets/javascripts/bundle.51d95adb.min.js.map | 8 + assets/javascripts/lunr/min/lunr.ar.min.js | 1 + assets/javascripts/lunr/min/lunr.da.min.js | 18 + assets/javascripts/lunr/min/lunr.de.min.js | 18 + assets/javascripts/lunr/min/lunr.du.min.js | 18 + assets/javascripts/lunr/min/lunr.es.min.js | 18 + assets/javascripts/lunr/min/lunr.fi.min.js | 18 + assets/javascripts/lunr/min/lunr.fr.min.js | 18 + assets/javascripts/lunr/min/lunr.hi.min.js | 1 + assets/javascripts/lunr/min/lunr.hu.min.js | 18 + assets/javascripts/lunr/min/lunr.it.min.js | 18 + assets/javascripts/lunr/min/lunr.ja.min.js | 1 + assets/javascripts/lunr/min/lunr.jp.min.js | 1 + assets/javascripts/lunr/min/lunr.ko.min.js | 1 + assets/javascripts/lunr/min/lunr.multi.min.js | 1 + assets/javascripts/lunr/min/lunr.nl.min.js | 18 + assets/javascripts/lunr/min/lunr.no.min.js | 18 + assets/javascripts/lunr/min/lunr.pt.min.js | 18 + assets/javascripts/lunr/min/lunr.ro.min.js | 18 + assets/javascripts/lunr/min/lunr.ru.min.js | 18 + .../lunr/min/lunr.stemmer.support.min.js | 1 + assets/javascripts/lunr/min/lunr.sv.min.js | 18 + assets/javascripts/lunr/min/lunr.ta.min.js | 1 + assets/javascripts/lunr/min/lunr.th.min.js | 1 + assets/javascripts/lunr/min/lunr.tr.min.js | 18 + assets/javascripts/lunr/min/lunr.vi.min.js | 1 + assets/javascripts/lunr/min/lunr.zh.min.js | 1 + assets/javascripts/lunr/tinyseg.js | 206 + assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.e5c33ebb.min.js | 42 + .../workers/search.e5c33ebb.min.js.map | 8 + assets/stylesheets/main.558e4712.min.css | 1 + assets/stylesheets/main.558e4712.min.css.map | 1 + assets/stylesheets/palette.2505c338.min.css | 1 + .../stylesheets/palette.2505c338.min.css.map | 1 + configuration/.pages | 9 + .../content-security-policy/index.html | 1301 ++++ configuration/data/index.html | 1310 ++++ .../environment-variables/index.html | 1561 ++++ configuration/index.html | 1285 ++++ configuration/oauth/index.html | 1267 ++++ configuration/rate-limit/index.html | 1265 ++++ configuration/recaptcha/index.html | 1283 ++++ configuration/transit-agency/index.html | 1347 ++++ deployment/.pages | 7 + deployment/azure/index.html | 15 + deployment/index.html | 1257 +++ deployment/infrastructure/index.html | 1531 ++++ deployment/release/index.html | 1330 ++++ deployment/secrets/index.html | 1250 +++ deployment/troubleshooting/index.html | 1381 ++++ deployment/workflows/index.html | 1272 ++++ development/.pages | 8 + .../commits-branches-merging/index.html | 1315 ++++ development/docker-dynamic-ports/index.html | 1291 ++++ development/i18n/index.html | 1293 ++++ .../img/docker-desktop-open-in-browser.png | Bin 0 -> 18146 bytes development/img/ports-local-address.png | Bin 0 -> 116329 bytes .../img/vscode-debugger-launch-config.png | Bin 0 -> 45072 bytes development/index.html | 1315 ++++ development/linting-pre-commit/index.html | 1249 +++ development/models-migrations/index.html | 1255 +++ development/test-server/index.html | 1244 +++ getting-started/.pages | 3 + getting-started/development/index.html | 15 + .../docker-dynamic-ports/index.html | 15 + getting-started/documentation/index.html | 1296 ++++ getting-started/img/edit-pencil.png | Bin 0 -> 44841 bytes getting-started/index.html | 1280 ++++ index.html | 1314 ++++ product-and-design/.pages | 4 + product-and-design/copy-delivery/index.html | 1294 ++++ requirements.txt | 5 + search/search_index.json | 1 + sitemap.xml | 153 + sitemap.xml.gz | Bin 0 -> 511 bytes students/index.html | 15 + tests/.pages | 3 + tests/coverage/coverage_html.js | 624 ++ .../d_5351a2d360ecd143___init___py.html | 99 + .../d_5351a2d360ecd143_analytics_py.html | 129 + tests/coverage/d_5351a2d360ecd143_api_py.html | 384 + .../coverage/d_5351a2d360ecd143_apps_py.html | 109 + .../coverage/d_5351a2d360ecd143_forms_py.html | 128 + .../coverage/d_5351a2d360ecd143_urls_py.html | 115 + .../coverage/d_5351a2d360ecd143_views_py.html | 225 + .../d_7435199c01eb52ab___init___py.html | 99 + .../d_7435199c01eb52ab_analytics_py.html | 174 + .../coverage/d_7435199c01eb52ab_apps_py.html | 121 + .../d_7435199c01eb52ab_client_py.html | 163 + .../d_7435199c01eb52ab_middleware_py.html | 122 + .../d_7435199c01eb52ab_redirects_py.html | 130 + .../coverage/d_7435199c01eb52ab_urls_py.html | 113 + .../coverage/d_7435199c01eb52ab_views_py.html | 226 + .../d_795c8c28b74e7b9e___init___py.html | 102 + .../d_795c8c28b74e7b9e_sentry_py.html | 207 + .../d_795c8c28b74e7b9e_settings_py.html | 425 ++ .../coverage/d_795c8c28b74e7b9e_urls_py.html | 141 + .../coverage/d_795c8c28b74e7b9e_wsgi_py.html | 115 + .../d_8c0b35f1ea7ee6af___init___py.html | 99 + .../coverage/d_8c0b35f1ea7ee6af_admin_py.html | 121 + .../d_8c0b35f1ea7ee6af_analytics_py.html | 259 + .../coverage/d_8c0b35f1ea7ee6af_apps_py.html | 109 + ...c0b35f1ea7ee6af_context_processors_py.html | 172 + .../d_8c0b35f1ea7ee6af_middleware_py.html | 263 + .../d_8c0b35f1ea7ee6af_models_py.html | 374 + .../d_8c0b35f1ea7ee6af_recaptcha_py.html | 130 + .../d_8c0b35f1ea7ee6af_session_py.html | 388 + .../coverage/d_8c0b35f1ea7ee6af_urls_py.html | 151 + .../coverage/d_8c0b35f1ea7ee6af_views_py.html | 193 + .../d_8c0b35f1ea7ee6af_widgets_py.html | 141 + .../d_d1111b74f8c04d3c___init___py.html | 99 + .../d_d1111b74f8c04d3c_analytics_py.html | 164 + .../coverage/d_d1111b74f8c04d3c_apps_py.html | 109 + .../coverage/d_d1111b74f8c04d3c_forms_py.html | 257 + .../coverage/d_d1111b74f8c04d3c_urls_py.html | 116 + .../d_d1111b74f8c04d3c_verify_py.html | 134 + .../coverage/d_d1111b74f8c04d3c_views_py.html | 272 + tests/coverage/favicon_32.png | Bin 0 -> 1732 bytes tests/coverage/index.html | 452 ++ tests/coverage/keybd_closed.png | Bin 0 -> 9004 bytes tests/coverage/keybd_open.png | Bin 0 -> 9003 bytes tests/coverage/status.json | 1 + tests/coverage/style.css | 309 + tests/index.html | 1306 ++++ use-cases/agency-cards/index.html | 1287 ++++ use-cases/college/index.html | 1253 +++ use-cases/courtesy-cards/index.html | 15 + use-cases/img/senior-success.gif | Bin 0 -> 1130314 bytes use-cases/index.html | 1244 +++ use-cases/seniors/index.html | 1251 +++ use-cases/students/index.html | 15 + 137 files changed, 56124 insertions(+) create mode 100644 .nojekyll create mode 100644 .pages create mode 100644 404.html create mode 100644 assets/images/favicon.png create mode 100644 assets/javascripts/bundle.51d95adb.min.js create mode 100644 assets/javascripts/bundle.51d95adb.min.js.map create mode 100644 assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 assets/javascripts/lunr/tinyseg.js create mode 100644 assets/javascripts/lunr/wordcut.js create mode 100644 assets/javascripts/workers/search.e5c33ebb.min.js create mode 100644 assets/javascripts/workers/search.e5c33ebb.min.js.map create mode 100644 assets/stylesheets/main.558e4712.min.css create mode 100644 assets/stylesheets/main.558e4712.min.css.map create mode 100644 assets/stylesheets/palette.2505c338.min.css create mode 100644 assets/stylesheets/palette.2505c338.min.css.map create mode 100644 configuration/.pages create mode 100644 configuration/content-security-policy/index.html create mode 100644 configuration/data/index.html create mode 100644 configuration/environment-variables/index.html create mode 100644 configuration/index.html create mode 100644 configuration/oauth/index.html create mode 100644 configuration/rate-limit/index.html create mode 100644 configuration/recaptcha/index.html create mode 100644 configuration/transit-agency/index.html create mode 100644 deployment/.pages create mode 100644 deployment/azure/index.html create mode 100644 deployment/index.html create mode 100644 deployment/infrastructure/index.html create mode 100644 deployment/release/index.html create mode 100644 deployment/secrets/index.html create mode 100644 deployment/troubleshooting/index.html create mode 100644 deployment/workflows/index.html create mode 100644 development/.pages create mode 100644 development/commits-branches-merging/index.html create mode 100644 development/docker-dynamic-ports/index.html create mode 100644 development/i18n/index.html create mode 100644 development/img/docker-desktop-open-in-browser.png create mode 100644 development/img/ports-local-address.png create mode 100644 development/img/vscode-debugger-launch-config.png create mode 100644 development/index.html create mode 100644 development/linting-pre-commit/index.html create mode 100644 development/models-migrations/index.html create mode 100644 development/test-server/index.html create mode 100644 getting-started/.pages create mode 100644 getting-started/development/index.html create mode 100644 getting-started/docker-dynamic-ports/index.html create mode 100644 getting-started/documentation/index.html create mode 100644 getting-started/img/edit-pencil.png create mode 100644 getting-started/index.html create mode 100644 index.html create mode 100644 product-and-design/.pages create mode 100644 product-and-design/copy-delivery/index.html create mode 100644 requirements.txt create mode 100644 search/search_index.json create mode 100644 sitemap.xml create mode 100644 sitemap.xml.gz create mode 100644 students/index.html create mode 100644 tests/.pages create mode 100644 tests/coverage/coverage_html.js create mode 100644 tests/coverage/d_5351a2d360ecd143___init___py.html create mode 100644 tests/coverage/d_5351a2d360ecd143_analytics_py.html create mode 100644 tests/coverage/d_5351a2d360ecd143_api_py.html create mode 100644 tests/coverage/d_5351a2d360ecd143_apps_py.html create mode 100644 tests/coverage/d_5351a2d360ecd143_forms_py.html create mode 100644 tests/coverage/d_5351a2d360ecd143_urls_py.html create mode 100644 tests/coverage/d_5351a2d360ecd143_views_py.html create mode 100644 tests/coverage/d_7435199c01eb52ab___init___py.html create mode 100644 tests/coverage/d_7435199c01eb52ab_analytics_py.html create mode 100644 tests/coverage/d_7435199c01eb52ab_apps_py.html create mode 100644 tests/coverage/d_7435199c01eb52ab_client_py.html create mode 100644 tests/coverage/d_7435199c01eb52ab_middleware_py.html create mode 100644 tests/coverage/d_7435199c01eb52ab_redirects_py.html create mode 100644 tests/coverage/d_7435199c01eb52ab_urls_py.html create mode 100644 tests/coverage/d_7435199c01eb52ab_views_py.html create mode 100644 tests/coverage/d_795c8c28b74e7b9e___init___py.html create mode 100644 tests/coverage/d_795c8c28b74e7b9e_sentry_py.html create mode 100644 tests/coverage/d_795c8c28b74e7b9e_settings_py.html create mode 100644 tests/coverage/d_795c8c28b74e7b9e_urls_py.html create mode 100644 tests/coverage/d_795c8c28b74e7b9e_wsgi_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af___init___py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_admin_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_analytics_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_apps_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_context_processors_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_middleware_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_models_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_recaptcha_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_session_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_urls_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_views_py.html create mode 100644 tests/coverage/d_8c0b35f1ea7ee6af_widgets_py.html create mode 100644 tests/coverage/d_d1111b74f8c04d3c___init___py.html create mode 100644 tests/coverage/d_d1111b74f8c04d3c_analytics_py.html create mode 100644 tests/coverage/d_d1111b74f8c04d3c_apps_py.html create mode 100644 tests/coverage/d_d1111b74f8c04d3c_forms_py.html create mode 100644 tests/coverage/d_d1111b74f8c04d3c_urls_py.html create mode 100644 tests/coverage/d_d1111b74f8c04d3c_verify_py.html create mode 100644 tests/coverage/d_d1111b74f8c04d3c_views_py.html create mode 100644 tests/coverage/favicon_32.png create mode 100644 tests/coverage/index.html create mode 100644 tests/coverage/keybd_closed.png create mode 100644 tests/coverage/keybd_open.png create mode 100644 tests/coverage/status.json create mode 100644 tests/coverage/style.css create mode 100644 tests/index.html create mode 100644 use-cases/agency-cards/index.html create mode 100644 use-cases/college/index.html create mode 100644 use-cases/courtesy-cards/index.html create mode 100644 use-cases/img/senior-success.gif create mode 100644 use-cases/index.html create mode 100644 use-cases/seniors/index.html create mode 100644 use-cases/students/index.html diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.pages b/.pages new file mode 100644 index 0000000000..0dc50f68f0 --- /dev/null +++ b/.pages @@ -0,0 +1,10 @@ +nav: + - Home: + - README.md + - use-cases + - getting-started + - product-and-design + - development + - tests + - configuration + - deployment diff --git a/404.html b/404.html new file mode 100644 index 0000000000..ee7b4c5d27 --- /dev/null +++ b/404.html @@ -0,0 +1,1154 @@ + + + + + + + + + + + + + + + + + + cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/assets/javascripts/bundle.51d95adb.min.js b/assets/javascripts/bundle.51d95adb.min.js new file mode 100644 index 0000000000..b20ec6835b --- /dev/null +++ b/assets/javascripts/bundle.51d95adb.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Hi=Object.create;var xr=Object.defineProperty;var Pi=Object.getOwnPropertyDescriptor;var $i=Object.getOwnPropertyNames,kt=Object.getOwnPropertySymbols,Ii=Object.getPrototypeOf,Er=Object.prototype.hasOwnProperty,an=Object.prototype.propertyIsEnumerable;var on=(e,t,r)=>t in e?xr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Er.call(t,r)&&on(e,r,t[r]);if(kt)for(var r of kt(t))an.call(t,r)&&on(e,r,t[r]);return e};var sn=(e,t)=>{var r={};for(var n in e)Er.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&kt)for(var n of kt(e))t.indexOf(n)<0&&an.call(e,n)&&(r[n]=e[n]);return r};var Ht=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Fi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of $i(t))!Er.call(e,o)&&o!==r&&xr(e,o,{get:()=>t[o],enumerable:!(n=Pi(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Hi(Ii(e)):{},Fi(t||!e||!e.__esModule?xr(r,"default",{value:e,enumerable:!0}):r,e));var fn=Ht((wr,cn)=>{(function(e,t){typeof wr=="object"&&typeof cn!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(wr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(T){return!!(T&&T!==document&&T.nodeName!=="HTML"&&T.nodeName!=="BODY"&&"classList"in T&&"contains"in T.classList)}function f(T){var Ke=T.type,We=T.tagName;return!!(We==="INPUT"&&a[Ke]&&!T.readOnly||We==="TEXTAREA"&&!T.readOnly||T.isContentEditable)}function c(T){T.classList.contains("focus-visible")||(T.classList.add("focus-visible"),T.setAttribute("data-focus-visible-added",""))}function u(T){T.hasAttribute("data-focus-visible-added")&&(T.classList.remove("focus-visible"),T.removeAttribute("data-focus-visible-added"))}function p(T){T.metaKey||T.altKey||T.ctrlKey||(s(r.activeElement)&&c(r.activeElement),n=!0)}function m(T){n=!1}function d(T){s(T.target)&&(n||f(T.target))&&c(T.target)}function h(T){s(T.target)&&(T.target.classList.contains("focus-visible")||T.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(T.target))}function v(T){document.visibilityState==="hidden"&&(o&&(n=!0),B())}function B(){document.addEventListener("mousemove",z),document.addEventListener("mousedown",z),document.addEventListener("mouseup",z),document.addEventListener("pointermove",z),document.addEventListener("pointerdown",z),document.addEventListener("pointerup",z),document.addEventListener("touchmove",z),document.addEventListener("touchstart",z),document.addEventListener("touchend",z)}function re(){document.removeEventListener("mousemove",z),document.removeEventListener("mousedown",z),document.removeEventListener("mouseup",z),document.removeEventListener("pointermove",z),document.removeEventListener("pointerdown",z),document.removeEventListener("pointerup",z),document.removeEventListener("touchmove",z),document.removeEventListener("touchstart",z),document.removeEventListener("touchend",z)}function z(T){T.target.nodeName&&T.target.nodeName.toLowerCase()==="html"||(n=!1,re())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),B(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var un=Ht(Sr=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},a=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(re,z){d.append(z,re)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(T){throw new Error("URL unable to set base "+c+" due to "+T)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,B=!0,re=this;["append","delete","set"].forEach(function(T){var Ke=h[T];h[T]=function(){Ke.apply(h,arguments),v&&(B=!1,re.search=h.toString(),B=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var z=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==z&&(z=this.search,B&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},a=i.prototype,s=function(f){Object.defineProperty(a,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){s(f)}),Object.defineProperty(a,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(a,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr)});var Qr=Ht((Lt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Lt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Lt=="object"?Lt.ClipboardJS=r():t.ClipboardJS=r()})(Lt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return ki}});var a=i(279),s=i.n(a),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(O){return!1}}var d=function(O){var w=p()(O);return m("cut"),w},h=d;function v(j){var O=document.documentElement.getAttribute("dir")==="rtl",w=document.createElement("textarea");w.style.fontSize="12pt",w.style.border="0",w.style.padding="0",w.style.margin="0",w.style.position="absolute",w.style[O?"right":"left"]="-9999px";var k=window.pageYOffset||document.documentElement.scrollTop;return w.style.top="".concat(k,"px"),w.setAttribute("readonly",""),w.value=j,w}var B=function(O,w){var k=v(O);w.container.appendChild(k);var F=p()(k);return m("copy"),k.remove(),F},re=function(O){var w=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},k="";return typeof O=="string"?k=B(O,w):O instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(O==null?void 0:O.type)?k=B(O.value,w):(k=p()(O),m("copy")),k},z=re;function T(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?T=function(w){return typeof w}:T=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},T(j)}var Ke=function(){var O=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},w=O.action,k=w===void 0?"copy":w,F=O.container,q=O.target,Le=O.text;if(k!=="copy"&&k!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&T(q)==="object"&&q.nodeType===1){if(k==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(k==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Le)return z(Le,{container:F});if(q)return k==="cut"?h(q):z(q,{container:F})},We=Ke;function Ie(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(w){return typeof w}:Ie=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},Ie(j)}function Ti(j,O){if(!(j instanceof O))throw new TypeError("Cannot call a class as a function")}function nn(j,O){for(var w=0;w0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof F.action=="function"?F.action:this.defaultAction,this.target=typeof F.target=="function"?F.target:this.defaultTarget,this.text=typeof F.text=="function"?F.text:this.defaultText,this.container=Ie(F.container)==="object"?F.container:document.body}},{key:"listenClick",value:function(F){var q=this;this.listener=c()(F,"click",function(Le){return q.onClick(Le)})}},{key:"onClick",value:function(F){var q=F.delegateTarget||F.currentTarget,Le=this.action(q)||"copy",Rt=We({action:Le,container:this.container,target:this.target(q),text:this.text(q)});this.emit(Rt?"success":"error",{action:Le,text:Rt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(F){return yr("action",F)}},{key:"defaultTarget",value:function(F){var q=yr("target",F);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(F){return yr("text",F)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(F){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return z(F,q)}},{key:"cut",value:function(F){return h(F)}},{key:"isSupported",value:function(){var F=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof F=="string"?[F]:F,Le=!!document.queryCommandSupported;return q.forEach(function(Rt){Le=Le&&!!document.queryCommandSupported(Rt)}),Le}}]),w}(s()),ki=Ri},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,f){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(f))return s;s=s.parentNode}}n.exports=a},438:function(n,o,i){var a=i(828);function s(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof m=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(n,o,i){var a=i(879),s=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(m))return c(m,d,h);if(a.nodeList(m))return u(m,d,h);if(a.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return s(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),a=f.toString()}return a}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,a,s){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var f=this;function c(){f.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=s.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var is=/["'&<>]/;Jo.exports=as;function as(e){var t=""+e,r=is.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||s(m,d)})})}function s(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof Xe?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){s("next",m)}function u(m){s("throw",m)}function p(m,d){m(d),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof xe=="function"?xe(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,f){a=e[i](a),o(s,f,a.done,a.value)})}}function o(i,a,s,f){Promise.resolve(f).then(function(c){i({value:c,done:s})},a)}}function A(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var $t=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function De(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=xe(a),f=s.next();!f.done;f=s.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(A(u))try{u()}catch(v){i=v instanceof $t?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=xe(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{dn(h)}catch(v){i=i!=null?i:[],v instanceof $t?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new $t(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)dn(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&De(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&De(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Or=Fe.EMPTY;function It(e){return e instanceof Fe||e&&"closed"in e&&A(e.remove)&&A(e.add)&&A(e.unsubscribe)}function dn(e){A(e)?e():e.unsubscribe()}var Ae={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Or:(this.currentObservers=null,s.push(r),new Fe(function(){n.currentObservers=null,De(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new wn(r,n)},t}(U);var wn=function(e){ne(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Or},t}(E);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ne(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,f=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Ut);var On=function(e){ne(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Wt);var we=new On(Tn);var R=new U(function(e){return e.complete()});function Dt(e){return e&&A(e.schedule)}function kr(e){return e[e.length-1]}function Qe(e){return A(kr(e))?e.pop():void 0}function Se(e){return Dt(kr(e))?e.pop():void 0}function Vt(e,t){return typeof kr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function zt(e){return A(e==null?void 0:e.then)}function Nt(e){return A(e[ft])}function qt(e){return Symbol.asyncIterator&&A(e==null?void 0:e[Symbol.asyncIterator])}function Kt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Ki(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qt=Ki();function Yt(e){return A(e==null?void 0:e[Qt])}function Gt(e){return ln(this,arguments,function(){var r,n,o,i;return Pt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,Xe(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,Xe(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,Xe(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Bt(e){return A(e==null?void 0:e.getReader)}function $(e){if(e instanceof U)return e;if(e!=null){if(Nt(e))return Qi(e);if(pt(e))return Yi(e);if(zt(e))return Gi(e);if(qt(e))return _n(e);if(Yt(e))return Bi(e);if(Bt(e))return Ji(e)}throw Kt(e)}function Qi(e){return new U(function(t){var r=e[ft]();if(A(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Yi(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?_(function(o,i){return e(o,i,n)}):me,Oe(1),r?He(t):zn(function(){return new Xt}))}}function Nn(){for(var e=[],t=0;t=2,!0))}function fe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new E}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,f=s===void 0?!0:s;return function(c){var u,p,m,d=0,h=!1,v=!1,B=function(){p==null||p.unsubscribe(),p=void 0},re=function(){B(),u=m=void 0,h=v=!1},z=function(){var T=u;re(),T==null||T.unsubscribe()};return g(function(T,Ke){d++,!v&&!h&&B();var We=m=m!=null?m:r();Ke.add(function(){d--,d===0&&!v&&!h&&(p=jr(z,f))}),We.subscribe(Ke),!u&&d>0&&(u=new et({next:function(Ie){return We.next(Ie)},error:function(Ie){v=!0,B(),p=jr(re,o,Ie),We.error(Ie)},complete:function(){h=!0,B(),p=jr(re,a),We.complete()}}),$(T).subscribe(u))})(c)}}function jr(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function V(e,t=document){let r=se(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function se(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),N(e===_e()),Y())}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function Yn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,we),l(()=>Be(e)),N(Be(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,we),l(()=>rr(e)),N(rr(e)))}var Bn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),xa?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ya.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Jn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Zn=typeof WeakMap!="undefined"?new WeakMap:new Bn,eo=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Ea.getInstance(),n=new Ra(t,r,this);Zn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){eo.prototype[e]=function(){var t;return(t=Zn.get(this))[e].apply(t,arguments)}});var ka=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:eo}(),to=ka;var ro=new E,Ha=I(()=>H(new to(e=>{for(let t of e)ro.next(t)}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){return Ha.pipe(S(t=>t.observe(e)),x(t=>ro.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(()=>de(e)))),N(de(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var no=new E,Pa=I(()=>H(new IntersectionObserver(e=>{for(let t of e)no.next(t)},{threshold:0}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function sr(e){return Pa.pipe(S(t=>t.observe(e)),x(t=>no.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function oo(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=de(e),o=bt(e);return r>=o.height-n.height-t}),Y())}var cr={drawer:V("[data-md-toggle=drawer]"),search:V("[data-md-toggle=search]")};function io(e){return cr[e].checked}function qe(e,t){cr[e].checked!==t&&cr[e].click()}function je(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),N(t.checked))}function $a(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ia(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(N(!1))}function ao(){let e=b(window,"keydown").pipe(_(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:io("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),_(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!$a(n,r)}return!0}),fe());return Ia().pipe(x(t=>t?R:e))}function Me(){return new URL(location.href)}function ot(e){location.href=e.href}function so(){return new E}function co(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)co(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)co(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function fo(){return location.hash.substring(1)}function uo(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Fa(){return b(window,"hashchange").pipe(l(fo),N(fo()),_(e=>e.length>0),J(1))}function po(){return Fa().pipe(l(e=>se(`[id="${e}"]`)),_(e=>typeof e!="undefined"))}function Nr(e){let t=matchMedia(e);return Zt(r=>t.addListener(()=>r(t.matches))).pipe(N(t.matches))}function lo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(N(e.matches))}function qr(e,t){return e.pipe(x(r=>r?t():R))}function ur(e,t={credentials:"same-origin"}){return ve(fetch(`${e}`,t)).pipe(ce(()=>R),x(r=>r.status!==200?Tt(()=>new Error(r.statusText)):H(r)))}function Ue(e,t){return ur(e,t).pipe(x(r=>r.json()),J(1))}function mo(e,t){let r=new DOMParser;return ur(e,t).pipe(x(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),J(1))}function pr(e){let t=M("script",{src:e});return I(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(x(()=>Tt(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),C(()=>document.head.removeChild(t)),Oe(1))))}function ho(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function bo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(ho),N(ho()))}function vo(){return{width:innerWidth,height:innerHeight}}function go(){return b(window,"resize",{passive:!0}).pipe(l(vo),N(vo()))}function yo(){return Q([bo(),go()]).pipe(l(([e,t])=>({offset:e,size:t})),J(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(X("size")),o=Q([n,r]).pipe(l(()=>Be(e)));return Q([r,t,o]).pipe(l(([{height:i},{offset:a,size:s},{x:f,y:c}])=>({offset:{x:a.x-f,y:a.y-c+i},size:s})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(a=>{let s=document.createElement("script");s.src=i,s.onload=a,document.body.appendChild(s)})),Promise.resolve())}var r=class{constructor(n){this.url=n,this.onerror=null,this.onmessage=null,this.onmessageerror=null,this.m=a=>{a.source===this.w&&(a.stopImmediatePropagation(),this.dispatchEvent(new MessageEvent("message",{data:a.data})),this.onmessage&&this.onmessage(a))},this.e=(a,s,f,c,u)=>{if(s===this.url.toString()){let p=new ErrorEvent("error",{message:a,filename:s,lineno:f,colno:c,error:u});this.dispatchEvent(p),this.onerror&&this.onerror(p)}};let o=new EventTarget;this.addEventListener=o.addEventListener.bind(o),this.removeEventListener=o.removeEventListener.bind(o),this.dispatchEvent=o.dispatchEvent.bind(o);let i=document.createElement("iframe");i.width=i.height=i.frameBorder="0",document.body.appendChild(this.iframe=i),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Configuring the Content Security Policy

+
+

MDN docs

+

The Mozilla Developer Network has more on Content Security Policy

+
+
+

The HTTP Content-Security-Policy response header allows web site administrators to control resources the user agent is +allowed to load for a given page.

+

With a few exceptions, policies mostly involve specifying server origins and script endpoints. This helps guard against +cross-site scripting attacks

+
+
+

Strict CSP

+

Benefits configures a Strict Content Security Policy. Read more about Strict CSP from Google: https://csp.withgoogle.com/docs/strict-csp.html.

+
+

django-csp

+
+

django-csp docs

+

Configuring django-csp

+
+

Benefits uses the open source django-csp library for helping to configure the correct response headers.

+

Environment Variables

+

DJANGO_CSP_CONNECT_SRC

+

Comma-separated list of URIs. Configures the connect-src Content Security Policy directive.

+

DJANGO_CSP_FONT_SRC

+

Comma-separated list of URIs. Configures the font-src Content Security Policy directive.

+

DJANGO_CSP_FRAME_SRC

+

Comma-separated list of URIs. Configures the frame-src Content Security Policy directive.

+

DJANGO_CSP_SCRIPT_SRC

+

Comma-separated list of URIs. Configures the script-src Content Security Policy directive.

+

DJANGO_CSP_STYLE_SRC

+

Comma-separated list of URIs. Configures the style-src Content Security Policy directive.

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/configuration/data/index.html b/configuration/data/index.html new file mode 100644 index 0000000000..4487ef14dd --- /dev/null +++ b/configuration/data/index.html @@ -0,0 +1,1310 @@ + + + + + + + + + + + + + + + + + + + + + + + + Configuration data - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Configuration data

+
+

Data migration file

+

benefits/core/migrations/0002_data.py

+
+ +

Introduction

+

Django data migrations are used to load the database with instances of the app’s model classes, defined in benefits/core/models.py.

+

Migrations are run as the application starts up. See the bin/init.sh script.

+

The sample values provided in the repository are sufficient to run the app locally and interact with e.g. the sample Transit +Agencies.

+

During the deployment process, environment-specific values are set in environment variables and are read by the data migration file to build that environment’s configuration database. See the data migration file for the environment variable names.

+

Sample data

+

The sample data included in the repository is enough to bootstrap the application with basic functionality:

+
    +
  • Multiple transit agency configurations
  • +
  • Multiple eligibility verification pathways
  • +
  • With and without authentication required before eligibility verification
  • +
  • In concert with the sample eligibility server, verification with test user data
  • +
+

Not included

+

Some configuration data is not available with the samples in the repository:

+
    +
  • OAuth configuration to enable authentication (read more about OAuth configuration)
  • +
  • reCAPTCHA configuration for user-submitted forms
  • +
  • Payment processor configuration for the enrollment phase
  • +
  • Amplitude configuration for capturing analytics events
  • +
+

Sample transit agency: ABC

+
    +
  • Presents the user a choice between two different eligibility pathways
  • +
  • One eligibility verifier requires authentication
  • +
  • One eligibility verifier does not require authentication
  • +
+

Sample transit agency: DefTL

+
    +
  • Single eligibility pathway, no choice presented to the user
  • +
  • Eligibility verifier does not require authentication
  • +
+

Building the configuration database

+

When the data migration changes, the configuration database needs to be rebuilt.

+

The file is called django.db and the following commands will rebuild it.

+

Run these commands from within the repository root, inside the devcontainer:

+
bin/init.sh
+
+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/configuration/environment-variables/index.html b/configuration/environment-variables/index.html new file mode 100644 index 0000000000..941ec54edb --- /dev/null +++ b/configuration/environment-variables/index.html @@ -0,0 +1,1561 @@ + + + + + + + + + + + + + + + + + + + + + + + + Environment variables - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Environment variables

+

The first steps of the Getting Started guide mention creating an .env file.

+

The sections below outline in more detail the application environment variables that you may want to override, and their purpose. In App Service, this is more generally called the “configuration”.

+

See other topic pages in this section for more specific environment variable configurations.

+

Docker

+

COMPOSE_PROJECT_NAME

+
+

Local configuration

+

This setting only affects the app running on localhost

+
+ +

Name that Docker Compose prefixes to the project for container runs.

+

Amplitude

+ +

ANALYTICS_KEY

+
+

Deployment configuration

+

You may change this setting when deploying the app to a non-localhost domain

+
+

Amplitude API key for the project where the app will direct events.

+

If blank or an invalid key, analytics events aren’t captured (though may still be logged).

+

Django

+

DJANGO_ADMIN

+

Boolean:

+
    +
  • True: activates Django’s built-in admin interface for content authoring.
  • +
  • False (default): skips this activation.
  • +
+

DJANGO_ALLOWED_HOSTS

+
+

Deployment configuration

+

You must change this setting when deploying the app to a non-localhost domain

+
+
+

Django docs

+

Settings: ALLOWS_HOSTS

+
+

A list of strings representing the host/domain names that this Django site can serve.

+

DJANGO_DEBUG

+
+

Deployment configuration

+

Do not enable this in production

+
+
+

Django docs

+

Settings: DEBUG

+
+

Boolean:

+
    +
  • True: the application is launched with debug mode turned on, allows pausing on breakpoints in the code, changes how static + files are served
  • +
  • False (default): the application is launched with debug mode turned off, similar to how it runs in production
  • +
+

DJANGO_LOCAL_PORT

+
+

Local configuration

+

This setting only affects the app running on localhost

+
+

The port used to serve the Django application from the host machine (that is running the application container).

+

i.e. if you are running the app in Docker on your local machine, this is the port that the app will be accessible from at +http://localhost:$DJANGO_LOCAL_PORT

+

From inside the container, the app is always listening on port 8000.

+

DJANGO_LOG_LEVEL

+
+

Deployment configuration

+

You may change this setting when deploying the app to a non-localhost domain

+
+
+

Django docs

+

Settings: LOGGING_CONFIG

+
+

The log level used in the application’s logging configuration.

+

By default the application sends logs to stdout.

+

DJANGO_SECRET_KEY

+
+

Deployment configuration

+

You must change this setting when deploying the app to a non-localhost domain

+
+
+

Django docs

+

Settings: SECRET_KEY

+
+

Django’s primary secret, keep this safe!

+

DJANGO_TRUSTED_ORIGINS

+
+

Deployment configuration

+

You must change this setting when deploying the app to a non-localhost domain

+
+ +

Comma-separated list of hosts which are trusted origins for unsafe requests (e.g. POST)

+

HEALTHCHECK_USER_AGENTS

+
+

Deployment configuration

+

You must change this setting when deploying the app to a non-localhost domain

+
+

Comma-separated list of User-Agent strings which, when present as an HTTP header, should only receive healthcheck responses. Used by our HealthcheckUserAgents middleware.

+

requests configuration

+
+

requests docs

+

Docs for timeouts

+
+

REQUESTS_CONNECT_TIMEOUT

+

The number of seconds requests will wait for the client to establish a connection to a remote machine. Defaults to 3 seconds.

+

REQUESTS_READ_TIMEOUT

+

The number of seconds the client will wait for the server to send a response. Defaults to 1 second.

+

Cypress tests

+
+

Cypress docs

+

CYPRESS_* variables

+
+

CYPRESS_baseUrl

+

The base URL for the (running) application, against which all Cypress .visit() etc. commands are run.

+

When Cypress is running inside the devcontainer, this should be http://localhost:8000. When Cypress is running outside the +devcontainer, check the DJANGO_LOCAL_PORT.

+

Sentry

+

SENTRY_DSN

+
+

Sentry docs

+

Data Source Name (DSN)

+
+

Enables sending events to Sentry.

+

SENTRY_ENVIRONMENT

+
+

Sentry docs

+

environment config value

+
+

Segments errors by which deployment they occur in. This defaults to local, and can be set to match one of the environment names.

+

SENTRY_REPORT_URI

+
+

Sentry docs

+

Security Policy Reporting

+
+

Collect information on Content-Security-Policy (CSP) violations. Read more about CSP configuration in Benefits.

+

To enable report collection, set this env var to the authenticated Sentry endpoint.

+

SENTRY_TRACES_SAMPLE_RATE

+
+

Sentry docs

+

traces_sample_rate

+
+

Control the volume of transactions sent to Sentry. Value must be a float in the range [0.0, 1.0].

+

The default is 0.0 (i.e. no transactions are tracked).

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/configuration/index.html b/configuration/index.html new file mode 100644 index 0000000000..16ce93013b --- /dev/null +++ b/configuration/index.html @@ -0,0 +1,1285 @@ + + + + + + + + + + + + + + + + + + + + + + + + Configuring the Benefits app - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Configuring the Benefits app

+

The Getting Started section and sample configuration values in the repository give enough detail to +run the app locally, but further configuration is required before many of the integrations and features are active.

+

There are two primary components of the application configuration:

+ +

Many (but not all) of the environment variables are read into Django settings during application +startup.

+

The model objects defined in the data migration file are also loaded into and seed Django’s database at application startup time.

+

See the Setting secrets section for how to set secret values for a deployment.

+

Django settings

+
+

Settings file

+

benefits/settings.py

+
+
+

Django docs

+

Django settings

+
+

The Django entrypoint for production runs is defined in benefits/wsgi.py.

+

This file names the module that tells Django which settings file to use:

+
import os
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "benefits.settings")
+
+

Elsewhere, e.g. in manage.py, this same environment variable is set to ensure benefits.settings +are loaded for every app command and run.

+

Using configuration in app code

+ +

From within the application, the Django settings object and the Django models are the two interfaces for application code to +read configuration data.

+

Rather than importing the app’s settings module, Django recommends importing the django.conf.settings object, which provides +an abstraction and better handles default values:

+
from django.config import settings
+
+# ...
+
+if settings.ADMIN:
+    # do something when admin is enabled
+else:
+    # do something else when admin is disabled
+
+

Through the Django model framework, benefits.core.models instances are used to access the configuration data:

+
from benefits.core.models import TransitAgency
+
+agency = TransitAgency.objects.get(short_name="ABC")
+
+if agency.active:
+    # do something when this agency is active
+else:
+    # do something when this agency is inactive
+
+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/configuration/oauth/index.html b/configuration/oauth/index.html new file mode 100644 index 0000000000..2e2b76ba34 --- /dev/null +++ b/configuration/oauth/index.html @@ -0,0 +1,1267 @@ + + + + + + + + + + + + + + + + + + + + + + + + OAuth settings - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

OAuth settings

+

Benefits can be configured to require users to authenticate with an OAuth Open ID Connect (OIDC) +provider, before allowing the user to begin the Eligibility Verification process.

+

This section describes the related settings and how to configure the application to enable this feature.

+

Authlib

+
+

Authlib docs

+

Read more about configuring Authlib for Django

+
+

Benefits uses the open-source Authlib for OAuth and OIDC client implementation. See the Authlib docs +for more details about what features are available. Specifically, from Authlib we:

+
    +
  1. Create an OAuth client using the Django configuration
  2. +
  3. Call client.authorize_redirect() to send the user into the OIDC server’s authentication flow, with our authorization + callback URL
  4. +
  5. Upon the user returning from the OIDC Server with an access token, call client.authorize_access_token() to get a validated + id token from the OIDC server
  6. +
+

Django configuration

+

OAuth settings are configured as instances of the AuthProvider model.

+

The data migration file contains sample values for an AuthProvider configuration. You can set values for a real Open ID Connect provider in environment variables so that they are used instead of the sample values.

+

Django usage

+

The benefits.oauth.client module defines helpers for registering OAuth clients, and creating instances for +use in e.g. views.

+
    +
  • register_providers(oauth_registry) uses data from AuthProvider instances to register clients into the given registry
  • +
  • oauth is an authlib.integrations.django_client.OAuth instance
  • +
+

Providers are registered into this instance once in the OAuthAppConfig.ready() function at application +startup.

+

Consumers call oauth.create_client(client_name) with the name of a previously registered client to obtain an Authlib client +instance.

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/configuration/rate-limit/index.html b/configuration/rate-limit/index.html new file mode 100644 index 0000000000..9ab9d9a770 --- /dev/null +++ b/configuration/rate-limit/index.html @@ -0,0 +1,1265 @@ + + + + + + + + + + + + + + + + + + + + + + + + Configuring Rate Limiting - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Configuring Rate Limiting

+

The benefits application has a simple, single-configuration Rate Limit that acts +per-IP to limit the number of consecutive requests in a given time period, via +nginx limit_req_zone +and limit_req directives.

+

The configured rate limit is 12 requests/minute, or 1 request/5 seconds:

+
limit_req_zone $limit zone=rate_limit:10m rate=12r/m;
+
+

HTTP method selection

+

An NGINX map +variable lists HTTP methods that will be rate limited:

+
map $request_method $limit {
+    default         "";
+    POST            $binary_remote_addr;
+}
+
+

The default means don’t apply a rate limit.

+

To add a new method, add a new line:

+
map $request_method $limit {
+    default         "";
+    OPTIONS         $binary_remote_addr;
+    POST            $binary_remote_addr;
+}
+
+

App path selection

+

The limit_req is applied to an NGINX location block with a case-insensitive regex to match paths:

+
location ~* ^/(eligibility/confirm)$ {
+    limit_req zone=rate_limit;
+    # config...
+}
+
+

To add a new path, add a regex OR | with the new path (omitting the leading slash):

+
location ~* ^/(eligibility/confirm|new/path)$ {
+    limit_req zone=rate_limit;
+    # config...
+}
+
+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/configuration/recaptcha/index.html b/configuration/recaptcha/index.html new file mode 100644 index 0000000000..beca7798e5 --- /dev/null +++ b/configuration/recaptcha/index.html @@ -0,0 +1,1283 @@ + + + + + + + + + + + + + + + + + + + + + + + + Configuring reCAPTCHA - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Configuring reCAPTCHA

+
+

reCAPTCHA docs

+

See the reCAPTCHA Developer’s Guide for more information

+
+

reCAPTCHA v3 is a free Google-provided service that helps protect the app from spam and abuse by using advanced +risk analysis techniques to tell humans and bots apart.

+

reCAPTCHA is applied to all forms in the Benefits app that collect user-provided information. Version 3 works silently in the +background, with no additional interaction required by the user.

+

Environment variables

+
+

Warning

+

The following environment variables are all required to activate the reCAPTCHA feature

+
+

DJANGO_RECAPTCHA_API_URL

+

URL to the reCAPTCHA JavaScript API library.

+

By default, https://www.google.com/recaptcha/api.js

+

DJANGO_RECAPTCHA_SITE_KEY

+

Site key for the reCAPTCHA configuration.

+

DJANGO_RECAPTCHA_SECRET_KEY

+

Secret key for the reCAPTCHA configuration.

+

DJANGO_RECAPTCHA_VERIFY_URL

+ +

URL for the reCAPTCHA verify service.

+

By default, https://www.google.com/recaptcha/api/siteverify

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/configuration/transit-agency/index.html b/configuration/transit-agency/index.html new file mode 100644 index 0000000000..e5d7ccc7ea --- /dev/null +++ b/configuration/transit-agency/index.html @@ -0,0 +1,1347 @@ + + + + + + + + + + + + + + + + + + + + + + + + Configuring a new transit agency - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Configuring a new transit agency

+

Before starting any configuration, the Cal-ITP team and transit agency staff should have a kickoff meeting to confirm that information provided is complete, implementation plan is feasible, and any approvals needed have been obtained.

+

Then, the following steps are done by the Cal-ITP team to configure a new transit agency in the Benefits application.

+

Note that a TransitAgency model requires:

+
    +
  • a list of supported EligibilityTypes
  • +
  • a list of EligibilityVerifiers used to verify one of those supported eligibility types
  • +
  • a PaymentProcessor for enrolling the user’s contactless card for discounts
  • +
  • an info_url and phone for users to contact customer service
  • +
  • an SVG or PNG file of the transit agency’s logo
  • +
+

Also note that these steps assume the transit agency is using Littlepay as their payment processor. Support for integration with other payment processors may be added in the future.

+

Configuration for development and testing

+

For development and testing, only a Littlepay customer group is needed since there is no need to interact with any discount product. (We don’t have a way to tap a card against the QA system to trigger a discount and therefore have no reason to associate the group with any product.)

+

Steps

+
    +
  1. Cal-ITP uses the transit agency’s Littlepay merchant ID to create a customer group in the Littlepay QA environment for each type of eligibility (e.g. senior).
  2. +
  3. For each group that’s created, a group ID will be returned and should be set as the group_id on a new EligibilityType in the Benefits database. (See Configuration data for more on loading the database.)
  4. +
  5. Cal-ITP creates a new EligibilityVerifier in the database for each supported eligibility type. This will require configuration for either API-based verification or verification through an OAuth Open ID Connect provider (e.g. sandbox Login.gov) – either way, this resource should be meant for testing.
  6. +
  7. Cal-ITP creates a new TransitAgency in the database and associates it with the new EligibilityTypes and EligibilityVerifiers as well as the existing Littlepay PaymentProcessor.
  8. +
+

Configuration for production validation

+

For production validation, both a customer group and discount product are needed. The customer group used here is a temporary one for testing only. Production validation is done against the Benefits test environment to avoid disruption of the production environment.

+

Steps

+
    +
  1. Transit agency staff creates the discount product in production Littlepay (if it does not already exist).
  2. +
  3. Transit agency staff takes a screenshot of the discount product in the Merchant Portal, making sure the browser URL is visible, and sends that to Cal-ITP.
  4. +
  5. Cal-ITP creates a customer group for testing purposes in production Littlepay.
  6. +
  7. Cal-ITP associates the group with the product.
  8. +
  9. Cal-ITP creates a new EligibilityType for testing purposes in the Benefits database and sets the group_id to the ID of the newly-created group.
  10. +
  11. Cal-ITP creates a new EligibilityVerifier with configuration for a testing environment to ensure successful eligibility verification. (For example, use sandbox Login.gov instead of production Login.gov.)
  12. +
  13. Cal-ITP creates a new PaymentProcessor for testing purposes with configuration for production Littlepay.
  14. +
  15. Cal-ITP updates the existing TransitAgency (created previously) with associations to the eligibility types, verifiers, and payment processor that were just created for testing.
  16. +
+

At this point, Cal-ITP and transit agency staff can coordinate to do on-the-ground testing where a live card is tapped on a live payment validator.

+

Production validation testing

+
    +
  1. Transit agency staff (or Cal-ITP staff) does live test in the field.
  2. +
  3. Transit agency staff uses the Merchant Portal to verify the taps and discounts were successful.
  4. +
  5. Cal-ITP uses logs from Azure to verify the user was associated to the customer group.
  6. +
  7. Cal-ITP verifies that Amplitude analytic events are being sent.
  8. +
+

Configuration for production

+

Once production validation is done, the transit agency can be added to the production Benefits database.

+

Steps

+
    +
  1. Cal-ITP creates a customer group for production use in production Littlepay.
  2. +
  3. Cal-ITP associates the group with the discount product created previously during production validation.
  4. +
  5. Cal-ITP sets that group’s ID as the group_id for a new EligibilityType in the Benefits database.
  6. +
  7. Cal-ITP creates a new EligibilityVerifier with configuration for the production eligibility verification system.
  8. +
  9. Cal-ITP creates a new TransitAgency in the database with proper associations to eligibility types, verifiers, and payment processor.
  10. +
+

Cleanup

+

At this point, the customer group that was created in production Littlepay for testing purposes can be deleted. The temporary production validation objects in the Benefits database can also be deleted.

+
    +
  1. Remove the association between the test customer group and discount product.
  2. +
  3. Delete the test customer group.
  4. +
  5. Remove temporary EligibilityTypes, EligibilityVerifiers, and PaymentProcessor that were created in the Benefits test environment.
  6. +
+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/deployment/.pages b/deployment/.pages new file mode 100644 index 0000000000..464f84d634 --- /dev/null +++ b/deployment/.pages @@ -0,0 +1,7 @@ +nav: + - README.md + - infrastructure.md + - secrets.md + - release.md + - troubleshooting.md + - workflows.md diff --git a/deployment/azure/index.html b/deployment/azure/index.html new file mode 100644 index 0000000000..d57cb5ce16 --- /dev/null +++ b/deployment/azure/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/deployment/index.html b/deployment/index.html new file mode 100644 index 0000000000..665a97d5e5 --- /dev/null +++ b/deployment/index.html @@ -0,0 +1,1257 @@ + + + + + + + + + + + + + + + + + + + + + + + + Overview - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Overview

+

dev-benefits.calitp.org is currently deployed into a Microsoft Azure account provided by California Department of Technology (CDT)’s Office of Enterprise Technology (OET), a.k.a. the “DevSecOps” team. More specifically, it uses custom containers on Azure App Service. More about the infrastructure.

+

Deployment process

+

The Django application gets built into a Docker image with NGINX and +Gunicorn. SQLite is used within that same container to store configuration data; there is no external database.

+

The application is deployed to an Azure Web App Container using three separate environments for dev, test, +and prod.

+

A GitHub Action per environment is responsible for building that branch’s image and pushing to GitHub Container +Registry (GHCR).

+

GitHub POSTs a webhook to the Azure Web App when an image is published to GHCR, telling +Azure to restart the app and pull the latest image.

+

You can view what Git commit is deployed for a given environment by visitng the URL path /static/sha.txt.

+

Configuration

+

Configuration settings are stored as Application Configuration variables in Azure. +Data is loaded via Django data migrations.

+

Docker images

+

Docker images for each of the deploy branches are available from GitHub Container Registry (GHCR):

+ + + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/deployment/infrastructure/index.html b/deployment/infrastructure/index.html new file mode 100644 index 0000000000..3797a4dff1 --- /dev/null +++ b/deployment/infrastructure/index.html @@ -0,0 +1,1531 @@ + + + + + + + + + + + + + + + + + + + + + + + + Infrastructure - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Infrastructure

+

The infrastructure is configured as code via Terraform, for various reasons.

+

Architecture

+

System interconnections

+
flowchart LR
+    benefits[Benefits application]
+    style benefits stroke-width:5px
+    recaptcha[Google reCAPTCHA]
+    rider((User's browser))
+    idg[Identity Gateway]
+    elig_server[Eligibility Server]
+    ac_data[(Agency Card data)]
+    cookies[(Cookies)]
+
+    benefits -->|Errors| sentry
+    elig_server -->|Errors| sentry
+
+    rider --> benefits
+    rider -->|Credentials and identity proofing| Login.gov
+    rider --> recaptcha
+    rider -->|Payment card info| Littlepay
+    rider -->|Events| Amplitude
+    rider -->|Session| cookies
+
+    benefits --> idg
+    benefits <--> recaptcha
+    benefits -->|Events| Amplitude
+    benefits -->|Group enrollment| Littlepay
+    benefits --> elig_server
+
+    subgraph "Agency Cards (e.g. MST Courtesy Cards)"
+    elig_server --> ac_data
+    end
+
+    idg --> Login.gov
+    Login.gov -->|User attributes| idg
+    idg -->|User attributes| benefits
+

Benefits application

+
flowchart LR
+    internet[Public internet]
+    frontdoor[Front Door]
+    django[Django application]
+    interconnections[Other system interconnections]
+
+    internet --> Cloudflare
+    Cloudflare --> frontdoor
+    django <--> interconnections
+
+    subgraph Azure
+        frontdoor --> NGINX
+
+        subgraph App Service
+            subgraph Custom container
+                direction TB
+                NGINX --> django
+            end
+        end
+    end
+

Front Door also includes the Web Application Firewall (WAF) and handles TLS termination. Front Door is managed by the DevSecOps team.

+

Ownership

+

The following things in Azure are managed by the California Department of Technology (CDT)’s DevSecOps (OET) team:

+
    +
  • Subcriptions
  • +
  • Resource Groups
  • +
  • Networking
  • +
  • Front Door
  • +
  • Web Application Firewall (WAF)
  • +
  • Distributed denial-of-service (DDoS) protection
  • +
  • IAM
  • +
  • Service connections
  • +
+

Environments

+

Within the CDT Digital CA directory (how to switch), there are two Subscriptions, with Resource Groups under each. Each environment corresponds to a single Resource Group, Terraform Workspace, and branch.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EnvironmentSubscriptionResource GroupWorkspaceBranch
DevCDT/ODI DevelopmentRG-CDT-PUB-VIP-CALITP-D-001devdev
TestCDT/ODI DevelopmentRG-CDT-PUB-VIP-CALITP-T-001testtest
ProdCDT/ODI ProductionRG-CDT-PUB-VIP-CALITP-P-001defaultprod
+

All resources in these Resource Groups should be reflected in Terraform in this repository. The exceptions are:

+ +

You’ll see these referenced in Terraform as data sources.

+

For browsing the Azure portal, you can switch your Default subscription filter.

+

Making changes

+

Build Status

+

Terraform is plan‘d when code is pushed to any branch on GitHub, then apply‘d when merged to dev. While other automation for this project is done through GitHub Actions, we use an Azure Pipeline (above) for a couple of reasons:

+
    +
  • Easier authentication with the Azure API using a service connnection
  • +
  • Log output is hidden, avoiding accidentally leaking secrets
  • +
+

Local development

+
    +
  1. Get access to the Azure account through the DevSecOps team.
  2. +
  3. +

    Install dependencies:

    +
  4. +
  5. +

    Azure CLI

    +
  6. +
  7. +

    Terraform - see exact version in deploy.yml

    +
  8. +
  9. +

    Authenticate using the Azure CLI.

    +
  10. +
+
az login
+
+
    +
  1. Outside the dev container, navigate to the terraform/ directory.
  2. +
  3. Initialize Terraform. You can also use this script later to switch between environments.
  4. +
+
./init.sh <env>
+
+
    +
  1. Make changes to Terraform files.
  2. +
  3. Preview the changes, as necessary.
  4. +
+
terraform plan
+
+
    +
  1. Submit the changes via pull request.
  2. +
+

For Azure resources, you need to ignore changes to tags, since they are automatically created by Azure Policy.

+
lifecycle {
+  ignore_changes = [tags]
+}
+
+

Naming conventions

+

The DevSecOps team sets the following naming convention for Resources:

+
<<Resource Type>>-<<Department>>-<<Public/Private>>-<<Project Category>>-<<Project Name>>-<<Region>><<OS Type>>-<<Environment>>-<<Sequence Number>>
+
+

Sample Names

+
    +
  • RG-CDT-PUB-VIP-BNSCN-E-D-001
  • +
  • ASP-CDT-PUB-VIP-BNSCN-EL-P-001
  • +
  • AS-CDT-PUB-VIP-BNSCN-EL-D-001
  • +
+

Resource Types

+

Use the following shorthand for conveying the Resource Type as part of the Resource Name:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ResourceConvention
App ServiceAS
App Service PlanASP
Virtual NetworkVNET
Resource GroupRG
Virtual MachineVM
DatabaseDB
SubnetSNET
Front DoorFD
+

Azure environment setup

+

The following steps are required to set up the environment, with linked issues to automate them:

+ +

This is not a complete step-by-step guide; more a list of things to remember. This may be useful as part of incident response.

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/deployment/release/index.html b/deployment/release/index.html new file mode 100644 index 0000000000..e98d245149 --- /dev/null +++ b/deployment/release/index.html @@ -0,0 +1,1330 @@ + + + + + + + + + + + + + + + + + + + + + + + + Making a release - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Making a release

+

This list outlines the manual steps needed to make a new release of the +benefits app.

+

A release is made by merging changes into the prod branch, which kicks off a +deployment to the production environment. More details on the deployment steps +can be found under Workflows.

+

The list of releases can be found on the repository Releases page +on GitHub.

+

Start a new Release on Github

+

0. Decide on the new version number

+

A new release implies a new version.

+

benefits uses the CalVer versioning scheme, where +version numbers look like: YYYY.0M.R

+
    +
  • YYYY is the 4-digit year of the release; e.g. 2021, 2022
  • +
  • 0M is the 2-digit, 0-padded month of the release; e.g. 02 is February, 12 + is December.
  • +
  • R is the 1-based release counter for the given year and month; + e.g. 1 for the first release of the month, 2 for the second, and so on.
  • +
+

1. Prepare release in a branch

+

Typically changes for a release will move from dev, to test, to prod. This +assumes dev is in a state that it can be deployed without disruption. (This is called a Regular release.)

+

If dev or test contain in-progress work that is not ready for production, +and a hotfix is needed in production, a separate process to test the changes +before deploying to prod must be undertaken. (This is called a Hotfix release.)

+

As implied in the previous step, all releases follow the same version number format.

+

The following diagram shows how a release should propagate to prod under +different circumstances:

+
graph LR
+    A(Release branch) --> B{Are dev and test ready to deploy?};
+    B -->|Yes| C(dev);
+    C --> D(test);
+    D --> E(prod);
+    B -->|No| E;
+

By convention the release branch is called release/YYYY.0M.R using the +upcoming version number.

+

2. Bump the application version number

+

The app code maintains a version number in +benefits/__init__.py, +used by the instrumentation and logging systems.

+

This version number must be updated to match the new version in the same format: +YYYY.0M.R

+

3. Open a PR

+

Initially from the release branch to the target environment branch, following +the merge sequence in the diagram above.

+

4. Merge the PR

+

After checks pass and review approval is given, merge the PR to kick off the +deployment.

+

Repeat steps 3 and 4 for each deployment environment target, again following the +merge sequence in the diagram above.

+

5. Tag the release

+

Once the deploy has completed to prod, the version can be tagged and pushed to +GitHub.

+

From a local terminal:

+
git fetch
+
+git checkout prod
+
+git reset --hard origin/prod
+
+git tag YYYY.0M.R
+
+git push origin YYYY.0M.R
+
+

6. Generate release notes

+

Also add a written description, and include screenshots/animations of new/updated pages/workflows.

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/deployment/secrets/index.html b/deployment/secrets/index.html new file mode 100644 index 0000000000..a7580e6382 --- /dev/null +++ b/deployment/secrets/index.html @@ -0,0 +1,1250 @@ + + + + + + + + + + + + + + + + + + + + + + + + Setting secrets - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Setting secrets

+

Secret values used by the Benefits application (such as API keys, private keys, certificates, etc.) are stored in an Azure Key Vault for each environment.

+

To set a secret, you can use the Azure portal or the Azure CLI.

+

There are helper scripts under terraform/secrets which build up the Azure CLI command, given some inputs. The usage is as follows:

+

First, make sure you are set up for local development and that you are in the terraform/secrets directory.

+
cd terraform/secrets
+
+

To set a secret by providing a value:

+
./value.sh <environment_letter> <secret_name> <secret_value>
+
+

where environment_letter is D for development, T for test, and P for production.

+

To set a secret by providing the path of a file containing the secret (useful for multi-line secrets):

+
./file.sh <environment_letter> <secret_name> <file_path>
+
+

To verify the value of a secret, you can use the helper script named read.sh.

+
./read.sh <environment_letter> <secret_name>
+
+

Refreshing secrets

+

To make sure the Benefits application uses the latest secret values in Key Vault, you will need to make a change to the app service’s configuration. If you don’t do this step, the application will instead use cached values, which may not be what you expect. See the Azure docs for more details.

+

The steps are:

+
    +
  1. After setting new secret values, go to the App Service configuration in Azure Portal, and change the value of the setting named change_me_to_refresh_secrets.
  2. +
  3. Save your changes.
  4. +
+

The effects of following those steps should be:

+
    +
  • A restart of the App Service is triggered.
  • +
  • The next time that our Azure infrastructure pipeline is run, the value of change_me_to_refresh_secrets is set back to the value defined in our Terraform file for the App Service resource.
  • +
+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/deployment/troubleshooting/index.html b/deployment/troubleshooting/index.html new file mode 100644 index 0000000000..af170c6446 --- /dev/null +++ b/deployment/troubleshooting/index.html @@ -0,0 +1,1381 @@ + + + + + + + + + + + + + + + + + + + + + + + + Troubleshooting - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Troubleshooting

+

Tools

+

Monitoring

+

We have ping tests set up to notify about availability of each environment. Alerts go to #benefits-notify.

+

Logs

+

Azure App Service Logs

+

Open the Logs for the environment you are interested in. The following tables are likely of interest:

+
    +
  • AppServiceConsoleLogs: stdout and stderr coming from the container
  • +
  • AppServiceHTTPLogs: requests coming through App Service
  • +
  • AppServicePlatformLogs: deployment information
  • +
+

For some pre-defined queries, click Queries, then Group by: Query type, and look under Query pack queries.

+

Live tail

+

After setting up the Azure CLI, you can use the following command to stream live logs:

+
az webapp log tail --resource-group RG-CDT-PUB-VIP-CALITP-P-001 --name AS-CDT-PUB-VIP-CALITP-P-001 2>&1 | grep -v /healthcheck
+
+

SCM

+

https://as-cdt-pub-vip-calitp-p-001-dev.scm.azurewebsites.net/api/logs/docker

+

Sentry

+

Cal-ITP’s Sentry instance collects both errors (“Issues”) and app performance info.

+

Alerts are sent to #benefits-notify in Slack. Others can be configured.

+

You can troubleshoot Sentry itself by turning on debug mode and visiting /error/.

+

Specific issues

+

This section serves as the runbook for Benefits.

+

Terraform lock

+

General info

+

If Terraform commands fail (locally or in the Pipeline) due to an Error acquiring the state lock:

+
    +
  1. Check the Lock Info for the Created timestamp. If it’s in the past ten minutes or so, that probably means Terraform is still running elsewhere, and you should wait (stop here).
  2. +
  3. Are any Pipeline runs stuck? If so, cancel that build, and try re-running the Terraform command.
  4. +
  5. Do any engineers have a Terrafrom command running locally? You’ll need to ask them. For example: They may have started an apply and it’s sitting waiting for them to approve it. They will need to (gracefully) exit for the lock to be released.
  6. +
  7. If none of the steps above identified the source of the lock, and especially if the Created time is more than ten minutes ago, that probably means the last Terraform command didn’t release the lock. You’ll need to grab the ID from the Lock Info output and force unlock.
  8. +
+

App fails to start

+

If the container fails to start, you should see a downtime alert. Assuming this app version was working in another environment, the issue is likely due to misconfiguration. Some things you can do:

+ +

Littlepay API issue

+

Littlepay API issues may show up as:

+
    +
  • The monitor failing
  • +
  • The Connect your card button doesn’t work
  • +
+

A common problem that causes Littlepay API failures is that the certificate expired. To resolve:

+
    +
  1. Reach out to support@littlepay.com
  2. +
  3. Receive a new certificate
  4. +
  5. Put that certificate into the configuration data and/or the GitHub Actions secrets
  6. +
+

Eligibility Server

+

If the Benefits application gets a 403 error when trying to make API calls to the Eligibility Server, it may be because the outbound IP addresses changed, and the Eligibility Server firewall is still restricting access to the old IP ranges.

+
    +
  1. Grab the outbound_ip_ranges output values from the most recent Benefit deployment to the relevant environment.
  2. +
  3. Update the IP ranges
  4. +
  5. Go to the Eligibility Server Pipeline
  6. +
  7. Click Edit
  8. +
  9. Click Variables
  10. +
  11. Update the relevant variable with the new list of CIDRs
  12. +
+

Note there is nightly downtime as the Eligibility Server restarts and loads new data.

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/deployment/workflows/index.html b/deployment/workflows/index.html new file mode 100644 index 0000000000..dfea7482c5 --- /dev/null +++ b/deployment/workflows/index.html @@ -0,0 +1,1272 @@ + + + + + + + + + + + + + + + + + + + + + + Workflows - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Workflows

+

The GitHub Actions deployment workflow configuration lives at .github/workflows/deploy.yml.

+
+

Info

+

The entire process from GitHub commit to full redeploy of the application can take from around 5 minutes to 10 minutes +or more depending on the deploy environment. Have patience!

+
+

Deployment steps

+

The workflow is triggered with a push to the corresponding branch. It also responds to the workflow_dispatch event to allow manually triggering via the GitHub Actions UI.

+

When a deployment workflow runs, the following steps are taken:

+

1. Checkout code

+

From the tip of the corresponding branch (e.g. dev)

+

2. Authenticate to GHCR

+

Using the github.actor and built-in GITHUB_TOKEN secret

+

3. Build and push image to GitHub Container Registry (GHCR)

+

Build the root Dockerfile, tagging with both the branch name (e.g. dev) and the SHA from the HEAD commit.

+

Push this image:tag into GHCR.

+

4. App Service deploy

+

Each Azure App Service instance is configured to listen to a webhook from GitHub, then deploy the image.

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/development/.pages b/development/.pages new file mode 100644 index 0000000000..8b2801411f --- /dev/null +++ b/development/.pages @@ -0,0 +1,8 @@ +nav: + - README.md + - commits-branches-merging.md + - docker-dynamic-ports.md + - linting-pre-commit.md + - models-migrations.md + - i18n.md + - test-server.md diff --git a/development/commits-branches-merging/index.html b/development/commits-branches-merging/index.html new file mode 100644 index 0000000000..fa4794a544 --- /dev/null +++ b/development/commits-branches-merging/index.html @@ -0,0 +1,1315 @@ + + + + + + + + + + + + + + + + + + + + + + + + Commits, branches, and merging - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Commits, branches, and merging

+

Commits

+

This project enforces the Conventional Commits style for commit message formatting:

+
<type>[(optional-scope)]: <description>
+
+[optional body]
+
+

Where <type> indicates the nature of the commit, one of a list of possible values:

+
    +
  • build - related to the build or compile process
  • +
  • chore - administrative tasks, cleanups, dev environment
  • +
  • ci - related to automated builds/tests etc.
  • +
  • docs - updates to the documentation
  • +
  • feat - new code, features, or interfaces
  • +
  • fix - bug fixes
  • +
  • perf - performance improvements
  • +
  • refactor - non-breaking logic refactors
  • +
  • revert - undo a prior change
  • +
  • style - code style and formatting
  • +
  • test - having to do with testing of any kind
  • +
+

E.g.

+
git commit -m "feat(eligibility/urls): add path for start"
+
+

Branches

+

The default GitHub branch is dev. All new feature work should be in the form of Pull Requests (PR) that target dev as their +base.

+

In addition to dev, the repository has three other long-lived branches:

+
    +
  • test and prod correspond to the Test and Production deploy environments, respectively.
  • +
  • gh-pages hosts the compiled documentation, and is always forced-pushed by the + docs build process.
  • +
+

Protection rules

+

Branch protection rules are in place on three environment branches (dev, test, prod) to:

+
    +
  • Prevent branch deletion
  • +
  • Restrict force-pushing, where appropriate
  • +
  • Require passing status checks before merging into the target branch is allowed
  • +
+

PR branches

+

PR branches are typically named with a conventional type prefix, a slash /, and then descriptor in lower-dashed-case:

+
<type>/<lower-dashed-descriptor>
+
+

E.g.

+
git checkout -b feat/verifier-radio-buttons
+
+

and

+
git checkout -b refactor/verifier-model
+
+

PR branches are deleted once their PR is merged.

+

Merging

+

Merging of PRs should be done using the merge commit strategy. The PR author should utilize git rebase -i to ensure +their PR commit history is clean, logical, and free of typos.

+

When merging a PR into dev, it is customary to format the merge commit message like:

+
Title of PR (#number)
+
+

instead of the default:

+
Merge pull request #number from source-repo/source-branch
+
+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/development/docker-dynamic-ports/index.html b/development/docker-dynamic-ports/index.html new file mode 100644 index 0000000000..53e314fb8f --- /dev/null +++ b/development/docker-dynamic-ports/index.html @@ -0,0 +1,1291 @@ + + + + + + + + + + + + + + + + + + + + + + + + Docker dynamic ports - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Docker dynamic ports

+

Docker dynamically assigns host machine ports that map into container application ports.

+

Inside the Devcontainer

+
+

Info

+

The Devcontainer can bind to a single container’s port(s) and present those to your localhost machine via VS Code. +Other services started along with the Devcontainer are not visible in VS Code. See +Outside the Devconatiner for how to find information on those.

+
+

Once started with F5, the benefits Django application runs on port 8000 inside the Devcontainer. To find the localhost +address, look on the PORTS tab in VS Code’s Terminal window. The Local Address corresponding to the record where +8000 is in the Port column is where the site is accessible on your host machine.

+

Replace 0.0.0.0 with localhost and use the same port number shown in the Local Address column. This is highlighted by the +red box in the image below:

+

Screenshot showing a Local Address for a container application

+

Outside the Devcontainer

+

When running a docker compose ... command, or in other scenarios outside of the Devcontainer, there are multiple ways to find +the http://localhost port corresponding to the service in question.

+

Docker Desktop

+

The Docker Desktop application shows information about running containers and services/groups, including information about +bound ports. In most cases, the application provides a button to launch a container/service directly in your browser when a +port binding is available.

+

In the Containers / Apps tab, expand the service group if needed to find the container in question, where you should see +labels indicating the container is RUNNING and bound to PORT: XYZ.

+

Hover over the container in question, and click the Open in Browser button to launch the app in your web browser.

+

Screenshot showing the "Open in Browser" button for a service in Docker Desktop

+

Docker CLI commands

+

Using the docker command line interface, you can find the bound port(s) of running containers.

+
docker ps -f name=<service>
+
+

e.g. for the docs service:

+
docker ps -f name=docs
+
+

This prints output like the following:

+
CONTAINER ID   IMAGE                 COMMAND                  CREATED         STATUS         PORTS                     NAMES
+0d5b2e1fb910   benefits_client:dev   "mkdocs serve --dev-…"   2 minutes ago   Up 2 minutes   0.0.0.0:62093->8000/tcp   benefits_docs_1
+
+

Looking at the PORTS column:

+
PORTS
+0.0.0.0:62093->8000/tcp
+
+

We can see that locally, port 62093 is bound to the container port 8000.

+

In this case, entering http://localhost:62093 in the web browser navigates to the docs site homepage.

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/development/i18n/index.html b/development/i18n/index.html new file mode 100644 index 0000000000..b679b4cbd6 --- /dev/null +++ b/development/i18n/index.html @@ -0,0 +1,1293 @@ + + + + + + + + + + + + + + + + + + + + + + + + Django message files - cal-itp/benefits: documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Django message files

+ +
+

Message files

+

English messages: benefits/locale/en/LC_MESSAGES/django.po

+
+

The Cal-ITP Benefits application is fully internationalized and available in both English and Spanish.

+

It uses Django’s built-in support for translation using message files, which contain entries of msgid/msgstr pairs. The msgid is referenced in source code so that Django takes care of showing the msgstr for the user’s language.

+

Updating message files

+

Django has a utility command called makemessages to help maintain message files. It ensures that msgids in the message files are actually used somewhere in source code and also detects new msgids.

+

There is a helper script that runs this command with some arguments: bin/makemessages.sh

+
bin/makemessages.sh
+
+

Developers should use this script to update message files in a consistent way.

+

Workflow

+

Updating English

+

Add English copy to templates directly first. Then, run the helper script, bin/makemessages.sh, so Django can update the django.po files for English and Spanish with the new copy.

+

Updating Spanish

+

Find the English copy in the Spanish django.po file as a msgid, and add the corresponding Spanish translation as the msgstr. Again, run the helper script for formatting and bin/init.sh to confirm the translation is rendered properly.

+

Adding agency-specific copy

+

When templates have different copy per agency, create a new template for that agency-specific copy to live in. See the example of the MST-specific agency index page file, named index--mst.html. Include the agency-specific template file name in the migration object, as done here for MST, with eligibility_index_template="eligibility/index--mst.html".

+

Fuzzy strings

+

From Django docs:

+
+

makemessages sometimes generates translation entries marked as fuzzy, e.g. when translations are inferred from previously translated strings.

+
+

Usually, the inferred translation is not correct, so make sure to review the msgstr and fix it if necessary. Then, remove the commented lines starting with #, fuzzy (otherwise the entry will not be used).

+ + + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/development/img/docker-desktop-open-in-browser.png b/development/img/docker-desktop-open-in-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..d188e491b93bb97ff925a0a31d02e814ff32257a GIT binary patch literal 18146 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 Us3pTeUy!psUHx3vIVCg!0AXBjOaK4? literal 0 HcmV?d00001 diff --git a/development/img/ports-local-address.png b/development/img/ports-local-address.png new file mode 100644 index 0000000000000000000000000000000000000000..13636e91f100be4b1386cfdcc6eaca18e8f45837 GIT binary patch literal 116329 zcmdS9^;aCt^9D)??yifw1a}DTZXrl;g1auZu(-Ph3l0e$+s>ZWbD>SHMIgLxY2Z!&Fj~)rNz64}^nz*Mx%f@6FFhJEHw3o52Itk zTFGOxD;>v}KIbppE_a(x&r|1t{#^8MA$TmK9gMm`xi-PsHAhEBg1PgbjJ@(y8Q0jT z{{z?4T@e)(6_o$t%Tw#r+8R0hf3YN96B`v3{9p7oB_2ONKmUh}n)-iq{{H^|WkdNN z-Pjn~f7#&w(aqcP;rth^w>0uFg#W-BHLMr^KVK{};J|B0{(msbltdAG_x}NG3{Ug_ zQvxuiEgu8$mbAKk2&SN<&dmPy(v!@y#3S>XvbH5^=+{ zC84#6Y&X-BJ?L8mI5_w1GXIkWxRwH=Bw3=mx_TQ5g=1o35)u_1SX?A@c6N64@#%qk zE$BJeqM)EKx3NLaQ*Q~^JoBXdSL-Ll`fb6!sjj|0Il%K*kaI+z;7a6!(5)RuOIv>4 z6y3j{s57EL$PW(gUBXhm&*E#)zkFe)L$IOg^i;Ei-9W50QAclvJ&l&u7 za6T|FfJ;Jx!pX_01atG<&iSV^rV1!vCTeO}6I`ko=-YJ2#L$o&o*nyQULYzD4^N3@N@^;D^r%JB5M`uAtbC_V_*U&O)6 zs(dL3nn_7Xp?zR}veeW2jaWB*d3RE9GX9(tK{X5uC8q=*Jhqz1y!=mTJ1G87{wIE1 zzEb~^A?&MJfJ-KPS){i>VMp;{N!ILuh_Eolu(Wb#vMHvp{NUd7KA2gU z!lJKLuEI7@HUW|RX~mjq<0LVsenXJ-nJWU37X2gTEPnCn|GG*1f8DJ6*=IGyWlszM zu%k9KHu6PdI5Q@$MenFD2jmvA2TjYSDZu;cCMlrb=UOyMNT_B{S5Q_a5c9s3-v&<}hr^hd+^Y&q-n78-MIFO_5Ggk%A8V zd=2?y6~zl+YX1~y1LOZJ&zm8u^P*ApA2CDf)|Pp{O|NnrH;x1r!Y(!xw>8e& zXz9Q9Z$Qqt?hz$0wj*i9zOF)5OiP2vj(8VFvCZ{WV-WTKO#VNY2QnLMJw-oxXg z@;tw<-(Pj7fA)dXn(;sV?KLI;hIslc1A9A8yhUy&@}fr2EI7Ngj@^E=uT@)PRQq^ZiXQ-qWx4^V{JrM&lb^mL`N z`hRM E^ThUB0d^-7mU7?y?{DVs?_T$sq=kPJK?xeiI2$?;n+U_JnEcl#fr+ZSfA8KMx-tNJMH2b6*2k zD^1g`J7QjY#Olko*xRsb3dAKM!;X%B-?e#~ygILO6H9q%Z3Kw57^^uou3xlnpjSsc z_DZ8RNn?e7!Fo{FWG>V+P=5KPW9m*}*g#binwV+K%(fOBGWM%_u3j{4scwT6^J}wI z8FJ?5i+aOmwgw&X%(&xC&l7d%r8`5~iG~P8o`woY+PmfAX{gy*QVEiR`OA{{?t?h7DBKlTj_i-I2sgX3&jlppxnj*gid2rlDtY&6$-YL=cp=AazEkU!u0HhsDDVFp4P zJH4&8#FJ$MvhK|2eSzFLFufI?w-zvvx2krgn^lDRVx-f6TpqIBuC$HTXI}+gx=H<< z%M$kse5xXvhQF>AwScFao5n49%Qq{XZz5`1OV|&Zr&~F{BwWq&v%s!pGbsGogxvnR zkT3;x^*Mp995Ei_T}Fo8GGrM!gfkL(b7K<^a#X*T>EFfE;IZzq_rm;az8Y~Y zCv1zmL3MtvfH3!0Zm%JVceAdXZ5V4=o*FE+mw`!m)R_lQ{dY2^ky0m0*wPpWHstAN zf=iPHC!FL`eVP)PEB9*lbW}1uhwqUKTy|S9_1#5Dp9QxZek7izCng`QRH(d>gHJ5{ z$$OMZRwvGTE{a&6Hz{JP*=Uju?JDsORg`8!qix7u3go>?=$Yt*g%L+tu8mjK_gg}y zAw?-$Y&$HrV&9l=$eUcsy-gjG9<$G?tunPwpl>$yXsvQx z-eACme7|`xKHiePuh~0ovGQHcj16W; z^TbV%o~mVE-8S*M`it-NT@af<_Zl}0!K5QJd8uYbr*pLhaGZD ze9wWymhQplxXGoy*HM2>G9ujv_P2=>LD%9*0eJBF`a|7}v4_a&ijx2@*W-}A1*f}23u<$zNp*vocS(?y5p zM7ZDeoz6gy$HxGSyX`;X8ImvT>GRk>{*J>g{@K0fm-WpGSkRnSSSk4JKU3MSM;PuOth2+YAe=t22+_YE62_3O^-U4neAG?D2!)9vy6@Jyawczf7{@;1ER zYAWCuAlfQk*~7n-(p#)f6WQ1ZJtQ!JUB@a|cvD`+plNAp20wR-?ftQ$iw*I%))R1d z$DZ+d7~w40ial&CAsOrt>K|G~{QXV~MM+)3+8fIqYi}ElDs%d~9~zogT4wY~2@V2K zM6&c(X#WS2&Dl}K(Ftj9j=Y23;S0}AcvhXP)QAZo4Ftc6QQHO0AJz25W|2NOsFJeE zM03B~lhOfk71SScPalSpxLq}uz4PXD7#`Dvfs(`okrinefRUm-LJEw}OrjNO%C^-` z+k|-G^vc$isJJjUpNuGxrNh4zQP4q*5^agSG6=1};Ct_85oX0NU&N@$;hVHh;h9=c zWa-OjLKEUqFTz4U^rT8UZ7nc*o5d@m`V;#gKy=(EM`aDIQ2LvGyXyU2^Y5!0?QI2> z=8-o_)q9?%siqF#?AoAzHfj$F*od(7VO!>|oVH5HuZtR!bM*a4X1F%%R)uW55%~A` zSSIKV&d=X}W>zyshJ_NvD?Me(KTb>R1BzjUt*9ln!`H@?UD;UI~ z5p;*`Sy+2YjrBFWe`6C(-kN7*`Qo7uekC-0ii|RORI%DtQcVYWHPNd$cI@_6YX#JA zM%CUyu9-=eAe+tLc-!75T!9m=kXY#9iW5qHaCCMe>7auDEyrkfc17;=cP16!RKe5% z^Qb=%*Vz-F{VPwaTxrqwLUM>7G8doZk}~AxW?mRsWJdc_m^Kx5*o+qs={>DRr#Lo% z-Pbetu6?)s35lmA59)-t;)$0$F-TY8oYlJ*ZRGan#Hn@ncFpa{sb%Mux7>AnmEh0g zv8G;DEXgI1gMjN(&d%AGufRrmaGVFP)4nv8T(q)Ah#T5G@ zC#lqPC2DHA1}7<9Bv{0zzRx9~e~g~FkU?4Vz$m=?W8oB7u|{;y(!am7vu&d61gYZ1s!xz3S<04fV0F>W?0{=uRUYW+i>wQ zdjpB+Ln7sK9Yd(>7gA-mCoZm@gd5(L9f4c5ehM4szG{bE{lA>DpX>lTESU^*fogAn zEi00#NgaM+6W5L!-?w!PB85R>D{HKo6?tlQFc9~Oru`f#h=rN@ZB$)eJBc`8M4@Sq za}0B~w6t4uRalCY0I@(9ZyHrg(bX#gIdJ+SCp_PivOy|hy^ru{Mjt_YVmt~>iXa-8 z_mk7p%`QqpGuWb`t>{^dV9%OPF!o72)_#i~vpSSS1r&)Y1`yd-2Xvcu zCV(L4rvGDs_@r@{8DV;qVhrb@IT)pdMsziReJyGNp!{R2FIFgk^+2eh1AYvjd?{Ze z=zw_d=!I#J>Qy(g?>EQke5@IbePxOi4R2_Ig}Ye*5)YQE1j*49)7zdnO^{j<07tQN zGh#<*(W#K}EiGqYY*Q{~KU&zclVJR2za&fjHJ$W4GK_kkpAn|`T#@c=6`+NpQ6&}l zMj~%r#?37f6Mf;PIz4G_=Sw=Sa+Q_$ZSa0sXqbtSi(fP(Il>)qaTC(wAp}5Mbp;0C z{&Yas&o3C<8zdPXM%mT1cE_8y-1u8NEw3DfK5-&u2GB`Zm|{__X5Ss03JjoVZ~mD@KLAh?Rn z*tfQbx|71p-~A}f^e^8m2IsS{Pg|052bOt} z1co*$^7IGCYzal&jd6~~=o2w-EH*lD#Y*Q=P!{7iqmx?JL-LU~dqt^wBDcElcuWE> zNX4!e=7E3Oh+WS%d@gQt5_d+8j=8eVF(jBM8R$AR?F*9+&1yY@89M+qkn@2-%Ub9u zu}QDd^v776d|xsRi7T}Hpfjh=w@XXllU_{Rc$-(roT3M*xnGC*W5P z2%WR%j$8cW8<|PaEveWWUwY6HY3DWL;=`eErym4E9Z7-O*F|h6 z7QLZYLi>Rq7j<-Bkxe+^LDaPWA?W_&D-5y)o!5{UD(`uEFaA;<)F<&e&vbu^zBMog z!?h}7qGrjyJ6x^ojx_^Ait(SZsPPRMlOoC;*VaJQ!T3FfETMUs5<7{W-<_<}cTQuTfcRB)FI6opl2U;<4K*`! zFC-60LJ_|ZoEy8cZzFiz7t+^Vg^Zbsu21?y)**u~B$s*nN6Rpuc%*V+@J|iu%Y<8V z+XsS-N(pNwCNAj%sDQi4x2)oxr<_>;c>>VGLr8N1Q&baNcBrE-(w)#W8*0nKs3q?< z|3&KH`+fI=iDF3lWw>E^sDUEV^ACj?-kyjxJq7L=%_vD18@q3fF&4>9)$id`Jv7}N zF=X(svZ%_R7;YYf_92MvraDPK=u{UAUk4cf6gyaSpX&@Cey+v z2{}-QkVH)KV<_|8l{`**{ci&tggP?L- zWZ57~M|^hsd>5;?%?58{&&WeAo0$%LFjgOVG9}sRr7TR4M&6UIjntED5T}l5#+W|% z^B3`6b|j8}+z!dbD5>-D9>K5;F}Fsi``2ho{0GbyON(zcUkT>B_$$U@!+xfIAXo#= zH=*5X5#c@KaI)9pEf%w785s^80fwU7+CSrcX4I@I$o=mBQRddOij3u=u&!mc|91=( zH01?$LR(Ft8p6A+!yi?%!cbK^WeH9w_W@}JeceuEjDV)$O7F@5GU@&& zTXG|6(d_iqN$3||9_drsHGLD>wQ5#b)Wz)~Y+k{YRXx|ugUE0Z=Eml_dLMaJLk{~V zC55CQwUkM>t%Sssc#RJhp5~-imXR|i%I*DX?>$J}yi55nQ2Y_ftcyz=05Yi_o)bdp z`vURWZN72ZVpLQ@VbP4W4z36qPHOZ6_-EqWQEH`olB|Ms9|(~zV`uodqMLrT>}2ff z{IwN|i@U}s!ENy1O(Mk)QEY#wXL8{xCKi*?^s9Au4~0ksUCTf*Mj|xIOkMz&M|(2+ znXiuuG$oQH1RS8RekW{6KQmPYosj*o%Es$PotL0AE_uCOtVn7q^>$%U6`uyR?Iwp8 zQ^ZMzHq6-K<|zo;9(D+MLGCV!=jpOeM`UG#c_;S8eD3C$#50sV4iB@9rNw!kmg<4G z>d((LyZWRR(z5H8w|dr<6-iJ?8BrRY ziSM+;RqpfO+jDAtiIf;T^)M!xzLO3SMN|=~?cR(x(@<~N5`EVk37Pc{dGu{V9AI#c z^3+js+H31utMwO@Qs|paNg25T9VTHu6X>`+v%Nwjdcq~do8#8X6N~C=$eQAvvK}`z zE})S?y-ep4(ZnuHeI~N>6Qclmf2Aa&J!mg38g&l{e#`$(EdRbQ4_H@?nTqLUYg?_L zc8c?j92C+4lB7Kstle%aKkHRCra`Ora^+3!n*dU=GfOggQ9+!}aZn#Kx3*g(-Z;9q ze#hju`{{oETxb(OcGn4G6@Q-W03QG5&yAjtNjD;k8MoKkSOG`08#XpK?GAP&LaTe8 zHJ&Ace2EiZ?}0wr)*BhDd)&x1Tde>-S)}yMFnCJb*M3pRx8ZipG zcIXzwR6{Go1w%{+E<>Ck$v!V>v~9 z){`r=cUMb{C|6xwop{x^I!4USo)DGaAMdL2Lt50-dcv=9+3tk2T}4A49_@zY6vNnW zWu)x6`U4lE9G+j9Xji`B)z@=7R(?@tV<s?_`R!#p}LC=$){4HYPnh#*_JoIMZVx6<>!ym5 z2%^;HEf@D^rTLX+RR>1$xeBY9q)E}zWGF=&se{jYc)><}ShgiH?#?khKE5UG!)??U zEgr8Tf`vDl<;|k)cY!^au2sW){|`S;RdtK99>65&EzCnx>h&`Y5M$u4h=%d2syP#Q zYcf_wI^dl}@e-Ht9j(a&j9=_>FHqk!7qYV|B2O8J=<_ljv}5|Vb$0>zoB`W-_yS;6 zmzL&PnJ4T_g*ZWl`ii3|DJkbVfa_BZPc#@8^msST&Xg#l!eJ>91{l|P$rQEn3ObAp z9zc6mH@eI8=OPQwI8_Uf9i~TKyzL~GY)CDGBOe-bG~oOSqkr=UB|yj*$;NtQt0*Ly zmhQ4g4q8bM_DMD6Qzch$z5m18>Qq500~SYV_6ipedxv{yFfy(xvkIoAU%V?ee7 z)O*?!F?pmWYYeWeGGUx4IpQCVFmyz5RztwA462&}Y-d7J&(&RU>2g-0q{1m1dnLov zA|vmyJe?`~I=HkwF+-7=*((yOD8AqrY{uO-t-1dBLl_h3C8X>|m$?-=Y4J3w;BAfn zO;oX@_Cu=K@z87o!Cv@U@4Oz(q(G{{N}mud71)Vh#JQVv%iyqI-;zTa%e9l~RsBv( zH0R78X+O%9vsL(S-84T&M)=1H!&bzWO|j4>aA*Zv>8?&nX|`*VaaWeHL7%+_`YL`u zu1#e~{1>nbB7@|naCC%``tI*Dq-(!+x;V%5t5Z2JQzpXlmcmylmAj_njMUbq2b|U1 zX#d%IcE^XlU-b$IfO+g~GY6(W2L?&EMMa&uV|UqBkbrZYlrc@+&BQ+69n@2PTTd@} zoTgoOcnyb2#x32x>OX{W0}rWTuPTM2GB3z(oc4G(wO z9)&(_!q+~Gv&h3?mhWFwyAE`RfE%d1j0?{9td^eQJqzM)6j?l50hTe}NvfmLDvHnKPCnB0*zRzWcd1_`w_DeM2P8O>O)vqR=(T&2q zensYv#JwsJ;G*E=vWhC`m*~;_v?20fyUH>WjXp#~^Zr=L9PJ&FngSo9?6QCaw%H>@ zUuysX@{9&XEB!nVI%uCAXkm>6^n9|4E-0@~Hu1oIC zz&lj2)lXnnUY)<)UhlhoU%a?lT;-J?M4SU}3t-#jwb1y{0TNGv(7X$ibz7^rrw@5lKc)@=CXxCXYZp45+{3loW-XIb-d&zZ;;!^3cUN;3(np0;#Ga)NovW9+Can^ z9aU78ohbAhr`ftvEhOciv7-@U%Py??!VC-%~1uS-xy zL;GzDeYJ2(4JtrVW_MYHr|Mc82v)Ed6eStG=^_#8j@j?_TX<@t>jg?o+~v%_ct;v* zlDa?r;r3!%xq!nBHM2orR)qQ@(shGVXot%8Hw_maTKlF)5dJU{XsE@TACN`I$?RS~ zkygbE6pBt@a%b>lr9!zpuw?ap+1mA-hbPM+^|BY`LMv$Mzpc6WOD!iRDk&|OTY)sm zQ3h-MB6{lT=7qfIrRG?!e^?4+{{@1v>Li4M%N{=E<_d+}sv5B%pKP>XTN6OUwAoy3 zj}VBKSk8#b=@O%2==MW%0`M!>slS!F^9yAh$Khm-qllFPYCts1cI1pS4Ph6}+F?nW zYaP>3mnjcJ;gP^(2HJ7=7-A7}qd`L5BClI(_FA28A#R1DpG0kL;;sva(PKln@RX=! zt!ztu^2A~HMU0If@&{KB=UXkkFz7zW(PQM9H=4djPLxgainDYMB)ZCztAP20m80&< z9QojnC6f_rAlO*Z2?`P&(uk$!Z^iE%w*u8ni5$<_^MTW%po7yG`E&ZYl;FJ*F?vRf z@5TE<>bWHHmO3;RAuqT9N<_$*8^P8Ss>zEW+}i`ZIIs>8a>Rp+KYhM;s(YXZd%!F{ zd9JD2Htuxz3{6(kw>Pss#W*OD1^^6(k5+btKFWB1I!Z9}Y5usQ%J_$iE=b3~9ycp1 zcHre#mUvsluFj(E^o3k&#${_X#?!`2{kfTF1JUPto;h$bjGErW8o?#e1wqxpU#q<# zX17aW_V_%US%+7DY=rYL7fpxmq`qz-KDjjNNWb@bPKYRHM~j4(X~#+VRij_U8O_6k zzv%-S$h^yz)Sz#5hsHB)r4KTTP_`!_IQ9WHnFPECQ1tyB6LHS@DQA?Aev(`5W^6_I z4P9$`H&Jj=qJnmM5UZCW!|Hcg}@Wy$EKc4l4Y(QEaFJ{5MHA@GuEjL`@l4S&% zXF&TlXTmpeXep;qMlpnTOMWEb(e-vILr3Ht6z?D7;o#A69H)~(GRQ`*R>*k1$cZuQrasBSwTSDRB zQs1hZJsjeu;9tX8XDxs~bJ9*2J6F0)8cc$2=%JrLOjznQ7G7nj?}sR(JPSR}wjy7w zS%nl1ijwYcF)j|hfc8hUJti2hSiwgj`#=L}=Nhg5Zm==F)*dw;S-8-0Lk)HB(< znwIbrP1_~OuSu5MD{_cl#j4JBm#l1!ab)vTx(K3S@O309>DP2YvV*ol~dQ>v5Zj*kfH zGr~+*HZ)6p8G|Iqq9C#WMEFFCyhqIgXn0bCccZ$Rg)8Yu%i5i6 zY|{aM3A=tr0Yp<3G%k7T){93Mtp**M;=tRGUZcMgnfYfMz-Z%O5n&`BYlluA63Nu``FrP09|u8{PI^4x%RkMLlnG{n7oNotN;vb03pNH3+uJPF zi*JGZXleb`*&+wmobsXLvrAy1?iDrop?!v#S`~00cok69Z7E+_lO84^-|EL zOxg(u>XVZ0Xw<^rD-19SIOa0tGNe~D?v@nKK8;fyf7>LVT=INdvE*Lo%c8#GFNW&_ zb8>C`zUz=C9aJ=X2$$OqLWUmm<`jlyYg;fvKKkwXRp?Kjj|sON=3bSEfnU3$Txq3; zil1eBTc?3BT^TQPn|j|YN>hopEQ)+CzI9_P|IW6$7{H`7PK=65qVx+m8z5GKUi7nM z@7X*j1inMN{FRiS9CF`^BAfd55J@R#2N?iDyWV==`dlt@Y!ZuG>Jet$qUZb@M3*`U z*fxdUzJs6FULsjs8RgWTS^+nN;`ENo4-4aEa33Zt2|o zK(;z3zQg@fpQ={6e2{pO8TpEJ*6(efAKkmQ_h+%tD10q;U}bU?-Mgw%5*PV;l^RVQ zDz_6_Fd+#zezIzX6nD0|(fJtVvi4{+VI~R} zMlKVrlL#XtiK1=pg^ND~cRwQ#q~t0g5;3*Nz`m4Tm~WABrw%{8%Zp%g?+vzjFF|?? z=d;{ODGhpoFZ&$9HJ=`~15^;269#&H9rY2gXpxo@mxTYlI`t0g_O@tuZ)kyxre z%so-r4m)0e95p<3f!ITzj7>7I2E8-4DTTsq<0MtA)TE|4Y<)H13{2-(-BdTXLd{i) zaznQl$wbVhz+E#jkGkpEX?SVrjGbAm!RF7J2~V6^=I{w>92VhBiTuHcNw9>{ei#L{ zsI;C4poozXl@>CHCQ7M62PJptq=AtsLGwszhwI;4A=D7u>jNGAAW|wy-?mCu*8dsNQ@2opItw=<39@`HEh+WEr` z4;VW@A{Kz5a8=1JJof0xl&~*Ln}o5Fi=kLK2TJJOB`)XJ%+{RqQI&FjAwleS#{0&i zsKO#+aflINW5XT9hyPVP!}Ygz5Z5R!yo(L^?vDoS+JZGf_9rt+g*mIvz@9umVBKTJ z?dm=7^zIxwRFEq#*RM7BXDiMqUm_7Z$X}2$Yf`eU3x4$@eMaj!v-x2of=X zC@4frnnc%iz=;UZ@spCe)hotDE2svDXlwgkbH1;Pu2)RJlG=q5fG+xacHW-0-V#1j zwNfaI#VSIFz9jcra)B{0FxJE26I0$K>BwZBapX}=nYv`yBa_F-?F&Q*T69vx)!m9m zsZhnsQhH;5uHXP)6x{fh$!TQMN~$H<)0cmu2Ge%pmUhXWWJGIH-bz8EyNNt0zSzka3#ZM}xTyKLX{Fp98%klR&2 z9;38+R_LnEo&w%cAA~}tDhUCm-v*y|*oTR2xRyReUDAJ60%p(MT<#bf;`wwUj)NdL zBigI~jsPMbnuOzYy0h{~hBZAXLju3lySR|7cP13JoeQZ~ds}?xftNbdcrp?m!}M)0qi9`L%(BXStI@F6Q~|rIZl1PzBLT=rXq`Cc zW^peypw`NhSkz8RJYcfbIaz}b{{^avsigBU+OJl$L7xkhO@s_X9ZfPjxo%YpDYa(l z&N1m~2624Mo;u1#3>|>}59uYE&j`Nyis;L39J4rEZrBNO=68J7BRypfXi0w#-xWtW zwkGu+uaWe1kbyrdo_aDCP4cP;QC@@qo*uf45(wu|C?CWwhBQC_NKDvo!fU>M>|VQ|ez%g4o{+Glxy~mTGBHNfO!|pA{|jV}$8AX_dc=pn98fzH-`57? zjf370eiv9lGVvDk09)*riKmrurF{cUZ;t$P*;`v?b*;2l2WsRY7;2K7ZQS|Sio9W+~(vZdB_1#Tl^RWX=Nvggj!flyoCW{Af?y7!1i|(P1&cq zYEG-Q)ZP23!GnLKRP(-z13pHr%6#{aNltyoD!{bEu+drZ_c0lwFRE`9m8+cC-b z_A*cg>D!*%*`VlU7rvd)h!MF{IN+A-i6?We(>_+ShB(tKQIE}$dxb#uM29W_RhEx zJaNK{Kq*R28k_uyx)JwAXt^isE1rGV${EVsLqF3q^Xl+Hoq$1jnIm?qi2cYz6c-Bk zE*rj`7!-xSo?~ecRaD=A?Ro)7UhOYk&VPt#NcKPR-nrF;shq^X2VEF%>M^ z&}&(A`!L_r92#c$jdD)=SnP&}j#LR1H7cH6_G%gXV(kkhS%O4rLnoLh-eKY~}D$FOXs-3oMt%Q3=$+0JsR^_8# zF(!#O8E*k4p9}r%79#~=LHg9%KfMUQB&y@cy2TfgX{RFGOEUbV%cb=onY|c1m@TZz z-fS);h{vML;JB}9hTmP+yNlb2#>)7k+%6v2J;Utu9ZfmXXrB7W#-EFSHtx0%6(cdxysi?b zP<%n*>5KWPUdVNZL0qoM@TC?0kE3^W%SQ0 zjY~QZG7}9}bbWo)_9wF9U7P~p;7$Bm=wi^b2DD(a*T^TXaMw)S0xu<5waBU5uX$hw z3&fd($c)dx7%pl&zQqF^tBJX6kMEM;!D?1{HcM~S1X&~z1Wk@cJZ$&=b|=!V$;^4^ z9j?KX^HR|_q#FK0JT#*@w#yESF-mT`UPKWiMHQRp{I~fC!0kIo<|j*H41;jL17dFj9qI-sc@)Bsn>+PO^wr85tal1c$Oqzf!$YWs~^_HHPureI)i3`5U< z-j&Nx>Ono$1^HjSfAiK}5$hBlhaTe~`UvG709on84M5*L>1J_-vbn0ti=?uSyKjMT z^}=%^co$5&%~hmO#@)IedT~j=mptRnlOBi){v2XR?f@ipfRbg0rqFmXh#%v^p)L{t0FhP$n+0GbDrXW<~8adggaRk*^malL%Jf<|LAdytONu zlFVT*dxnOpMV&bMyD#d)`^?cZH|XFNW9SQs#x&E883}4oHuo(VE~>y~r0EIspR+(j zsvueTlT8MC&#kx;BNIsR!Swbnm7ryrBD$cza2&JSRS~k0d{;$*6M_7)&2h-Fdt|cr zop0y;tHb@?6r<{JcHj6cQlY|u>(0IgZ1ZV)`|yGYaOEkT#GmLMHo(?Mb=o2pVIvXv z{($nM%pi02VBR;pDnsRnhH+FeRu|S(m4kzi&%lKN!CI^hABjka_$$Th{VYE4p*&UB zG^J6u12jgA3$KioBk1+Y+NTsPShXLnijk9J`T@Ki7uQHoLXQWI8&%N0#ADP5j7oev z=-HNdd;TcqN=vo>Fd7xNcn)dOd6?u%GRTJ?K1XpE^=XfAcqfY13Bm(*(hH*!gSH~5 zl1PPTnyh`k+wTY^a5kEMpCC`3V*KX@)}BJ(GRf`bRjg2oyvUXfS9C05BwZ~&9zm?@ zTE#*v`Lkgj9@K?@-kn3~ghU#nQkf!VKj*+xJ#MD$V4hQX+KEOt;whzNBlNQ+$?nbY zpvs8YcwwN*kOzzl+si-?Ep;@&#QShWH1AwJDkAO_ z2Z#xPsT=8vQQ!4dh5ItX00*QBAzfV`s=V~@>}FQ~BAT4D*K(|eKerddYDwj<21%?P z&@|7?FnzM)MhNkZ^LYs1&s&RyWQ&{wsxUUxDuvb;W4o1TB_bkx$~Jm33%w;|r8?G=l#kY3QA0pDe-9JOP#eiVy=z^5Tn zw)m7gJggz&i_oT^F)fQ&(bmRXj$H_|V^Yd`#}Ti@G9+u!#;qYzCR=ha33`M#O}+yN z@g%I^^Xii^OFNg~ql%g00Vm(O;9V|I5mm}oPwjOu z1GfY>_2KcAbl{jamH`jojQn@j5+>T5%oNrKVq~NLtwQBgeMWi5gMYViN{}wEEj5;x ziD`DBLP$jN+#8t*4G2c9h9l8Bl`tnf;20-83{A`O6t4EA8&(Wl~kNSIoc-?C-_t?6`}nkfix{MKU{x=<;%HB|I2;kpUS(ICb0o;H72pdU8E zrtX_gcdrC}J)jWAeTY-139XD3>^7bWNelNn?At=Pcp<<8b#8?0Gzg4fS9I6F*LGqn z*f6)%s+*bDp`I%I)HF5@JzDi!SX?S<7ehwmR!8wn>YJFtFH>3^`D^@LR!!Qr9sT_& ze??`D^!_&8zsv_o8&h7ar3{)6VkF!24PW-GVD6yv5koxS2JgSL(L*^C5aO4VZ(5rl zW7D@qu1njg<3>7S5WsD#WKR?+xSBJGo3n1JpO^|+8BmaqR%ZkSW9u#HVKsj##JYNo zioWw3Bn2bTYFO92!i49dx10EL4Q!=cKyb9|UNa{bk({WVWhw}i5~WG0`l&MH1<|du za;mxC2J}Bs?(iGxpRc|DD#H#RZx4G%tX zP~VSd?w+DEiXG3DZm*{ndUywZdrsWi*0@XD_WZ!$$1SY1W>_$i2 z9P)vMjF{{_L;DAjy$d7Vz6W9PmS=L!>qMWp+6YzLoI@EFZVP?(3DnuuE!(RRPd}ws z{P;0YI6z@r?I0aw`OaQt2bb~$snig;IIs_fYR3p}2}>_6{wwk~63&MfENl1>eyg&k zWR>8k&(IQKqez5xr)p^zt|;Zj3jgus2EMitJDwm%y;jWv^d0Oj$$<6Q`}@Dh5xNrK-=Z||XKv#1{HhDDbYSdP zj^O4K2&?>o08mGj>YKIH?j~_$kqaw`O(RxqvOl>%1#^_)$q}aN=avGLKr5Sik*u_R zq}<>-hD!_O?orvS@+ZgBY0k(=cuqdP>kx^PJvLEV2E@Zrr6?iY$dZROF8{4s*!)uv zI1=>Y(0SXJ1H9VJycp z>+hJ36BFO5`%2?DWZ_9SUH$y{UqsC3T|r`O=#TW zE7jI$5%?6U>@2Lb{I#@h6SyEnfeH<&{)!_iO+L-tWCUSlg`~`mpW*YC0 zd3aVb7{}e64Pkor+x}(Q59t=dB^t=i3LOT8!sqM1-=-GEomjw^VmnfsTf|KzlnOjvUc%*j+X*p zijMVsv((PO+SZ!&2iJNx3*NS=p_AwmL)cLSUV}_DBDAQAx(!($Ln~rG zdgk?2AIojPjBsk##dnfSm(uWxsYA33&17Yf<=g{Y$?7e|Tkk*&G$+g33fc5d_})TDjyoK7S} z!(PLAD2|0=mfIB=L{|FJ6p+tL+BWt-AW%KjR6F6I8ASiB*@(f1*;0z_PS5wRFwAUh zNbp1R4t{XSx<_R+g{# zXjj9`ZeTqpOfCHWO*02f0AnxUVP0L`GRGR`;$PHdgMJ)kp2gqBHJptITcPUX;OdUO za_Wt}6Tg4w^mYg@`Zyz#nL|1-EsQr6i3Uo^i5{G5hHc3rSl6Vaq&lS`lr+Ht9imZV zlQx#}2h>28QIpo+@0Nlh%zN~u?nb7OtB?)G0*JU545H!R*5J6VN%SLh8d{fwafLaV z!%+lLp$DaW1Z_7KN$o?IeLNVcvNS6Iq*_eeb)pY#rR(h2pl$`djtC8(NLU*6SIt?`+$sMsBe)$KK);#-y@%zDt}?cUwB-q zN8}yit6(X}gECxwlO4!allSq5Kkk_Yhze6A+m#J=)la%7fF5l-7(+DkAz;5qAq&d@Ij@aMlzK}yO)K7?}e=`uffuq2KYP(G0^?l&;LBQ&UZd@oqX!7N>QAKZ-L6?e zw>A`o3`IxO1u=ztaumPrJVdXi0ryu<`9q;?>Shf53cYy5OW=Jm|6gQ&w!tH_!&=GF z2gAy~y2DuO!Hp&XVRr#<_4m|oR~EUx&z%PjwQ}O1Si@hf?&~TKc5Kk!iV#HF##ZFD ziHe4v*0|kh9ut!Orlu%mCkQ-Idf+pY%y+zZN2odmoM%Y8j(M(DU>+3TOiwIXne=jE&M#Da8 za74|x%WeEufeXq~u0|yzrzQA#5}dUEi8jy^&28X|AlV@*HT^g-|0D{Blk^h?>d2W4 zL!Hs)7H-JS5mg-LZ+l#z=&n8>?8~3?iytSOkP}R<4Zgu)ufj4rRAp-&nAR7(S4v`u zW4oJ{o+ap)Hv6eJt}o^G2Lr<%A#F=5;-`_(fCP`iia?x5r(WdlsqV6k~P~f;p7XeTUfk5 zT2lhFk^}^Ob)|@MU-}2T>SuO`%AWM5&z$zx{c8-qnTXAmra(!kv>dK%e;$ z*6C2Y%)eLQ03Qu~Jm3;vb(cYj74%FN2%&?zy2EZ7dD3v|*~N-*^})LCM#=I9A``_R zGWh}3q$Rn}?ae;8f;M=eNq!YJkJR#o5X-{VA=aiS z>*Y8}DccIJ=GD+@Ck5^1s`hL{l)PDkuHlB93+BuRo7GCr?>X8%iV$TO^P_G{pGWsQ zWg!nqebo%X0$dlnn33_> z%i=a9;`oPk((52Ha5vQL-no%z;A|M$%ow~N$kiQscjgO=!fAy|B{_|}6lQL!6>pyG zjj`T%qZju+LFh5q3;^|O=_M?MC` zkC;3nUB#@tmh=?xL&Hcr^v5!N zrATII*C0*1@ImSB;1>erd9)X#!?7;xb<`LsvH8ji=dh8D9PKWQ2z6QdqsN8D=8y4N z*|IxNEMxgBn>m6>8WVuiK2f#2TrgzX@80{8jxD##ODW-lUa~_LDFDoTK%y@nm0R0Z zeAgiHyw=PBHRt4P`e+<|>rS~F!M56--Wc%m9h{5HvBWbvk88=DABxWo!se9|kdZ&4$}K~fO^BGzp3 z(H%J*^Aa3l(%{j;cL*sGLWKWl_D|q}_fv1tZ=oaubC%pyW*c`2Poq>pEa z6vU`go9{u)Iskbp8^M^N>4Pt~zaL_CNxUEqH41DNk?wey@q+TVu5e$KKe^!`&R^*X z3Ppskf3^%iHr?5a%u&(@4Z%58Vh+&0SAZOv!S)C0^S#r|a8K2~nzC?C-EFv`uj^3{ z#c)$`2*wJL#*n=HzUO<*=1){$(1(Du2>V+F)EEnC92w7V)=ZJhhDaU%kOB)dP!@{3 zF*Go~a*j)fml2&c)xu5r$UuxT^xvOHGpgX}>QAc5SK$EhV29!9D^<@^9LxgOGN*i=YT`tZB&_S3`X606G?2eg?POarB3_i6ESf6 z%fYCNd=fq)G9w}hW0ma(3n`E92Pwp4n{qmwXkG7r&&kn=B*n>3`dU>&$kHNaUWIQONbyxUQmXuhgXH{+`!Q`6M=>Gqt%3j-TCMqy zKcX>Tf9}2mTo9Jq0(fU`*V$XzH`|{DC&D%Kz5M40rpp7$Y0mJ@5uJ{3&fkTC+DR6*G*YYssti#3o?C4`m=1<1afd4Ie{2o(WJGVd&~a{<21{E5EH{w;{8g!56J#k=Xbq?^*24lc9aQ3ETPgcFM_ARakPe^E^0QD=t@P;%FJM6kk{g#F6Q-!8-FnEM-3fR{ zhfm?@K-0qVr5=PSVv$J%M@kH`Ob#D^&+>7J-$T+S6$Ulfb@dtOKIVlj^Ep8bgUj97 zfGcb+p?0g~U-+u0vreNVnLC>O;|!(776qfi%)}BBX3m)b+!}cd@gxBgj7rNAi2|?P z!oh8E(YLO6h73xof51;QO5XBtcerUAbkKiMi|$2qycplhKA|q*Abe{n@{BWgG+>IM zr^6vIP7FOw^?(W zB=8#<$p0+~(G@pf)&68|5M7IV$_j`5xzgd%oDKO|47KW?Hk2v+GDD6A!yB)mx8 zJwxJq@TRqqfqdnb>N#S~Ub7)J9$t3E{DtL9GnnJ2Y@vELg2XRO>29ej`e>ub6Esy{ zYIwte2XuswB{9q!I0D&Ic(#VHE%oLmwCo(0@gD=j-Nt+6Ns)0yT4E06wv@g`j~3dw z(gZW3>e52R4IrRI==UDHdL}CNS#OyX{rFXo(CN)BwDBwYqrgdcsTjnbl7nC-xsv2}rls!z5{mC<#i$yt9AtL0 ztf9vn-QfaMUxySN`Eq;w=_##4Gyi$#t}Y^knMxP-534JA|=9y@HzDPWcY4Y@f51?M#R9~}eRZPdnCo~Eh zf4IcJN1oUFTULtmgz%DDLX|(!Bqx6ZO%RaqyysUKO3P{Kd8nq#(ojsEa3eNet-GEi zUc&-?Pj#MM8%b`;aHC?dWPt9XxDy1?2<4?X_ml-}dZyvvQ%D-T_jQOOXWNy2%L50T zlLg_QU1PlO;*QV_YlN&u)VM$2W3b-UeRa8e`NGG>F?NJLKvYm~0kJmg~xGmAxPT1OdlT8W-&}@%Ph@% z1&Lpvg&H1VHei1>~vYu4S0?BQFnSv8p-vLwb z$P;f>r5GMhk#FCA@}%96Ln(z-O$hx^Rm_=fO}_phiqn2ywbNzFH(^g^@^*a^zmzsb z)At^qRu~kv;wscDKrAX2Z?jLPA0A9xwM4gMo!{t5Fyf`1lw6_yA58ziLLTofJ7u;N}z4-IkV$aQDm85I|>4^nK= zFQYc{g2u}z&7&X@>o(5$B4CWCt-|a&iEo^W0{F97 zYsG;yJ2C0qgnJyfgngxdLgx8GgVR2(ifSeNQ}MlDWD_V~y70hf<`W1Wo+Onji?{hI zoG^4~At)O%nC6-h;h%DpGWw~-;J;camlzr9Afp4&4t^})n$3~T(y_ryBFKHK0p4yl z^GGbJ%M^QY)<5}bD>w(y*hCyv)DF0KxpznG00R51gj`Of`$Mht6(J9FlQL?(tfrh5 zPu4q#0(YT;b3%!`DYKdOf8?5bd3#Rr&G)dP;^;S zZiuv5j`QLxXPRV5TFWto*pDXsjyF5QRmyE8#^f!fXUhJYyB~@pkiS=#Y~IBn>rIgK zY*p{e;La#kxr;~+%OPm9VGah+th6QPNN?^Tefx@zM(i|vw1p#&1)yKjRSk(-Zqsa= zBepjn2rPAT$>HGBSn8mq>qRnho6s{kko7lXIH`>;!PTA3px0#fZ&KJprlt)muJ7n0 zua*@-eAgTA4^$OOK!AFst6|7GXE7af|psN1R2Ok}38n zwx!Yh^T;o7N?4}+w{UJ_%iWMZ<)rkYjEUb+he+p2Ed!TQFN`bZeV#7v(CJ5+%GlfR zpDm0a25miGB-%seM93jT#lMe@SRXoZi>AFO5_zoT>ok+M*ptFs*nExO*irj{r zevdp(lWK@{kfOxR)t(sP)*y|L)I~?DW8&uFjqDH-sv8TirT%ejh-aoN9HWK?*EY#t z__GgxeUj+DT$!&%SpZ?(5);JYi=ZJveiabWSeM8mA z4zQDb6>M5p=XyhsIA@j4uBUfgZbSZE+BSowlQ%T?eO(id`0a{iF)v(33<2qFvmSWf#i00=OtC>n2PoqvwqAhZFzJWaJ zzpzcdr*_{tYW&%6Uv%>V$DiOc3jBG-Igf|Aq1xrt$bI@ggY9)j4 zh@!a zQfL%>@5WCN0bwU;d|ZchWOe0*;aRimb8EvehF323i$ptiKGG2l3BKsE6gQIWynbg@)*t<1W&ra)QAh4j zbbx$`6eNx2cOqgL{E58`E-6)Xp`@46>xazjY}~Qk!y?y{pPs>Xk5UwQhQo&~QW$fL zG@}#gD2@4*_C1j%1V|fmhwjkZ+w|us|1Gx{UJgoD#pqFCji95+fVenMvmG0z@t?&w zl%xsJD4Y9oLVB+)k@n--z@c7<1XN<Vlz&7P~a-X!c{m)wz zfz-v7wXB6~u}{-ZQ7Q8>ho{DlZBZ^n$cXCc+)}ctx*9>v2jw%Pl5)Y0ll3%We$>@< z@q=aLDi?!nszYJb>y|XiR4IlZiC7ko*JKyL!p(b+G^wMlL*n?Ol?gAF0|LM9!PTh> zXIp5KcpZ*h@j;!{YWizQ8$8K6em&oV+h7ZDXdQ~HZs;Aj(^3czBC%F}9Z2mL(P zr};OJWKCBa1YX{&Oi~BwuWEvZdCgjJDYo{e zu2PUcW-ZLf0CNtA`L`}xYSD;*0cS+iikF;Qn1#u9Sh5kB&PcUXSN=9@+y#D!H>bwJ zCewK2PqjZuRg8342A1z9wZF2vC8bVOY}T+Mw{h4w2{TZ$MtkNZL)&ptCO2<`Fy8S1 z#6Kz_W*jO*`r7dCD;~2}|Kd36IMlwhE#Az>0r%MAiw)bQOc?eq%eZL)i9K`(e$JuOz;;cy`fpA8;g9Dlfg^JuP$AM9hLTR`G zSO@T`;4y)sq^(V2>(IzWgBHN5sIYS*Xg}ZJrYxz0h{eJ%OH^0O zcd=D9sxzw{CX6;xbs%1KDnrV2Tg)y8St$y>1EgG05MvzR65--`KnN8B12$jvN3y~_ zF?6+%0l>bA9JtUn>gVTc=p6&dsybYs3|uX9JBnIU7jNFkL*y+Q0#HoLIMn&D+tA=4 zC256uRIavl|0tUN!A;ZB4pCQ`L)sNvNp_PDacQ|&R%?yOa&CvB+_=G`N>Xu_fT-6C ziFtvEJ2NXxg$*y!5^Ckjy9kV=DKmSQQazD*S}R@CjJ+P92q{}|H@C{G^JpRzLIo%H zZO6~QG+`A71W?*BTXBEM4u&NT!eV1hEfv8V-uN_BV z9wRN<^G@vT`e)L>M~E)a_7mm&bOtd{)LP#{QO)rP6?q?NE^$55r04DZ`V*9~OH?Fo zo5$Wa7$p{!b`umj`uD4Qyl70tMgZk`AQUIy(^`V3I&t(U8R=>0_#|p{-bDNEGxf!o zlX1Tred;}|pZ}62BuxmUM41|+!Y~&=?bUUG{5&Ah70i4_BI}sT%%~!yqELFlw;l5O zN%S6EaFIwY9&-Z@ZP<^Wtiq~YCE4>lYv&Ba$g(~kQ9m~s0P1#J*L~Gf5Ht^kgXHOA zEo@5+-(OL04loDARqS7C($R$tOChwx8KoE*`Jmn2gnVSY&-gFi_y@g|}o zgSxA`Yf}|LjFz4pli^HWSKTzGO|lSy^0+8MzAClc@oXD&0z0wL;~|v+5`hh%iML4U zL|%OfDdeb>-|2&sXC7%>%?yuW!OV5|a^DJ|JHbjUc91i1Ja8sqw8jPk9TIHCp%>1< z@&DlJUTIklON9^gi6au|M1;ud+_9XT3gSh#)eE=9C?S@$L70gU!g+J&T|PC=+ihnx zzGzH5SnKug-2i8(Xxu=aZ1S0K{LK(8ogt4xc~^PKlbPDZFi%jF&jd=j4iV3^W{9(i zz{v(NhI-=nIe}zBdv>M`jF8A2synJmw01_oWgM-y>7xEkeG~AwFqx9)uAv^zp{7k z9QuDKgPSCyG*FSKVBN)-hRRZ;xa57L=V)~tC%&35{DC5$go6sKLGnV_>k%l z0cEW5=vOI%;{W1gyf2)aw!;-u8yKic8H&s)(j$Y?h2kho2L7bXAF!Y$x7Uaa*;AXq z!wQcFX9&7mM6L_YF<>J>Q8&Db^e3K`q@XT6?0fHa@SmJPJx|h*Ulr40VxhaO=5u9& z10$M+XPN5TyYEqViLj$ zYZAVcu&!W&>wJQ}#zKIpoeb+L{M%${A%=KKVpq(K!}RgBJfmut!NhmSyGViRUV_8r z{XMrNEk&3{jMFGCKO-7J0=aA0CC@C;huY|Rxo1+kYQjxbVp5h1>+4j`n2C|TX6Q;j z6^*TYKVh|8ks07;yLe%TV$26cpWu;myaU91EuVgiL0?iub*C{iSZbcbFYHh;%7cRw z+5JWf!GEy8(tI^{fz(aYQVGA)?jp5H`Qg7V?DcV`n>OrNBnbKX@bP>{(l;WObd8o| z+tmWww;jgrmh$}zL%33;-vs%+D3~)MG_(~Ez{5&D-;4MfQ}b(>s6-=*Ztw-g*TToc zG}=KHdN@-1Uj!YZWf|x1%>1(Ml7&MAiGWXcvmFYMFCM0kgNii?0`wxZJ0X2nQ#F#O z?6K3fD(Qd!^wL1DwMp+;3&N`y35a?$cKK*L&a7(IUD40P*AwevNE9JL!@DJxTY^Mt zTo#Su)Pz*RKqUkf^mwzL`gA-RRI9p)d0woJ+U?A)v?DSaX^I0><`HMKNB>F#!Ds^~ zf^U-~XJJt$+EukO^JW?99N%G ze4X$76Spevy2CT5nK$n>1gwyoKx7A)TyCH2#{z(1X%$5b&0O2R$$Lit1zkbb5yyXP zeuhM5<-*k?De*Gy_&S(nFUH+d_ePt%T)hoS^!N&#ktIqgRiX_)-Adcq#y_urc|Piv z*qSr%a+^ociKu*pWLo>OWYi}TZ{BP@5Yws&RKrAw`f1^ zUVuWd&-?u4mMXDZ!z=J_-mh%x%FXELi}+goJYO6Wsa1y9l+~?R%J}`1f7dM4D`zxH2T`5RnRfK%1Zf*?T7u zfoe#G1DEO3QcpN5qduDxOVe>uqchTkHH+rmo@Pi}Sjs;NGCPq)fE68Nl;()px}j+< z_JTHsm6AgD{wMyn#eiNWrL?~X;#U7N4#XT4*)HFzgc_C-i&g3cB~sryIh3m*G1nH^ z4L+md<4`GaE5+{qYRhut8q_AnHBj4cTp+usi&66Jg21uNwOU(@B;7Sqyxc=m3(zjkf`t&`~>S$Mc#1k)T71gEMh?Pap>64%>@=cV+=)nc-?!O{yNACckr= zwr2i@o^1GYo~}acPcKR5@0{a4Oxzzkq8OXjrf)qd=zSf}&t_6~n~_@Fts%^uX(NXkSB6dSUeI{iVn zb$*>+u*L!Y7k)q{eY}O5n^h;{LNp$E$taP^r0sN*ndj1V)kJh%MI!Vu4i z9<%Uc#9yA4sObyNxKCw8>_gaRYFIPhYegIAYF8(C{rYd{fuSSO==qt$GkU~JZtgp)RK%airWs} zVLgD11Z@kqGs{YS%XPqGfABRCBQ0@~HO7%n}TbGxn6IwxLd|bbaDq2V>8*vr9 z7QjR4r)yeGU)W|)^|yYt*3~&<3I;9iI5vAE6=t6g(d?2Y+)#)lYIYhtj!X;uj2h|) zaM!h_5$>bnXCUyFB3egxvg=LuPv){sEB#XAU`m~ia2`WNv3JXXZTj7vJ`nd;Wvv4} zy^V49fQAL*vUE4Z+5r(?o4hiWA+aR$@RKW{Fz?yTrkzyz zG`dp~0@A*G*0U1qazZ51E>_BF7 zX=gw;15?dtGaOQ4DlPt-%tuk;lKlFO@xx`x=Kdx|RtITwD;A3Cq?}N_o^Ez{)H`*> z9ao4OJz{~5W<34;E=w$`90IPA?-BgEj4GP(95TD)XJJf7l!RahsZSrilRMXx;~;On zK%^N4*x0Ea07sf8fhfBdxKT9Q&HIRa7#{XP@P&+saOA|g2v!6|4UMEX-;g!-Hj%gt z)DO(jlUy5!;{V9*WUpMmz@RIu@-Hf5W!2rVFhE3wIA{v23&w+Q&Ywf3_eUn#Fek0X zu2-S^9)VijwfT=2g+?wT`kNf_tW(4M@ci%@j6HeZLYEtW-vA2I)H9CPx*IlXMW@SM z;@Pm+-3!6p`W?LA+n^@KXTiRyX_6-u;H(=O=V#=4cbjh(kwLlil?4*fUyK`n3Sxd` zfaVf>@V{gt|EV?R92&@=ay+8hee1vKZ$-k~DFebaL(C5-X`Z9~1wnbtP~Y53qn#sQ zRN((TJANPjExbj{F^*l@^cc=+$`AQ&Rn>uhRdwqO5cU-CNWbd4K&F;zRU9W<^b){J zVsLPG-FdHdp$mWOl@B*of}p9XiRZmtaV3H)A{GU!htSjZAR$-lG~RYUT}&`jjclun z$6FWpQgL-b`}uLI{9Z{#pQ=}&1BYnM=_AS|2nCGL`)8HU-k(9lK|IY@*@Qr?OWpnm zb8TmN+88@K%`ds(>|B@Lww%oqO>#Lvp-gcbvty9X5g z`%5-HG}SvHkM#FcQXwCc|Mg~|^1k4QCHBnNs_{FTsnT)a26TL>{Qf-+&j5LZkm=p2 z?9TGhv_^UajS~P5@G+D1S~g`0d@=P~=-odT#L-l9ZC)}1jK*k52FPM#i4D7gTr1{M zGLJg>7IS?%x7Y5fW?0RDtdBltyzME$?5~xCWK$6u)rvMZPk`Xi&}D=tg~NAwU$$LB zapZl{!+XxhGnQ%jLle=bNEXhBCyfo;D0WTp!hmUz;HBbdf3OU| zxjo80W;8y;T!d@6zkg-&539B0Q~^m)GI@xT5Zr z`ps-Oi6F+NA9O2A`y}s-%voKl4X*EU?Ym@}WCALsHs+I5M;?_a#zsHC|I|D1vx@WZ zes=U*6=!Q%g2fK6gNMy%x$pPd@YpItA19kDyfabN_^aZXW!#t` zS%f|6qD+I}AFDWnPx{~llwYr1mdB~opOqxqy@~1CO37NjRE?^;21Ta4l1xlBsdMb6 z$IEf+tqv0{6gG9hJE#P(iZ`rLAJg$j8P>%O%5v{A#D^@moCYJ^A+7|GUJVVJTk8HR z>=DI$(8PkemU{km>ihz6iQB1K)i?OH!BaI_3B$(7@ah}r|CMf3i=m{j6FO_1+qTgZ z{RqdgSx7tCukCvHs(8MavZiO_KywunX6Hj1^Mm|v7r3MBe#Ar3C22?zKO37$GM@{T zg#2=#L1i%YUtwJ%DhEM|{ zR4C&g%37QL{H*o#obAZ|ph-v^1588n+(R4F!DOpBl6nZ)xZ`D#DvOfW>o=ot zEa#`5-6wsyIeseqS(U!!#iT{oRkjE|1{!`i;|~t70FAIko!>--`0mkC*wgr~j+z;^ zl}h~@tgHr&@7h}KrV8BeVI}0+7LJ;2Yg8s!3&^!C~>>h-< z6j28TM<2CpwCfyJyNWFUi?9)_%u7$px_gE?n% zgaWr*350#n$$In7=4&iY ziVmL!`6}WwnRXL6c>Z9X)g?UYaoymaivUiG47WKSie5!$8p(P7{u#=DjyGp9g26+W zchQl37qw1y+u3e92!4+3+uI*LCm39XyE}Z&e{I_|bfq=1iO$u|X?f)DY}Z=hubVac zr6b&%Lw-}wv4;GkK%sV1zip$p9`4yR6pSqlBok1Nef~X^G}`tNNcQO^Kh^CWY4|r5 zA%(C5?uShfeX<0VkULJ~1e#`YM$GSKAOlpXjaBr;E%LDy>}>3aJc3STv@?AJ12&un zoAccXJfJ)Imb5bYdKFVNm>ti&Y&6K|TDN}6mmZGQJODVg?f5?C9D}T=a0(h33tSjG z=btDRq&#rs3B?WmL$5_>fjUagck^U3c7Zc=xr+97PnFQKf8LBoD1nF zY1`ds>z2)bDuaRCCX&rN)MAtgLRqkW;q9+$MZV|!{gqV$$E%snw}mG+V~btgL4acF zVrebdoahQ4&-VE!&C+hpSe&^&d2E)Ug@4e*sPa#~p+v?;`(53jBCC%6l_VN=fIaRV z`E%}x5nh7Q-W$1h#0tyO^w2LL&Pdv_xkYW770SofyIoJ`wHa8GAIG&&!qNXtiIx?9 zoQ}-h9rx3| zE&jdseX|!=LpPTGT#_7mSH-*SA5|(R!+*Qg{_a)neFxvA9Hm`Rs=VwyqJr%wCaj>( z1;K@vLZyc&M6R|!tpg;Y0fMDrtzSIsxM=oKVmiK9Ce}?M|^qPs*wN1`JLB@ zyxw`t!5pyb=1Mv9BmVPV|J?6W$T1}72gEZQoLeU|`S!C9l1A0RLGItBEpZk5`azg$ z;pPke>zg>VyAV=Y^i<0oz6zAMy*e`a_z9$nMdOpx(Y(r};mtHHT2uc~0CXF))M<@< zYY#R);rSRE9rw7BJUQaou4(MVtmAHC$t|$Vb=v(+g@4fZ`5?Oal|oD8L5xZ`vIBR% zZ3^MZJ3Ck31ev;`SonTTeVr}&&SA5VV}7bPr5lQ7MP&8)t;c_5xh=R`o|~>SY5F2v zR`^Z-?&~FUlh}YcR+mw~6`hxmr6mLXCq^t69#B+Vl%%YzVE;gWsh2A!;tLUB>_OOP zhB7Oh_hpwWN1b~00X|Gzk6TAaK1Ke|9okRzj0gVtenv5>&jhLFlq-W+BgP0LqB1pw znUcGu^NX#iE42J$QNRxZC+Q9e=1mC|tFz}B!vt_c90RXTNaX9QSo!@!+8r1|keS8i zY5wmcd#x@)lav(O5^UuzU!xEHwp^lxiXMj$mw&Xd7y9Qc`b2(*38IbJ(0a+)E+$~B zq+F#Uyb;>PV>Qv(ICR5)nLI7MtxCHl9|@S0aPLY8%aNA}sE6=7N!$%Tcq_HVEGzA2 zOt!gnn|#8!onjj!d%ogDqzVm~prIaDi37O0GZZV3I9{j*YzWYiY4VIIsFeHEnI|fV z3xrmv`}+7OVVk!5i?qDxM+9Fv2lQOO;*b6Vrt14BI4^Y#-pYxqb;;1u`}UMlT?%cP zDD{jOJbSa1M#lWE7^S-LRC@R$4YM65q?}|e+biyyQ(M33YIf&(@l$&VK_5_6O24=Y zk1#sSKV`)EeSRTz_@>Yp;+F}te$_GVd%de~rd;4_H@P;{7`w}Hh;~eAA$Ft^cIYN9 zX7fjHKK8o)b0o6mR_)GuA*csbjFJ1|*Vfaos@$-=1X}xeRgpRM4W}w=bpFSz#_n8% z*MJJw70D4tv2((7dXmX*Myfy}jD3h*H}qxCjl6HeX@|2)%ph>Xucvk)@Z#yzbb~Jr z&av*eI%3n0{g-zH>J#%5?$el>xI1paet}Qm&h&lW#5u;TW&(Cs$3q?P?yL!7_v^aH z3DE=wDfjAA+r{*@K+ai>ad0R)=e#(@v~StXOMGDW>p3@iT_x_@@6O)#B5yT(vkpq= z_S-OwesPc#P9JwRIlq3gGIY(4O@+lzWO5d84<@vi z81!DxV_P#56Lt$~FL*fjblms*^J-i;pbDKXS{zV2M?4Rt9|#frj@J^hH@+4IQ~CA*#b*c{rZErN*9ldyO~e8QY_ zSNHtmCFS_zJ@eZX{js>XcyhZXi1q)#-U8D9$A33Mg%94TxaQ>KoKeU(`^D5tfA05fB&5ZhHzorQs-;^DS?PC4`Zt> zPGOikbiNs3AxVu&gN`>J@84s`D*O)|kNp3G*=Yafm1yilNw|lb&BA^#zNd1otK~b648?C-TJp z#srBzwCcDP7p;`fVsDehNyN6?DtG26mn<~U`96qpm;d`GqQrwnjqSsE(aWBcKr+7+ z|I9>QQTNSD!^Hn1y4U}2kl!!&OEO_f|B*z2MR1Iej-P$5hYg;LNbpGAAVA=a^S`kN zImY_Gk=*mIog%u6f}i^TvG!hZO+{V%;H!cth@dp3D=G*gD$<*PfPi!oB$TKqC6s`) z5D1Yb3IZx1(tAlDL`vuZ0RibX0jZ%wXrY7>+Qj#p?>`rFH@{ieIVXFcbJp6cK2L)g z;vb{Efb1ckooM>wmxm=v1W{pOVT$*AcGy3v^#7Gw1`X213;&O-Uj6SYqyO}OEIwroi;>B{O^L?z5nS1iA4itu94j@EFr-nEiL^53~rm5$@<0h z+Q~_mT!up3y(KEDpACcH;BEEXbWQ)SR6Y8)Hwxp8*!GuBPM^qRi1JRKCTux3fihwj zGu^Kv#8q|H9qf^Qyp_=KR9E-n+`H;qNxo#~|Fc4gkbj#8vn!AO7J+4EW{OEjXuf@W zyQ8y{vH0w`!@YI-V}1GR)s0vA|L=b-L44)n;NVy`BXu+)?CtG+UVV*UL(uKt|H%LD zt4a77?3Pic@iVhajKzZEzRK@Jf&Fx|`Cr-J`tRl?-m9lv2za