From 368a8da7dbdb7bbf991d7919b2eddaeff4511945 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 12 Mar 2014 00:49:03 -0400 Subject: [PATCH] - Add data classes for notifications - Add basic API for notifications - Change the password required to be a notification --- data/database.py | 15 ++++++++++++++- data/model.py | 32 ++++++++++++++++++++++++++++++++ endpoints/api.py | 18 +++++++++++++++++- endpoints/common.py | 10 ++++++++-- endpoints/trigger.py | 4 ++-- initdb.py | 7 +++++++ test/data/test.db | Bin 171008 -> 483328 bytes 7 files changed, 80 insertions(+), 6 deletions(-) diff --git a/data/database.py b/data/database.py index 413d2261c..c98444ec6 100644 --- a/data/database.py +++ b/data/database.py @@ -271,9 +271,22 @@ class LogEntry(BaseModel): metadata_json = TextField(default='{}') +class NotificationKind(BaseModel): + name = CharField(index=True) + + +class Notification(BaseModel): + uuid = CharField(default=uuid_generator, index=True) + kind = ForeignKeyField(NotificationKind, index=True) + notification_user = ForeignKeyField(User, index=True) + metadata_json = TextField(default='{}') + created = DateTimeField(default=datetime.now, index=True) + + all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility, RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem, RepositoryBuild, Team, TeamMember, TeamRole, Webhook, LogEntryKind, LogEntry, PermissionPrototype, ImageStorage, - BuildTriggerService, RepositoryBuildTrigger] + BuildTriggerService, RepositoryBuildTrigger, NotificationKind, + Notification] diff --git a/data/model.py b/data/model.py index fd3b51855..9eec9f841 100644 --- a/data/model.py +++ b/data/model.py @@ -93,6 +93,12 @@ def create_user(username, password, email): new_user = User.create(username=username, password_hash=pw_hash, email=email) + + # If the password is None, then add a notification for the user to change + # their password ASAP. + if not pw_hash: + create_notification('password_required', new_user) + return new_user except Exception as ex: raise DataModelException(ex.message) @@ -662,6 +668,9 @@ def change_password(user, new_password): user.password_hash = pw_hash user.save() + # Remove any password required notifications for the user. + delete_notifications_by_kind(user, 'password_required') + def change_invoice_email(user, invoice_email): user.invoice_email = invoice_email @@ -1535,3 +1544,26 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid, limit): return (list_repository_builds(namespace_name, repository_name, limit) .where(RepositoryBuildTrigger.uuid == trigger_uuid)) + + +def create_notification(kind, user, metadata={}): + kind_ref = NotificationKind.get(name=kind) + notification = Notification.create(kind=kind_ref, notification_user=user, + metadata_json=json.dumps(metadata)) + return notification + + +def list_notifications(user, kind=None): + query = (Notification.select() + .join(User) + .where(Notification.notification_user == user)) + + if kind: + query = query.join(NotificationKind).where(NotificationKind.name == kind) + + return query.order_by(Notification.created).desc() + + +def delete_notifications_by_kind(user, kind): + kind_ref = NotificationKind.get(name=kind) + Notification.delete().where(Notification.user == user, Notification.kind == kind_ref).execute() diff --git a/endpoints/api.py b/endpoints/api.py index 22a571be4..117ba4983 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -28,7 +28,7 @@ from auth.permissions import (ReadRepositoryPermission, ViewTeamPermission, UserPermission) from endpoints.common import (common_login, get_route_data, truthy_param, - start_build) + start_build, add_notification) from endpoints.trigger import (BuildTrigger, TriggerActivationException, TriggerDeactivationException, EmptyRepositoryException) @@ -2518,3 +2518,19 @@ def get_logs(namespace, start_time, end_time, performer_name=None, 'logs': [log_view(log) for log in logs] }) + +def notification_view(notification): + return { + 'kind': notification.kind.name, + 'created': notification.created, + 'metadata': json.loads(notification.metadata_json), + } + + +@api.route('/user/notifications', methods=['GET']) +@api_login_required +def list_user_notifications(): + notifications = model.list_notifications(current_user.db_user()) + return jsonify({ + 'notifications': [notification_view(notification) for notification in notifications] + }) diff --git a/endpoints/common.py b/endpoints/common.py index 0fc3dc3da..0f0e68df7 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -5,7 +5,7 @@ import urlparse import json from flask import session, make_response, render_template, request -from flask.ext.login import login_user, UserMixin +from flask.ext.login import login_user, UserMixin, current_user from flask.ext.principal import identity_changed from data import model @@ -120,13 +120,19 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token def render_page_template(name, **kwargs): - resp = make_response(render_template(name, route_data=get_route_data(), **kwargs)) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp +def add_notification(kind, metadata=None, user=None): + if not user and current_user: + user = current_user.db_user() + + return model.create_notification(kind, user, metadata or {}) + + def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, trigger=None): host = urlparse.urlparse(request.url).netloc diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 41c32045a..82a3284ab 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -203,7 +203,7 @@ class GithubBuildTrigger(BuildTrigger): try: repo = gh_client.get_repo(source) - default_commit = repo.get_branch(repo.master_branch).commit + default_commit = repo.get_branch(repo.master_branch or 'master').commit commit_tree = repo.get_git_tree(default_commit.sha, recursive=True) return [os.path.dirname(elem.path) for elem in commit_tree.tree @@ -283,4 +283,4 @@ class GithubBuildTrigger(BuildTrigger): short_sha = GithubBuildTrigger.get_display_name(master_sha) ref = 'refs/heads/%s' % repo.master_branch - return self._prepare_build(config, repo, master_sha, short_sha, ref) \ No newline at end of file + return self._prepare_build(config, repo, master_sha, short_sha, ref) diff --git a/initdb.py b/initdb.py index a1e2fe646..561d98992 100644 --- a/initdb.py +++ b/initdb.py @@ -224,6 +224,11 @@ def initialize_database(): LogEntryKind.create(name='setup_repo_trigger') LogEntryKind.create(name='delete_repo_trigger') + NotificationKind.create(name='password_required') + NotificationKind.create(name='over_private_usage') + + NotificationKind.create(name='test_notification') + def wipe_database(): logger.debug('Wiping all data from the DB.') @@ -269,6 +274,8 @@ def populate_database(): outside_org.verified = True outside_org.save() + model.create_notification('test_notification', new_user_1, metadata={'some': 'value'}) + __generate_repository(new_user_4, 'randomrepo', 'Random repo repository.', False, [], (4, [], ['latest', 'prod'])) diff --git a/test/data/test.db b/test/data/test.db index 9779208a8859fc77e4860974aa2c534dc515b959..dd1b56708da5e40345ece6be2635fa96e48055fe 100644 GIT binary patch delta 20366 zcmeHv33yaR)@awgz1&{zEg&mo-yi`xz208JnmuF>SqWh1Eg@zPNdOUIt2jCixLmP7 zWJJ*cR1gO>E(oZDJMO!vs3@BZB04TO<2(0ufIugX|2Xfx?|a`%e-yW>PMtbcbxzee zr{_omEf+HZ*#wzg*qpR_SV$sz9Mj(O_-C{@IaG~`bbcC^O|#qAu577n(> z2)IKd$lD`@868?Wt#6$0K${UoK~64x<&?2PcWWvdw{v~#7!+L^sf7rJxOtaLKIu~RWgN6QPHgjvv`FFf#JRMeW`(ke zS5YlG;nTDhYN1wGoVJj@GFBthrp6+z(3kp4f3I2#nh(u|7M^AHOv&Yrb+|k|w zDGlweHs8gGn(W!RnN_**vr2Pv>*5nH&X&p~#+Q`_XC=m`CW;sn)BGHxiqPv(mcLAD z?Zrqgj;jajtjf$P&b<(^(2$rf+?>=dut_V0za%ybNr_hp83{*(f%vP1nD~{kz+6Qo z5ZJRIh&MnG7sWz|@OLu&-Oka;C+VH>$p?o7LOe51_)axSf&U@H{}=&czVHt%mqX)I zfJ)XA;9i#2ZPux@jheq}sx?aWy=o29tU9HtQ>m5Pl&qpv{;hnjTu1MqM@bxf3Bt0S zvX#PUW~M5=TErtGr^ZpqWY;&SP8qfOpIk{SK{b?_6rV;Bs<0DFo&N9`Qc*)$$cEFX zL2gbgdx2bk5cS}U(@*#MW24lvYMCX$uO~bHh9(i`r>F_re)vY_j~O?ZGl?Wvs7d65 zPf??CYVVju^<&A_%PA9C{25A-Yr}VElZ~IDBDqCh^Bg((8Cs*95`X*0ec|N9asYbb zbCjtEOdY0_&26AjK0L_Y&%GXK53xJWoaq;|Ve!-R~NzB(!%DxZ! zd$eaqls{ZS+P+2>a_v6oZ^f$*6_expP>(7sKSS@2rOAp@khktx)TGL|R5<02p-9{} zkdpBn^zRJSyua2XviloA_}+79Y%Pp$*L(=trbEygCo*9?8z;j@@EiCMd>6h6Ux7QZ z8&}~R%;OkLafi8=xt$#0uH-J|D!8d!Di_NB%zn!Ljor?!X4~0P*3QNSF({LYcE*kV zlFDY|W%x}zfREx$+(S5)yNa8JX>LDz1Mb2%vt>A!Tgx3`$2PWu{U?{kg|VNp z``8C?HTH0`Sw}GPG_*55>Qz9IRi`LNi zNf|x{bUrx(SbRk&P0I{DzC}F^LnnqXik6u=`e1VI>~8LJ_4pck7rB~zp$LFGU5gei z?&$V5bo;t`o4b8p4Rkh*cgyfw_&DA@LQM$;VYC4SRYZr=CKOx|6-pb_K^2LkDjPLI z1uZul73lwX=2V3me=ZKhSNQW02+I>bpOL7>hh+GBdED+{wsmHCdUOiN9D zMRs0Aan+n$2QT1?8a}5h%Y3P?Hrv~5ysWCrlIy6;E?e3=n|HeMOZdDw&93S~LqSdJ zrLJD|kMJmtpOFfr5^Rhk0^t`<0?3zX0t(SRwy;b?Xg%z{yy*aJc z%HH}}dA{NzyQ#z8wWui9Q`{?jUon~QF3)akahl9|=>@*#rpuP*cJU>p>9gv6MOjN) zoBHgzl_hzzUAEbEbrr^0eC}eSJI~lF)Xkp6*Jdxi%#&3)$8hPa!VXuSucfEB##!df zsj5vct;i{E&QDJ_=G(GeUA`=93t!h_T+)@BEj&7V0?%hT>f3XgdKnCeQB{e()K*-SZLh7bsO~vX|~;0YxNmCuJ$5R$3my0raW&+QRbZd>>6j?;_6mQPldOq zSNNcEQW)-);Tv!-{uY0NpT!$MY6tNB7qP_7c;CkgQd)qNU>x{aiu#Y3EMf?kiIWT^>TzdA)a2@guOE;LKQ7X#{w>^2h49m2 ztqOO^V10Znxc!gNWaC>%*-!o69-#6R(0=uQ*8bw~^HLUQ|0x;%5uXa8H^A>T!m0j{ zsZn4qp+0I9bql>rE|WLOzf#mIjw{`&t5heLdgh>dp89zWr@2ORK$obyT(^%+W|y)r zU={Aig+r_?c?>Ax5Er83AK4ile~2zTtI7X&6#4&K_6Qh7s))EaB+N@4i?o~iQW4lk zi_@BsXgaka(T=*BxyZnpheX4x1qG}y$tGKj&YNj`_=xtL0ilb`zwuGOZZM{!^Va25 zurbe?owlgoVyIqV-G(4^-tf(1{=KC7HOGvCrT-*+QxookV|-{Ve+d%$td{Ut_*`e}?ed{v;uOze@1# zj~&KvFC8ZB5)>mebWB090ZVyDM>&0EZS2mbtLGpTCp@*%f)WEcnroaWUdXw|rc9EI zMFCwCC9JzgL^UP~lh-Dr6ro~mR#=iUA|(uUNS3e& z)6m_~>Jw(IjS`+&8#gXl84(wTJm+&oOI8G%0EI&YylyO@Nn4kOQUmlXT$hE$3-_&y zM`^;#>pnme1iU^%xM96YD;8?=wYh!W6NT4qn~qW;ClyVC@3BI~`tbn2WW5nh4xrh+ zK5N=lvWcpQloYfwJIGibst@ajHIjuLS5GEsi6~YsTJ!>Hi_{G*L~cq%F(J?rm)F(f zYPfV!M|)y?Rc>7sNyk9hs+An^UxLCfEOW;tD4HFLr9mjaEwMkXeUd7IPeFbx_0k0Z zk~^WWZKmk2BCA88JTx(+NGkD|J#h68x>l7n+)&9w}IBQC}?S82I%AoNeHnn zAakLwTNEjVhG}X{3?we<2uLPF3%3a^+QD@ntl*`yCa}Ti%md7a7gXIXh;`)xj9aafs&s<*&V43CBm`$qSW~7KwG~ap{hzy zrNVv-)J}jpE-GcG@$nIGl?ulP#xfM1FT zy2HWSc>6gX`3F7|)QA9c%?+2+Y9z4~9RPnoh9AK1jQ}1xKb5XVzop641iD)NTUr#T z^%)ud3_mkMt*KEX>4NgenY<8X|ahK)Hh}>G9uaJxUO3P{4Ak&J27J`n;&~ZwjGwE04eF~Z4S!Is$ zF4ad&E%S-yR_z4sqq@1euh<8Yf$?i^pyBRn{rOAAYc+8B!VHi8C6Z{EVEe z>_$mD6+b!3fL9~*Q3n$+V*NW5lPvSH_qUSJQ!5{{B^Z1A3RQRaq5*}%lEftP_60H)KqGKb9 zM452x^Qf^pv$!hE;ZAVdxB~V^R$$G#_jO%5^F{LoI0b!z`A}4{)dp*T*Jstcd?3SC zyB($qletmvHS#9jV>a8pCcDseC@0zJax~g`uQ=oLjWDM)HtNA8=g?cM4v)*_a@#B> zv+({QKG|n8+I>EgM{jhRV7V}hYmU!s*EgEYjUKnxYqz`Xg87SS$u6tKX7yRkdauO= zfV|tOcNwi#KgE-x?q z_{HQTw-1&%r(5r`d2M>D&Fav*&Ada;LmjX}K^do6DEKln*=F$=eQvi)Z}Ex?n%iiE zRS}AKJ&ksg)6{740K#owMkm|cUc1i&IX)gHF00)I#JSu?Xq3%k1&mIU)h?X)vLKn~ zy+&W7&8auJjaFD^ogTdt+Nn1=9d@Uy(dY13J;I#B6O)Y=pT)?#dA-SNG3%lA;(~1C z^*$eOHhG**v&H2QHXr7*tS;UOHCXj_qt675c3SifOCvy=pbNp`u*2YptCJS*m4V-R zkSoE1_a1DCybPYdy%&wRv{MJe9wO{LnL8HCWjG2ua49H?f5BeZLy+So_{L#2B^W!s zQ+^tuB-|%@r2h`Cybr*Q_Z)r_KZLi!VzdVT5quqe;H6s-R3-B#IwTy{3nYgf2Ow7b zq$pbG`~HuqBJ~`8M4akHZ{lHCV?P4wp92rt8{lt$89#-e!1v;NgOR>Nt9A4!D6XWj z@W?M^N%$cdehik2Q=sj=jsFIOf#V!j$}{){Aa=L-3&!#mV3A8$C`=xrQAHBoKB&ck zm>nA0mH2T$xn2APqj?3;P-oF>SE1S@{P@{6o%gD~1=PHV_W`0k;x7nI@&Q_<5wR#R zd0dO~ZoM4IlR12cINy8=-q|m~oBJMg`-}J)ybC`Jq}&1H2EQOo%#*a1LlQ+Q1TDKG z1T`k%buzpWCZn&Qi=PD*Y8|xiZRnPtptJYGgaN-`L=V&KC>S`ADh5UklXVfORUx8U z7X%Cn8(9~Hw2hheIVGkF#~f#2aeZM~iPdN`6_wh|b8N+Bm9uN-fH=&_t(a5KmseI) z%`VK$tID$4viPbJXH892Wu3K(3D%gL=~jo$X5uN4V-S8qhMyR&e&FrIvK(*-tA8&I zfqKqB-;Yp_o1B?|Gz$FdAd`?riEosFRFTo&qLhoa%E1TUO!wD=ny#KdT4we&nk4 zm)GoYJIu?Lw}VzEVovFmc_GaT>J=zjZkR|sb|+wlI_ze<%crtgL6b9i$sPmBG>Ynq zR+%SL<|%im(p9&qf6&~fEzq9U-OK$8cd;?-R_Zyb4t@<|1cvYBGZvL95KP`>0LBCFzoq@gNRF$z+;egksyQ37JfqxYfFx7ra) z9_NvREl@?&I+4Fpazc;?6Hqv5uptnkA_(>-fPQidkH(V)HZ&77ygO|u50y#^5uhl7 zh7{W&$glxOhaHV3`|Zd=UtLi|A`76$$^-4(Ux?I_P9!<0NF+vy1J4zpO$Zf}mO^Ae z6%s~bEJ8~1SRqP5CFGMr=%?A|(x=?~vz$TrXKBJ3f!Q0zD9#z?zX?<(+3-k`juAy@ zeyEZafqYm7Tv?KjuFYs}Z|-S! zwKiYY+}XGPjYiP75fnD(tWF2t#fE=G)vXD zt5ccJne|K!IOOkCO;>)VEK)o)h?QA|`u(AiutKZVNR}DruMVNkb(J~ZXw0zMGI(dY z*~)`r2nJ!g5q_p+iQT@=jzlo{7d5waw)zq$$0siKxfgbHw19I>OfSu!Q&3u$T`;@a zl$AHfQaPuzt}3sR&#KQZGnbgGnZ@~z3UhT<=B%3joa(Z2Te-b5uhg7fKig*G3+v6s zIf={tp%Fvq8Z!)C_H>KcZnQam7hU}$5jGNC_2HVc-R0pfri#z7SkoOQquDu>D)C^N zbdU>rrm@?%XrY)Ob{ceJ|ImJ1?g~4HylXh*=5z;d+6$T-jlpN(gC`` zIxeW9CG>o^(k#_~;dqYw*HbNyAw_qrCu}!cGAhG~BdcBFu^Fp9Our2{} zO^Gg?|5>F8P7=?jScZ*f6MJb9i+`n_U;*VpQ65|1RE?WK;z?Y{0s zVwTI(Bc4`*NgU2AotGnb@6z^GS9gxe9bu>}Ml)}+k%INq&1Ju;^$jY0iOZ|B zBJavzzLzzrnP6w-d8DpesJ!7S`*;nNN{nkLIr-uiYQk_3P;wk`tf4Z8%Y@=->N81NI(if02juhTXjSscxjkcg{iK6>&rBa8_K5{EnA?ENw1%c2o;wIGO;j`}| zl)YiNg3A)ma;iAbiLAO@)E^@85*e6#Tk)UpAMllUIhfaHjr7mK3VRofxtCzA-XmF- zo8X`h!8Zl1@XSqgmgrNA6oUvckX+(n;#a7&;SwT+W2A7j6pj)zvGmDFA*iG+sL&dT zSIVH$XTh#MZ*lK}(?Ng5+wc~=2~7EQ_!c-j^hY>&^A$b-r*&QmCg{)fR1yEM1c@0T zVlYMwMvFm!81ah}1C~71fo9x-mrNWk8G_-G zp=Q2a4=eFcti(UjEP1RS z6Bi4LFrt>_OZ3BgB=^(LuMrH zm-Gh~+MzmSWGtPDE>uCp?6ay}#4y=Ibgb~KQ_Ep71ipPNs0!{(pM z*8Y07=GU{;BW3po7At0?{6My9XtptEDNtUJ9kdiEekHpfR!aE=`9TW;{p;BmZ0`jx zC*lPi1swE-zdjIF>4FYEtocHdqrQ_l!;GD+`in}YtX15npyV$4NjjWrMSD=BtR0*x zHd1pK-8&F|03D+TcD#(r>4Aj3=o5OtvEVos=+YG2bibz9hR?9J>cu1d936`~wa#>fxJ+vqRpJgN*lIZJkqW^xeK>LIso zL8+*j?Ad~HkekT2qGYr%;3k>A6?_RU(!LdqLrvtmt>{CvfcWl3PSi;5xEFb7zr2B5 zejlB=Ct{;CdS$S*MDKh(OnvByyxi zOTqks_HJ}5>R+aMLG&oU3rj|}cATbG6Q%w{{jmCa=6U8Z<|<|p<6$b9Y{tSQF(C}C zI<5L#^(MHDkK&J*rA#w3hndAVnKWiBqgMT>I;whK^#UBvh2u`#KJ8lVbR36S?i=oP z?jZLFcQ$HotZf&8~pp6Fy z`9A!ZcC=QZ`G@9^=2gvr=61~r&3sL%CQD<{BxuIKy`b;aU#QpfeZP96`X=?2 z>OOUox>`L`ovLP;lguliLTrS+h@0VB#zH&`*D`yU9n9U#TIM>C4cX4H&-taPkJH&ERwxJX$*H zY}7sE+r`jfQu+d^R6`(YFZVK>x`rYBsOSSyiq4y;BZBM_~Y>V@wrsoqKn((W`kj0T6v zFk6BQL@Ojn_%4?q%K)+;8SK2lZZp{JhSC726fFsW@`hpwv`8YzZZz0U2D{l{w-^co z)tpV46(}o33j$@0hJ2}Po>bPx8*Da%&2F$c47pNSDJ5P^%8?2OAhM;x@SP z7>GKh3gO!!RcMzWfj3P~18+9iE{yV0!9cDR#t@NEOoir4L=2|d4OWN2>NJ=oB8DcK zhTyW=4aOn34AQxJ;`%4DlKmXTk_wK>CZjoOrhE#$3`rS`)ohsLpnIhlFX!nF>C2=@ zGtG{%Us$zT^T`PU2fPeM@Ej2-&L>Ea3af&2hQ|U8WkqEsk zeU62>)}*DP@>s#*i&7RNPpWjssycn2ZLK)qpkh*ufY_u4|#9`%MPj z`82JH4iU385(2e^8~D#)H!zZ@i{B~>eA5z)$pC(HtTu>7 z<}x`*^m23JEma5MJc`M4HW z;3AxhXJ99`U_G9I6JS#~3}dW@%WnVVzUNMH$GI=KPq_EFH@W@XbKFzh6Wqhx1Kd__ z6So0w*WJWj&#mP8xux79u8nKrT;O%C22;Q@i_7MwadytcP2y6xIBqN#!m*r+L+ly$ zJN6`dl>MCjh<%rRoqY-J%sZeXutuVR<8i`g!=h4rxw>>Rd| zErIJznd}snXANu`n{<(F0o#9lqQx^X;U@Gk)h}M)6Hgz0J;Gk@7BJ*)$G5`f+#m7P zcm-JJ-MAIZngzHXZV{H^0-S}XVjDK%i8vX@;z&FO>#!2bxSzOhxxaHqxX-u)+&kQB z+>6}bxZT{N++V>E`U|&_yN$brTiwrH3s(;>=lZx#xPa*8F5&99*<3M~$Iaw2I4hS9 z7ZMY>XfB)^#c4P>`!Dtfu${hQzhn=xAFyw+udvUvPqVw&N7x=mIk6m~#chNxn8gz*2r|?1% zpcniO3t_BSb|1GvyHYz%H(z%T%j?2)C)nlM{q%flKl4u}MYBy+qWO+_m}!H>`Ps-j#G4g$iYE2Bd)^fhybDON80|kQrqNU;IdTM*t=TNX9)qMi-hur(;EM9Xf}cnUxj;c6R>Cr zo(UqG48p1eF+Ik8$h`tgxSjhm*y5LSt)O=m!==~BTok7P?du3g+Our%ohxPs8hCgE zDyJ%N#JQ7A;A|M`o_b7lFM#{uth*uLh7g?)E!c+}@ElwT)a2t#JO!@a8gLp;f(y72 z7fB~5U1Mkh9H=1Co6yk;488=>3l-$!0Z+@ra6n-z_@gA3*g1dDA~^Nfgk5+p+^;PI z)h-)P!**=KlW+>?ox^w*X0dUPd4=$AzFa_KV z2c;LnbT}KPLI>cB#UaqEXFz73hAYHnAh9Np)eue&r>Z^y?e-p+_^#uYac#kE`~?lj zw}UH60f#2=`k;K+_PeQE(dq+^&i(ECV#9AgQQ)iYZWT927WVXXW*7`j9UV=rzI0DV zTfhMc4@r!CQX-`|SN(e_5Ia?>WTayfz6=E=axz%8GVJ2^a8Ycp?k$~3`=@hu@PBFs zze!IE1Fto%gr$gL>vZ>N&u9xYH>=-Q$1s$3iZ~VJXfAT;9nDTA^|3!lT z7hfbuE;NK(dtHD_I%IeO@OC~tEYJX4;?X{>Ez~@yaf7+q$n0ZER1c|q@Qg*X;(0~6 zd_cYsp0Q}9UZN_|4EnTK1ib)m5L=ld+MhJ-al9}k%HGBvYww! zHcq69?q46^DVJd&_$5;YUY-;)~Rk97{dCNcPu#o9=!3CDfx zKJ1TioPTG+Lcu!|V=1pv9TK&x-X3_#;+?TnihSZbh63`-Sg0vdz4J{{I*z(kv9hAO z&l-5hVq6rJDNiJ}t)wA}+JI#*Zh6QbV?K}gG(4WMZWJ_Oe0Az0^`^iB7WUE5gcRc% zC21H9i1FpmRgmqYp$UnyhJAhMfd?#>jREUo>c_Vhl09Rn4RX99B9ANxp>9`B_+s0J z`bmL@D?a!Er6|)6cgjj864wjja}z(pa}#5piu%kSo<<&gL40oFNAbCd`El=&<3GZ4 z6KG|p$sap`tT4gTAM1XC=O#4#v#dX6Jc%TWV ziTWCz#u&Z$n~mhsuieGI4%AF|ZWpZA=O#ZY&tteNl zRUA~NDm#^rs${AH)s3q6n0RI(I6ZyC724-@qjYt;jk+VO zkzK;>@nQFD9bb|KrS~N9tU{3;-Ns()B1C6ZCL48bC zlu!i&D-d-hbxz+5Xqf{W<0)u~2sr?dTWB&>EVQwhI!K-`p=J&)6tKKCz>6cL>@Vyl zm;&73+p5q~n!msl;A2iORg6Nl6{diNimw&53PyfEOaU#_X{w&mq6c6K5PiO>#9c*g zP>t~%IpB|(7nlp2)gVZw*XG33&kf84?^c5#nV*@_Kz^wPK}yLRvx=0~fFMoo+WviC zU0^PVss%yPZZWz^LoJAC#GSVs^T*Vj7g6yQo7)NibE0b7HTBg2*|*OE*;gL`5T#@q6`|r&>AU^mC8Xj5Q8uSh4f51+vNy=_X;hDjz3bQse{2z1(Iq}o zGo4zfO8v3(1Ak0m;Mp4I45~@VKYY!K`dI<#e|H9zqGVmiW|kBLq`%AvH5ecId!#=+ zKOp@ZjZ}kD|L}po_2r4L*jS0vL`|3D1I)){nF(kPnG$t7i8cexX>+%&BfHE%vwiQO zyGpVG&({=N0HcLIKHVRl8F;>Cg9T`IzWK{&a(yb$9N||b`eSDVp09DHQB70L*!!n| z%)ct?BY)*Mwn_J-Zj`n|^O7c6eHrsU!>d*+4=J-1cgTN`m(q7qr>N=ZTG^|z6)^C# zG!=faIPAh>Vwyu`BJIF|IaD$=u)Um`O%KH7P@mERPtK&WA+%EKr~(l>Qg*Wp=Wv^V z<1W!Xt_#t&YM$04s4r(eWNfN6@ak8Y;$DSX?xmlhW2xonJ!FRhLzEz~PIBN1Du1BM zPu)Nbr1wy7&;#kMVi-41tkGXb%||(6U9rHT_*(8H*tR)fiqg94wcltrY4bIkfxlNX z-!Xq-3RU+4f8V6|L9ta)BHsr5eG7Glx}PdXJK(POXyzr-AOLrB2mdwJA9I<=-7w%+ z!|-a~N1^pgMec@IiBkKC?(WD4=D06|II5OJG7rv1F2oQ5kS&VC>=Hg8}}eVe$JnaI2S zz=O{GOP(lc6}j9*N;{#3$-n$X>kn@cx!g?d=%f}XtuKFYci*Mr(gs9y0nM?^ca0&- zx`5_HMS3QQ?gpAuSB@V=c69^IDL?j(?vC@@b=U5{W0#qn)ZPUgj>G)!{~ZfU`<=%hG`@I0q=uk zqZ>L_<5_SQIq8OuO?ZOd(sxN)iKKG-U*F)50*GI1SI6^JW|N;Y&YftBXQ z^XrM~J!R*GD~IBsxT+{q1M7-nEhY)Cp{awXpP5OB7?)Bif>%?s$lhgO)URDFn(9lz zz^-kCU{4v?+y_e`?G7<&moiGJ8;PL|g74R%%z-0bcm!c z6fk65$(XuX?zrmamPW^{I1|Tngm7tbaU2J3j^ozh-=*K=2ZxwUZcg{*AOWj_;c2y@ z9xlL7a2ig+5%>~5hkfuVd4L^xG0$HD#PAWN)VLiBN&&1pfCf$ z=#dCAhanhgMKH{Sz$_!sCnFdz06{`5f~ZIYy~7X$_CVk-bw^0xEC*+Cg%jXC8{1QW zpE&plizmVRliVY@i^wPb5|Q(v?XkHy=c=0!tY|>6%!Od-6a-$C`b9+u?$1TAAPvFa z?FjBNB4`_o;MPP0w?rYB6N=y_mB<#2$T$+q!BO}GUWCWsL6{FSzzO3a6BLLBN{`U@ z>GPD)+iB!rTQAPuD_sSj=8IOkVX~>`~XT z1ZSn9dJr6sBYuX(ojeX0&vE0)Y;uT>p`Sn@yn@S(fZJpIyg&KJkN|IJz;CqH-@CxS zmevQbQ%6W631m}_k~>K^ZzjB2Sim`9B_>a9gBd3kwbi|u){QyQ%;<7c&GxbtUb&#W!X96h7)<_dd@%QjoD zG?}xFEMz26(#kWe<1DuFvBeV#vnxiYXO7CU8zx$dET(c()~K?KT(c=xUs0N+j4@Ow zqw|Xk?Ph~%T&8_&cAmX3yV#P;?j1>@l(BN@=$dk8i^E=Tw=~_{G&gI;sFsw5+S2l> zy3z?VtaIw^WAf(8P0sRcOHpH0W=3^WlZAaYk_--o*&Hl{*>DC9!CSBbcA>i1fWk5f z*z6**C=_;a@C*C^-=SLi3J$`46sKL-{tB>NmBi51e(tIsyI_A;d$Zd9EC=7fVfYX1 zft~O^YWd^v0=6#&_G}v|4W$P;dJvV}T{wRM425ZM6C`5$8#F76_jD)!vo!WwNbdaK z*?eRnL50`Ap<-@8>ZSo}SV;of{#WX9iHKIz)1A`)FF}aDN)lA1Byi0f*Gx9jBvikX zy}@x);PzqR?EUY^Koa@UJuypSO}g31)I=dtOEsx}hycz^xPdzcP_#tMmB(KeI6E+RNnI&uI?M!)c`O8(^mo zp?<$&u)6Ua+Ibh*LeuG6$lmdA`-G^DAATSc35jN7&L9W-sr)G>oW!=nlYH=b~LJgNgZ`(NdmdP5nHOKf&>q9umv7Q${qmr@JL#J z4U>`gCO^sp@%QstsAP^y<`#2bkOAaL`UKkJ*?3^r{Ho*F{XdaJ*7F>imN$PQa&){H zo)k)&>zZ8dn^nRpTbdi(m31x^AIO5w5o6y3F+4VuR9)%P(g!(l z-#L`5WS_vibB>I{WxD;B#F4>F_h0fENn$h3lW^8~o=Ab};6`_&)7>(J8P1atq`%rY zkm%G>ANH^Fq(2$J_M_;M6yJogKa){i5{RDMFPblR+$Vv(fCBjY`;POtX1|eX2Z0@L zrjZhS%b~XW7Hd8O_H7%D^#|Eu@kD(5X!domls!Hf`u1>e?yRolwR9 zM>v7~J_mMUEA0~i(8$3@@F+B5WhJ!9;Kz|t;?EI3x7p!#DK?AFsCQa)X1lpsXScbW zI+IDS80}VrSur_*%2XYI{fVE|Xmc5@cBjr}vsLNLCbwSa)Z5)Uv%#X+UB+sg(QTn1 z6Kw#&6%2Nl#q4w|I!kqxOJ`OLN`lU2s50v;E{on`v{&gZiWNYW5a1d)u92>Xv0jzV zJBJQrZ~aP)MD95ME0HL8k_NGzRWOK6ae~BRoY0#XSg{)tSPv)kWG*Kd@OYnbKq_0& zN&JY>*Lcwh8C~87-lbpANY0)6tDFvycMM5RB2 z-ByK7NV7jnbAb(o>J}H+S%?d3ce_C6mm-DNn26^VHs1xYY>*rJ6RWzHIYee}yCII) z*fBRuy0R(t9dkk3R5TC%1bE(=P zuxGVRXV*KNZkxGUsdAdx{#QXsbh&L+cC*c-a~iES9ky6?4zt^(PVBC7n5(P?n~51W zLu#TGr*c=D%{o^#PG`2NJuOa@IkUszc32df!{ub&&5+U0d@O< zI!BekX;P|PR*S*P&Tobx{fth%-sY^*8B912u5Ca9ZC0JhWvwzA4T|1r)3e+yke+D8 ztE9MGE}c#ZVr%rD%S#@T&&8Bmh^md)0+GKO-t*$Dw!N?|V zh2+F)mxA}o>B2i^R&-`P-Z_WUY1LWm2A9L`HW*z7g{|8PirHqdSzHe7)+EXdCO0>!6Mhg-vaMc=pP4XeJgPo?nl+mHoRORxN#&=i_?7i>jJY zeb7shg&pXpeS(j>_hCEy8!1@~?8V=pC>Azx@G`#VPQ$mb8zT>!)Ta@u>I)c$_zbpS z&qcsC1@R@Z@JyHf$D8=|)oj98#U^+Ld*0V|vHumzkBfy@E|GA>QQn0UzYA~TfUVfw z=kW4ui_DMe;>aY9o5YBDF6z6_H`=mL^CbIUUQJpq3zqR;mi=40`e3qMyGH z&+A=y9bUz#%QLVN)*^ZHfUO+KSI5FK4pyOtI*AdQx1b%C;qG^%82pSJ{Q%Zs&)LA1 z74Z#%+H)C}7h<`R53J5pa&pIIRAicRD{K|^vWdCYam5wsCX2n$WXiXWDVi`kzbL~z zVSLuOaf)@KF)yQ_tU@WZ=h!j~>DhUsa{P>XgIT9H>GVpRK^dy(hw4o!20I!?6ICUb zz`r@|FrS0cOne&n|E~-B#Ig{BOWaGuLLS3I7(r`?v)r}(0FujQtmV`3`UnotlRP$M z4IjxauH_9Vp89osGRaq~(#c4+at&`|X={0r_1-P??&w^@2hac?!55J0$f47~F0}Ch z0$f#IsFoLTun61l2Du5ngHnIupER3Os_aS?syUt0ZbV&bL4D;k+Z5EgMuWxVve}F# z6Wh9yPw!gf7nD4l)k`i@D-L-`E0N^xlj9kqDjU$(d$38yd3H0TlFOuC8 zx_PQdHx3=A0?(vK7XKUHo-xN!H><9x=6a%H)SFFagL8IWgG;CSqrT(C09z=nqX(cK zMF=%eNhaI4flp=|+Ic?~x?2eD=-j{uQIfT^otN3leL^fNZ0D2wvZe5x!Ng-ycNoeR zwewMIz3a=2UyhyON_G+?Y`n;1(WmrdU%C}e_f z@y+`LBN@#)_X$=q(N`6?U+B)p?iU83ApdQ@5Jf7yk9H61Xx}d+P!!~M9^(hFj?aY% z=C_6)*hT394h|rt7&JOiBqa7w1G(z);cuNL)SZy9Hc%DwZmRBk<+4Tj3Khp*R50(M z=2P|F!@yo?7V@IH!FCR--$IRiMOUAM0~i;054)}Iau45Z6NZJTM<;R^G_`Y2lD=dy zIZg4c8ka;H$FN}sgh4FgAQJnx1A>keu=NLoWHObVevA)iCl3fRDP%KN^Fv6bTGf}` zvzqUwo<$-lV!^BVeq@@j>qnmp{ZPc8S;Z$2hp$S1Q0N~~B87*Cle-O?RjO-r)VMX^ z>}WbDEG4)N)I#b8{*6ZD1RB3X7!vsuRpSoWj@x*o%P4GJC}d&ZH~-WsVBaUYw)~FF z55vAMtDdiqb6b&j$1vRTIT|b9+j?2oyqnvFoTzT}ehx$EufyXQHmQaj7zqCKIDL<9 zp!egvjgFwfZc76eBm>@;U?$RUu$%Dq%@Eg2m$yJXnC> z0X3iS4>b{T-v|Wvs7Zyp)f~v}s1dmN$p~)ikKk|d2%nC*@vm1gLB1K4H zJwomg^iDM7`Ovs!z~31MS=o@G;$!XYr#}Jsp3@3#_z5A=zQ6RlI(u>rSom5A6 ztO<@Ibi!CRZ?6zbs@NlY1uJ!pW1)NTnz`A~y~0RZF`m7=M~EZU?6W_Ai3+P?6jow0UqK&kMI_M-?N{i@d znn6>kl^W<^46?=2KD0OOP9;jn1#*_0BFD&A?H4D7G*1WnLJN6l67P?d4wz> z3(39Y4st7I2WBEC-J}u&N8?BVM&Q#iw5AXl<0bvPsOYpOW5#HpP42|BOu ziNcc@JzWk<(LKKp?u6UWQE!0;R14Ff0#iqYkPD+=I9Nar$&iSG9s#|e8;HQspXnL; zJv~alqzC98`Z0Z%zCpLpm*~Gxk*uYw=)?4%^no^d59XWxMqB9&I-R=cR9a5QV!9@q zrqQ9)Oj9rkl|Z9t80|>|C{Heu|B@fbN%Ac@j4_^F~|QbvkN9?8Vej}1c*LonVGhjM`Vw6M77j=1jP*R-!c!E_PF zJ&q1|Ekq%~cTyX!w2F+!i)1HcB`YqJa(V3XFzY$Shy9bKjrL&+*+Umat@U2k^G6@% zS$fWg%wq#j`LI?t|Ad$uh?C6r#V3LF+##e*=%L<%0ptY^pBdkxo_-evaU;6QUbq8h zK@Bpy0Mbz05>e1Z6tUyaru=;OOof3RF=&0auf|0!65DFB9<#|3Cv+Ta!p#y8^L z9GKlIg(4UY8ITHAFu-6)f>`K-3%pK!@r~#r@vAPw{Z9@9aJNt=$~fiz=?#vXPa7t) zxjTh{q?xVRDP)pK?C4HZ6-~Z(OTs6D991EOM@Eu6Mrw9p|dBj=3VFk)TwSy1nbx(=)l95vtys4;+gKN z#35Go**JCp#aHfGwBZNQez&6s^ER^eHoQoWENe|gx)au$oCXE0|ySel}3`;+%C?rq(qA8 zFfJEAfoPG7=U@`uM0=AN+`Cw~`qwe`ZQ&4vsiG)h#vMuJX%#s^#uDJxA)_Dkt{Yx1 z4ol8oP#`kL2cpqyO%0a@SMUDR6DF|dJ`hdbdr~WT>y#fuyeCp;NlAx}p7X@Ol9`gp zyKF=aKlm3ayfEV8Ja6@| z6?~5`vfuPd!WE=IP#hA-JjGoX8U?pkN@Q(Wri)$cO?c9x9g{gY(D8`Pj*VWSzcT}7PT2@!$ zoE%hQp1qL#M%Li&oSYe#j_ulAgR+8;$-QZVx4UIIAH4CEh2D*pa$fkff1B6en$L&q zoAao*+`5F1kMI7O_q25uKceaNcfEDC6@AU`41Y-7oydi$3;Y}N_S3n|Sl~J$whPBE zt-gSRd*~@ty_u*ffm^_-v69@b)z2Ab@quOETfEK26@2PGzv14VCM09_e_!=JYbxja zE%-+8b~ESmy^bf?ypzmJy7$hiyR9|Mk1OR&@s-}7p*nA}G9n^4{e9+RG=@0J=u^1U za_$*zr7>NMF5f6VNmj7qihV%S1On&bU>sdTFg%N>UgMvs>E6@R|{6Ng-qv+#|J@bLMM6{h-xS%z``(3J$ z&HGSH|y=}PPfS}VSL=@_oPS==qy#pOE&S~1wW!ER(j-xU)@ zKL4|ff@H7XP$OIVt{BdTjd}f_?74T5-mu9ZKh!$V+lei=y(gwgy*s}M7XKn>*0MS%{v_Bk1w0 zL(k`V^nkXaCuBr_=u`BJj*%bnD-($ZqZLfTJck`WQn8VNw1AdTCvBp0>D_cOU5$G7 zHPozM(H}7K*az*+7?_SWsU653B+n zMi~Mj(ubL3-dC&F%kD1BDA#mhX1T5lv&i*bm`!fZ2cfv2P44Ym*(Mw8a>!M9_*Ho1Rk(Z=Ztel`+9WvEsFy=o_fz<;GRYBa=l6mx z)G9{>!+^^jtg_K2N3(_B3!xAWCiSfl!pHW-#0p~Jm?$3cJ&snN6DHCaw}ql}cG1Ns>~MlV25)a+KsMt8Oxpqde?@FHs=gL6@gWooEO@zzB6JCi4RoXjU;JKgb5NRZQWNhrv7*v#B}AlR_Fl ziuXur#V|j!S=Omv9B8FBESVKKMosEiFWk2nJ6-BoBDiv9=$aZZCtVCR~%0o5!E*BM> zJdAyGyz6-IZ5P6)s>g#^I7TsVJ1)d@8NC~sbnrwQy*(`^w(>1hQiIc)mR1Qtordj@uSRE1cI8S^^8l%<|@l{62SZ*a(hzFh8 zWuUhFM|50L);F@);EA!bAhTp#`np&v*%Ho=_C(v*vsl4azb@8@LyqMPtFW>n1*seP zhL|MwQ+_ZPSiC*VMz;J7+>dy=EX@<9c&D08?7KJ6O$a{u1}lD3oF#=6r~T-OF?pL6 z6TA4PSQ9;HS?o5IY>`7RTP0f{E^ZqZ)L+Nmz2cF-sGoIVVqph9OnP0iq8KJt?euw) z%`M_aA{$b=*if@eoN_UfDdp0HtFhrz@;x8s3YU)FK0L%rv*!{1*M(;^Zk zTRyyeS~+w9IgVuh{`&S|WzO`@_=KY|Vtm zbd%CBAX~TF6O)A|)Wja@Ce=t`r*FO26P>AOz7V3QX(JqvS$JN zt-F-Zo9`T7!wP#yOA-?L~MFwsc;q=@FOnbo|6ppF27L*i3(E2>X(k5`@@~SiyMh&J7ok14-4jHc!|%?atjV zNR@p5)vJGDoq{w=>Ycg-y{HoH(mg8T)`#yo`GhCBSW{3VC8@e!VtGx=Uvc``;fvy7 zW;-Khvn{`gqgaFL55?t5j1}cbxvV2sTEwIrDZR^)MOS$;G9!>oy~b5O-AfvKi5EB0 zL`$0R!ufB#=L;4@q~&~A4pz!3i1r^$b8%c5Z39a)kdcr1Z7j6;7^Y3*! z@M3GZcHss9UIXJ}Uq*OhCTMDntpcewA~pR;s_%Hw-!7nx93%h&h@f{M5&fYt}Ki=1puN?1$W$2(>hhtRj-be(&8yy zA*iPr$Q|YW^a1x?$33H^X2P~+Y;u-1%Bq)hv{8sy4RWqF%7X&?h}g_> zsn*Ab+Z4G>TmEvboa#1?2&*-i3PAEL<9H1h0Yrl2$rDzO ziq^=I?b0+}fAW(t>;XHfnIVU`#h#ct)UZZ2Y^YQ#`H7ZbPjoG6SR*?yRH_lf;_myT zq6Rgrfz2HzC5d$D-gO1lXvGX{&oG>PP@{Q?C(P|F$M@fW;nK8-P}#)AzBHkr z1y!wq9Z-s zpTN4AQRPO*ocSBuXqJ{ogh&}r{uy~kEARwD)f15L)4MCNBlmBOPEt25Cit<=jXuwi3wMILzVAg+#g;GY1mcL>$~1?g7q* zVmfF%=1mEHtH-5tHU55GiKVN*d$^RfVvfl|cCSC?@z}vB!a$~C`7s=5xZc2d3F?QV2}9!&THFqoyXB^YTh${b5W_(Xo=XTMdn&sH ga_YC2$nD#24ZY4+CYQNAf0D4c|6lq1BH`Bm1Dh{Dk^lez