From 658ac89af0b897befb7f91d85ef5219f48178506 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Sat, 28 Aug 2021 09:42:28 -0700 Subject: [PATCH] Maintain current `query_params` API for form fields, transform data structure in widget --- netbox/project-static/dist/netbox.js | Bin 318496 -> 318904 bytes netbox/project-static/dist/netbox.js.map | Bin 1148931 -> 1151953 bytes .../src/select/api/apiSelect.ts | 124 ++++++++----- .../api/{filterFields.ts => dynamicParams.ts} | 39 ++-- netbox/project-static/src/select/api/types.ts | 78 ++++++++ netbox/utilities/forms/widgets.py | 173 +++++++++++------- 6 files changed, 279 insertions(+), 135 deletions(-) rename netbox/project-static/src/select/api/{filterFields.ts => dynamicParams.ts} (51%) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 091be6775bef2bc3c25364f60b3b568d2544f3e3..e1a6dcbe59e6ff3f10cfcb04fddab073867a0cd1 100644 GIT binary patch delta 33889 zcmaI933waTxj+1zGuabD67N}UMRBBftjJkG%Gj}G%e!S;@)A2nku@Vrwzg=oYFwE0C93<*Z>*>Nv*&BdgpiUvpWaKw>GPSrq~rX%CE@I> zq;5T4cK%f(F{)+c2l)Lhv0T+kD5|7QZyY7H}%b$&Z>6{Y0|B3Sv7V}#{J8r72{REwjFGS~1)D6R}7r zsaO(9G8UdyiZ3jwp!&;I2T9MRs@2=chSnL;o#u0Kt6$nwuDaJ)H}}qn8Lf!J%&4AU zQvz6=xa@%XqNHYyC|5^o-bB zY~++znhW9MA@!;C`zlX)O+)-k0LyIWW1Uwdhso5TUbaCdXD=l-NMy5dM(ito-mN~o zQ9RK7hHpl+Z(3%={$jOuuN3A(992s;Io6MQEq-4Z*i^ln%4(c5;&id%fLDs}ISrP4O${XT*+Ttue3EpTb+ktwSK?mfT5`+1`=1DA{-{kxEK6W8T`@8pW+j zmzhaQox03!Gp6%I8+GfShuqQ}J`)peDU-l8=9bzxERwkLa-Ulo;e!aPKf3I|8e`fL zkfQ3Q&6V3?ZV;~+m6C2#RIIV`8pk#pN_{96qh@nLNu?9f>V(@=6H>#QYs!ab#FpaV z{cde)_7+YZt3v@(jbD9a^I%mvKPnlc;k`T4(lGBmpqek=N7Ab6^2$nQTJJbe=oked zvnZ?QF2C#W(2O`->?N3QE$)^EIjAD~9PPu)APTyh)U#VEHpJbQ0AGG$L=A2^w!3>q z94a<*Dy`#V$}NS^jC}}$E>)k}vW`rt&u?icZ=Vs{iJWJCq|1eJZ{M|VKuydgfpsQT7YgEQ18`itK?+)^_ieNTQitvYFly*ji-M^Y!io;)}@ z-BK&>XGZ-^>Hexd^je&=*qadDA+a+d_Pf=W*O#g%%gX7XTRp#X_hn{{RY&cd)>_rK zluZza`f{0x^r;)lpSi5lZ89WNzLa9%4gadVpY*Es3iVvSTbxo-DJ9{IC2WK40!NUe zd@_kXF;JYwbbk10StAm$BfoCWx}_{1t}%}!S(ehA3$(Z;S+<~Y(W%`j(LgNWSHyWN zV~t;#a8Y|JFW5Rbog1RQ2nJVMo0f}D5Ei0ea^f|vmSilA?-v(Mg^OT>QqEFIY0?$uTbT3C8-wU~z&Y`ULGkxyWBe3e-E3 z;MJ3A*Ysd zl{2AkmF=is)$KM7so&gDYw-7pbHzIC`8uQdE+W3HIG}FXdBD}q3D2HqK+zM{rcr>X zP7$qU+W?29QtUdTO*7pmx{J+r<@w*3TMBSIDH-*qowluGZd1$>R-#j>Ab6sL((mp3 zyhZL4J;g6)@`Reo=o~bfkutK$YzakWCF=@^A@z&9YHLy%3{DJTQN^V32{CC0+D*yo zP_#N~u9-=iLMq*Tf<)D(-DVO{L%Z)PO=P4#zCdEQa!N{Oq?o#Gk9$ob16G?@Bt^XY@4~$Vjcc#b8GA@D}50y!ybF0RIvn#?cIB6;wY~y_d-9cdL!%vwh-B zv6VbcZHj=K92b4G8_}1105Z9RM8Y>`D70_XmXf@vOes6uU>Gt(!I6l=9+ zbW|9Vl92#c27u7|clBd@k9L)R!?CGBZiY6<0`_ z$ynTTk;zzCoX$vtnI>1r<_gu;x4yQQ*KSzEH3tE>;u|J)CWr{)R#;>WLm8^ z?=?5~iDSjlbYyh>uuGOYcyDJxvRqj*W~8&~ocS>6QtvlkxxWh%gTpfw<78zvH$|n{ zSV*q6+Bz~29e`g{ijEuA%MNr|91#71f=*euRTBNJ$L}w7)+uD8Q$uGDbdG>}?agK6l=`H29KUPpTUNLBi7~Bo=TJtP28Jyd zMVrqb6uUE$lMlWfs8G+#uNd0_$Bk8>K*ZMMA($^=3i?dC6q?#HN`@rwX zPW6PA!qX@nZCG5aiK;)d z9wITdqM@aH8u~)9iL^)O>arSZIOIxuz{+J$Q82}|K06?Piys6$y22hrUtF1udvw(} z;*o|p`JQF*eW-ANs#2&Q>8YDJIw;_=M(8IO%m zp)k~hkJIlkBr#tp9haS7~ z{2Cg-0D)P%QEff(;@VMqkUp~Kppsfxq7v+up$>YiykYXHX zN>-oHd_c!tDyu6kQT0LTIKcHgX%AG$&5f0Cne1;ICS&T=jYm*^sc}efCT!|KUzNJp zv_Z8ro!%--N}Zv)07QeBR^Q#k+jyqwD1O(UJhrtZV45}8NRrP7S>X&cIo1A?2Tv$j z)2t{1KpKpa{M|@kTn3;5X6TVINpdD?Ya{q);kFCD=g+1z7Lll)Y%EouJlTQaRW=_i zZAmOHrok*tL9Pdf&7hllVT;PJ!))fFr zbCu2(QqQ!QNb=H6Er*ENm(|yk?9=$L7|!b2wm+*oR?-*`CpuaEnWLHbF4eXkCh%m8 zwe3C^&VuUwk{=g)tTAaf7euHm$ypQnN=KAvDmk94tDAuOF8P{dA0(>gKO6R4j7=cg?*u#wxv?ryrAN0fds z87v6?U{)7v@vIcgnhdF!ZjpL?Q&|16Ae3x-EI1xlUCt})seaK{{4kQ0=J`S!N$B3V zseD7TfYHA-G^u{sc?g{RPtM~cqSm($*34y1isYJ{R{SZ>&1SP^u4g9m?z^aMQoX0$ zvntvTSJV07@||t@-n>~U0F2wi!J-6gQx9|;*x}XjXRb3$6!p}g#tjo{tmEBnv3^l5 zjy;=|d>l_MOq$cra6&SS8|qwH{gsIWn#IV? zMVm8K+n_$uSy7UzJp?dAyMnhg9`^YalSBP(XIpo&UyK*~?aAu;Z!2aI1UtiQBVsV9 z58_lp>d9K-zC=>#jHblitaR|I@w)nnChPcUcH*Gesb1C9Kswbsy2?qbdZDWZy6v~R z4CUc|akAJ$UskuK`&H6?#fE-us92wUs-yewo74TGw^-At_VhG>(O%tS+$;Bs?&6CU zoyhI(Wf0f2WZ_rbuKuLw%DzCq=qc89W_5*N9_Zof=&Vl{ZzBL91Pw@jIIFA6r!X)~ zS&T=eVUR~eN%;WLAvMr@l8mSqdiR)zvSJ4QtX5yjXItR5%#zp*I2E)A?k`{99-Vph ze|m2R$353KK}WOP_p|y`wzhUa{Y&3|GNA5p8LGRp+?B(`v9^DzgUr#cXjRU3uyq%jz6PaYs%@&eixGgTGT;qxh+J&_2 z;cx`G1djKM&BeB(E{$fzkV|hn;nJ5}%q1n%!~IveB0jZa@QR(Xl(H+~q{88!mSjFEF>kk~ z)fe~fQa?YqYg^cbpo4B2gu#{~a4!7!plcs|U^*)Tj5yDWOR^TQu-BMorFfo8si)ob zFo|z=4@1#=*?mLRWWP9FoJ=4Ou)d%axO9zYi0o_8Krz=Z8jF>JE}aC=1~s{9HwM*5 zZ?90RN7ky9!#3!`Zv3{XZy&CjjJu=^SA!xheK|UV(jd1Vrd+ycopnh=oV885q&ChT z=3J7OUo$?v3y)7118pv;g9F#=(nYk}CAHw$K40j%(-v_-mjvI3$n-56W^fJoz~kY- z2rm=(q^>8sCI9&3gnjDLhenzSgyzpj$GNlM)DCghWf^Q680s8sYlUFd5K;|{V|){Vmd?4o|Qq*}UL!nfzjm`(s`FPS+Q(iey`>tFBHABx3VQ)TUeA zP%i-wuAtY}W&saXysAcQ#-ihX#Uf7peC6rb3rk58JVz0!J7LQ359Y4gy_vtYMaILH z5Hzl|dhb<-fQRR#eULe3qHpy8zzWO!r&n9^X|v+ zp1!K&a8^@VG++GxUkgB!)WwQt9yd_^6qs>Q+)Y$=9l%1OB&(Jb}T3j@at$oU)BQ&jgBk@_k5pAk5=@% zq?#`Er89e1bp^#Pb*(SHf6yhR;zbQ)#$__(Loo(XiKAh&z0U=MNPVC0KuPBtmaW(y zl?K%pe8(ZZOD2t*yAoigI`S2S{@Nz5<3&&~^cV(;o}E1R??pECJ^ta9#UW|nPWgSr zqrP7jX@^Ta8!p*;)&&*fbh2&{{~HfZA^7~7Tw!hDrPj%DYr_OeL!wiAj*VOEkLv~V zE~%)6jJPEAsnvVbzlUq+uuI(?xpMs}fX@Xtj!Qig*=ZeufYG$L72K^GfOf%gtE-Y? zyGsuYaIgKSx-L*nPr20ZM#>?qn*$XBaI3x^X;l9saOJ9Ymnp2yM<<~)zZf;+cWvxQ zr7X5ep8B-dCn@zk1rKthX;i&9RkJ1kvQR9mFHJqVHjOnMg0|ZgKd3$%w1XbEhIZli zV90>qKgS2v4~O1~?(NeVI_6UEI9o@RX7%OiJ*8t@w$;{ly9zQ(k~-AVnZx{j=gh5+ zvo6tK(D`OmYQZFxB-dXH-NT#ZEEYD+qcKY|9D*`<2;tMFL$)i-Hltb@KCn64F9zcU z9XzG>hq)0K50{RHn%{`C1b-f{ubZ%DfuSpX^#L78FaK`wUAkEcH^WL$y^&p3x%uC57-*K{=4BiPo0oAIS3eP{25z2+RP2Z}YmwBbPOc#^ z#-s@6x28(~sfa{3Mz4VI%|w-zIIa4B;56Q>erl@v|3Yem`q|jqS0Hx!-;nw)^^0dW zsdt~Pp|d(Jr!Y3)GSIw?%c%P5*~3sVjEURMg_>8OGKRVS8!D59 zTU}}?xXcqG&3Tt%Ce9T?7c}f5z9*X{L!0Uem#VGFJ(Y8K8jopfsM?v=c439IsSC+H z+r2>R`03ME87&9unoQ>L(-R9zixXJOZuK+CY9X@%u^tVvQ*kK+#Cm|(og3{1#$KIO zuT9kvmwJEds?z3WczfW_U$J5x>YnsPL`k1aS8wTQmS*G2m~T~IPuCEyS~GiUb1%MZ zwj|@6q+yTTID0!1I4Uy-;1zOYZ0avF+f|ksMB2sGnJTq6yH?$h-4Abe&-`|^Cwrwh z(`=tw;hxURD5>vF#SjP*ViwtwvcWgKff^xAtIua=$~v0iR^)+Ai`UD8L0&H+Z0Zwp z7WHj&N5NVjp4-lCn@pwp>YN3>jXm=`rKD&6jHjd7KKOqO;_RD>`5=sMDub+(THaK~ zhvd9-VFr09;f4L>oq+uyVzjy;YvXFwRCG!G-hx4We4#{LvskJAd0`7Y+CN#SP6(aot|GnBk>+$}DS@`5)C zbVVBDg^PcL+qKGUMbetlMwKuEL z=_Jc)kMz1!2!JfbRDxl%jt9#(NmCoI_)#$T0;IT*OdKN&6E*aIooI80)8;mCJ z6iFL&Q%`Zt+6;z5zY1c7052|WYUmobWc1~i*u1vHrUyl5afyYkQU_P*{Z`#0leX${ z4cV&OK`m;HZ}&N5ufY6?nHR!VOv%o*;iTlZ>iSvQYD%I3NyfDp#egN$wE()>Agy9RlgyS9AD!a~STo)G0aDQ%xjz#3hGy3U`(RFizdYFp($0U`C; z+e_6?UVHeQ+$dRX{zlo(Ynb&m;8KW5QBzFR8xoVtj)&M14B2IVvk+DSDO*e2RMUV2 zpU_lLf3OzA48t0bItIm&B9H=B-E|YRLQ&cV!)0W-y@1seR1?=3R<{g_-El1#<54eO zw-*}5GuN?-F5X&~FP~sE8rKb?0rlFq7!iv42!03D$KP@s_rhEE;(q^ITPx-V#j#@F zS*!MmeJrkSy?&p1;jQkPkwGyPDab@u{(UM^RI^;_t#93-9=krgdU#NbMfjMGHe03k zIPe^bNL~EuuU65FU0=Ow1~t{P8(!JfhvG2oZ~UJ? zc&gV5*S6K(X9euXeC(Q&LdA9{6Rn&=;7JKt628m|A=mY`1mdR;zwIDtlO=yl7d7!F zpDij*!rRd#Hz92<6pg1-uzR&@UW^1`8;o0FN8x8hk=k%mb+uNO&p@z6En#0WrCBQ= z5CxjHte(B8@*Hmt7EIlW$~M>zDPDmGjP>su8)|E3ta&pVT|qh*7A!a@H^|dcgFSts z=z+|CSAWQ!uB$T_i^tR4&!L+&fQ&qT8U^|z_oAc6fVKpX=aPt}<4Gwj^#Ei%yn>}- zMGt{|%UZh;DS#Hm@OT7c;y{RuH%t`M17uac`OwyWt8S(zBT_mB>g-o9+}w%u$JcH~ z*a3J&nHY}fl;@A=K9y-}J|#!J;+DxR+{7+$kMwwK0-=Bs_2FCALlybPEqn0$$}J9| z!>X5`y!A-A=CRfSDi~U9!USDUtVydk-@3b;o6#|74BVbCLNKj9a;pLI?&Vus(3BxM9M)6soPAR>QYduWla{WsPphS@ptgNAdgOZM(}y2gN{!RH9|`mMKDW4YaX`;Z{R zcKGM*m4+cJ4>`!vD8d`c#VA6>*28MEx;Y85-<##3FH|lER4@QC~QJf=)Tq z*RC#AFV3uEcRwL)P@C@T+8S{{NMbd~3jb(vF{s{sXANIMI#s&@CW8b;8)K?5fmi13 zp)}qn)02>{CM$dungf+n!E$k3oqVU-P|vAVx4TVIPP&s07^mQ#OG|}Zp{2t7t7p}# z)EW}Mbd6diK&!m_ogMi7uXnyeCNDkCyuwCVLgGOZ$s2ZAJ^Jq3R`pMdlj_NPn=h?> z&)+G0V+b9kmo=?0mK7g0Tm|x_-S6MX0{{o^v5@qoQ}@);nzU#HA;pmdAS1oVZbYo4 z220d5tCt|T;hqmZw!s+TO5@-(w7`@1mR`F0L!TpSTePxiRl4^P(8WLQ&0Ma@2)~)j z1~`r6OV@p56{&`6sR%;&2H=0FNBh3K)`md)M=l&CnBb2-x_d3W4j5`$CHGZZWJ^4q z-A{NnI5n7cOha8hd9UMA z?EYWysSjT~hxgyVIE>%TKdVxmpV|TROyZZwm#%)`+LEp5Vs_lT*238jZTT8m)54{P zQ!W4c?OWt!_l+5Dy!55deti`%ed!C|$Zdp08&J1CQ+Da5XPVcb#mAo?-?+*+Eqb&u zM1KAR#Tb0Ama4A*zN`c8)PsIb!1eMzKO8^c-r*AsO$IR0sBEwqv{i1fr?oW?Km$Rd zruX_|;bhS!Go>$m_`fgXbNlo!>ucnEc__-=ZCpv_m6!7a(s%x9cvX4=?xnF`wcwlg z{_5{=(^~EF2|tt%Fnvx?x^fM<>4=5fIZHo<`Vas52L>z$E>!$=$;M*acBn~B&|dNg zfABX>fcBrixekqA^V>a#Fv%uA*grHBzeQ6{FynNc&w?}bFh}UpH5a6UpujDOhaV% zwAiJ!XhBao6q&CpeX)!j%)ulH~C8qBA z^DDc0O6k&fCREv+U0O9ohG9l2%gkOuq~wthX?OkC zs2c8a?rUO)E65=FxuJr5Ku_G{^&swx6@B@Nj!JTv$hk}<`5A;gYiNHzOv1KBMYB_sQJ0|>>) zHXkCllF{6|50T>t-mqs5llse?5Td5+xN|~Ej5~9e9U(7lz^h+2lF=$BI8+w)8WL$v z)z*6RoNH8cvau%8gz??o1j0+_e$hm>LcFlQo+LXDr4g6p2898c;^{~yzpXaZd9ipU zyh_G>QM~abu-RuQ4({ZX(#+XRUM9|*sfB!i_x@ddWV}xV4v*mvq%POn|Bk zr%xN2+~Xa}gvrTH^^(PNPBEs4DMfVU!>=YMs=+v_aprZh4#e@Hz>SJs;*26Wxyt5* z0gM#;sGn~|KnPYbcWI21-7x};IX)Fq}MD3qxND%s)b(b-0nVXhpRdo<#dZyl ziLx#p-|9g6e-a9?_8)k2?)Cw)itcnuS^MY-XMux^LXt0VmqrX&VHe$`6$AgHo4f_H z$$H4BJu)jMEdFpTsYp}&*6W9lBBiv26`qO$FGT{{IT%4~PMYQKL&>(NZ!)YP@s({H zB2@?5cxghBVG&C+Vcc^sl$#gghC@=y@P|}vYAUQq&CESSe!j&!3vtp3Em_R+J5R1- znDmm0F_R${9aMZW(6qd2ow@r*$gPyLGTRv04q6==BVQn0xh>=5PirxcHGa%$fOY$c z6PG*vFpob}4&qbT~bOlrzp=%}F8a)3-pKGPf%6{4WV9aE%&-KP*6?ti8L zyOV5nfFP5A)dxreF5UovD~nwhz+xF-9}SQ;Tz(h8a0b}=Dbi>dFeNMzByR`%e6yk< z5`$(T$1+%ivSR9)MSj!(3r&I3Ykj>BmxDn9p9pgYi5ZurAgNw!go{obFtH0CBfHs` zgIdRb43Z!j$W4UER|%$NeF6iu%@7gy!5Oq?%-udij&Zi|^)Qhu1DGXP36%dR(&aH8 zUkEo?{4Eiz_BgvPLe8S@A0y-pE<;iB%-W##iOfC_1H{^51YutN{+bv7A?Hy3JKp{S z=SfNKLuZMHY-&lF+O#aNezaV32g=KnXb>M}Qox1G9!il*#KGce@*D(b*DP@p(!$=C zL4(fRqZzEC(st>r#UBiX<%AL)ADZCHw4FVjA*Jl?8FKk5bkWWZ&Vf(2=lpZ{ew#C7 z>J-~n&_z1;&3T|>b33%wp%YT4eF&k~cJ}uLa-h5&Y=v7YL-1?h-C+KmeG$-ZXUZZu zwz+-Z@fyjB<*R6)-M2_AoA_s(I0B&j+<&1KCZ5}VH5sP3p1p=pvPlNGv=udlxu3h1 z+yKz5xsF(IIer}xDe%{HEvaHxUq?=3?mxPYJj5IMkGB#;@@4kI_2eE6OAC34xlA^C zdll?sH;^mP8iyRn_t2=Xs?ks1=oRlSo;^H+Qck}38cM(#PNt!xKpGb>m2 zEQcl7Pb?Y;O%?fjZS7bt@-Fh1^*X(mv&9dRjkG1je)tZuC->10l2Zbi%+Ze!+iKua z`ZyLtdv5yUTsiAv4}OBo=h#JZg5vRe4*+d* zZ1tzfCrKOo%%{nN=rMeWd<0*XeulJC=w+uKr_mvXr^Utb39}{Qi<_Le+a4i{T;JI67;#`CuE$6x`uvZ_$hEla z_!5Z3$g*D|KgMO`%j9#Yvh^!uep~0PI5S)9ymPj0hQ0SIJP8iaK)v_mZUl1Z+i*4DPOFWpb7*Kzp`X{bG!R~7Eca;|Ta z4qD$XdZC}>q1e8xVbpt-E~apOQ}z;>i8vzsQ5D zcWKp!`R44QxD5HaazXXvF8vatAg#HVe+AJ89NzpJawRTb{SD~?axVWZc*NyS=%Aw$ zcAjkyNI2O*mX@-}@5uHlr^Oox`%-ZzyNMO+5q0kQ9VtJ$@^wKO6W>tSXL5?!#YOCT z6;J7gi8J@>-;o}|m1S=mDPw)Fl0c~uN=3kz4#Tg&mp)H|GO};JN{r1$OE`vo(%iLJ zP}@yj^8$(+tkGoQGbv@_pS-b9^g@v~vijFZmFvwNf61btBvLCsKvV$Nxn3Z5;@SvmqtJcW`TOyIAB;WXwIFa4p!EfQCO1 zl?)0TLE_n%WblVW@i?}(_xsRbDrpeA@V3x^L3E}hLp&CbD+z;WMD=3NXohY7Gilf` zpomBShwp5F4gHz)8ke7=@ZgEyCt^!jGE6D}gTj?DG|ImIXA)W0f*p)4VbPhh|An*F zF826e$vGDI8|44Q|C05Dj?BK(KudFreHucEQN*ua(2U5Mj8 zY^GEuR`^K~d*d!CwzQho)}5Ng2GBTijR0$G5)iS=DaB)fNL}Q{#GIiis@TxPtR%DV zuBOKv@CfSp@tORtXrCFl3Dv28U$EAwT3p+ zZVt?9R$-6rraQ{6UPmLKr{AojXB>!5T70rEo(jz> zJV0sDyojzq0Qz{6&kw^#hS=yVy0sM&I5DHbax*lg+3o8AF_~@HK>xN1QJwbOx{b7y z&;fl&jhm=v7rgXnNDuz-O`LwtRlzd3`jloDH_=Bipx9+Jj&@$UjJowF3!B_b53iBI zh0SL6*k&-Ei<{}KczNP-dV3Wdy^2Y(AZ<-cgwMke4qx`O%PH4d#4Xf;25Xjt9cgg6%YoKzaj7avp+B&f?qJ_D((=^^Q-DGLkCE;vh4CLX!6l=q@UZy#6Ep0? zSe?wZj~*#CB8v|BoO&KY54X_a(_;H*%hmxMG_oS~a9znXz<#xlj#YKRtz++MwA#Ds z>-8FN?{vV6!6s_y9VC(aT`jF5B*tE^p%rY8iQbHT!DC_UeFost{Ep|zg1?0&#g7b6 z029|Vd%{EyRv>#=lT>X&SdH7qI8OC-$p*HasN_Yts; z4F>Sho9b!fW{#{$9&7O>=Frx&^>h*GXNg!W)sip8sC*WB5U4+Iq0a&S(+6qSmBtCU z+dSj0iTdEUcLGkfcvF4rIQD5ALT(7)@8%d&ntD)V1FJ<2WcDalGvz~?qwrZ zdUSo)I9h8{q%L-sl^PAs`m|C%qty4=VoKpw9FUru>UnDi57jx@3s(A-^~RJo00{F7 z4RqfcEc#LCK>yi5M}hOYLv-Ip6gfj8d<$3?-JJlH_muG~w9=oh&?_?{#5YkX6f zhISq-tgVfiWR^Hed)bx8=*G*X^dz{LJ+D`{u;-7`GjPAyj?tH?94WQ96dqDpm}HSI`rl+WH{4Bk5!>7(68^5TOdK#ZNvAEA1Lo6~MazMMVjIf& z^Fh&zJDcsajO1N0l;rwzz%Mn3ZgzhUZGffnlOFm#N<8c{E;<9Fq`IHhQIcUjgY-)X z4oCbzD3+&;cs4i@zn}@eNT>MXtasDain&Ri8OimYWK$Ldz_X;V8<*YarUPrS%WOgl zi0tM2R+X?nxarj}bP^u=(RFRwR~Uz3h^p(0J+ViIXhT1^UX6@!Ogn}B!zX4Pzgmhjeu*X*}Wscj?A`?YND)hl-`2EZ;#SRIN0h>L0`D6c~UYQ zwOB2Opz|v1OJ81vExcoNH`J3C#-L~oupQ&{$GH4$oW6@RGxaq6&M^nR@r4zxsQ2J@ zGHJ;_#gft)i*cc6a_NlNiZ|MU74^P&$RPGkGV6o1bV#o`v0v<*l+I{gSmQ!0iTX2z zXE|Tcrxn01)1HkBt-RI#<*o;D8}P}BL7Zm)nxNYSr?S9az6;FR!H!;qb=S{cy^2~$ zC)?wtCNj-BymYrPIB90qMOwzL_R?-TGs&J_Ei|(AXDEyHO=6T{ZxqS>Q8}!%hLSi2 zEUE1g>BVtR_7=s2tG2c~__WhdKj7{>%kPtx!lMzx1`N*f(YD~#3K%%?=)>mlS&X^O z&tAKhmK@H127t^j&(}D)WL#+BEhKSR3HJ$9WA`prT(Y`2$qp9tQ4e)avh!EdE7>(W zgtct-By`1j);UQ}!;$lmNzB(hq7TpSgHoJS!Ul0r(FQoeo6q>)Fu-BdMUHGN(*y0v zAf75b4)Vl0gE%xP9kp7;ZdMVX-B88-0s2-jt^W>CQ~Btmv@kwAIbrjK#PL(OD$ym_ zU$s-zS~rH7Y5rlDZ%V?rFsPtwWiDSBR(!KczA(kEouZ}ZqIj2zrTxK3toSNaC<7Ss zwG$|c$3nbO?s?9a!f#(FO~>p+)$mV0{*_n8q7qx^p6wS5&IUhPFt(#vudjOpsPq1^?|W-;Zq&*3354AH6zFKUg$ zL*lESOxEi=d681>X3-E9K!&jp$ivBAdj~bJe}?FPZfwyu2f3n(sV(=zX}XHrp+A|S zOJLnkP1EhUOqjl#I}<17=yp~grQgB(zeVY7!2OLeYA4<7t1I+?s3(%-Q5Q z=ymC);G8Ya{xL@%0+apxJbi?ubJ+z7Zx{^yMcTWuD`n$0lM-#}V!vGk6Yk1wU7`jG zjpWEV`jb`RIbf&a8d_WKn-jUC1o^C~>XeiRU52@@p&ktNqu0Ph^RORZL-&&`TXQWv z*zW?VCWEnzaUmQ6OZu?nI`YrTHV#rtNoYRwYyx466N?-W_d+lemY^zob^IQ+cbK@NDx(asl^%S{_ z+$&nj9=RT{Y+*mex7-S2v?cee zTj>XReDAi~>3cAd19#9b~3N5sT~bw3(E~uxHrr^VGGbZ%#TS_Os-9 zYB)JqMDk1uG%&pa$yz}N3UnIL9?#|Q~y>gDQ=!O0`J~Z03isRJz`Q;D$zM-{x;0z6*;vmHWwE)J9nRZn`?Rn^A=d zr{>J;j#ubG_Sye{CVw_}_HN*ln}+YEgDn1Dx|$^_gp%Bj_s}6irgLZCOYi1ud(-=& z@XWK=`{}+-Bj7lBy?%_{^M0Dx=A1)Te^dLMt$WTEZ5o)fjWXjsP_Kq_qxaB$THqT) zxletN#t7+S)gOjd(ZXCGrcYs=SKkW;C9}SJX%)NWUb+F~{?2=0WJCSfbAf&cxcKMXQ*R~zpcPIrn#I>vnM}8c~E5SXQ_n3w(jrQ?%U}qpfRsz#;!?ld5kJGmxWW)f<18m|6 zkZU#%ut%Suf8wf!hKW&*33u-GC+P@T)!Zibu(78xNe8?3Y0#uMcj0NehOBb6p*ujA z0kECVP-R_PngwjPvlkeUiPae#06oMEBw zfnoHq`@TothV|L-ENwRTwrQJrv9+fl`)Atno8q*5F)i+d+?77|?q}(>WRUIpK74hZ zx%ThF8IP>cV(OazglzZo&BtxT z7ft(M9Hdfjwl{KL`Z2-`z%sb;4z}+lx{J=|IkYhsc!@Tzu9TPZX|sBe)}9xiG1f2+ zhV}fHQ;hj)gzfD1Ut(ImU(jm+mscVA+0Iw!Mn+$u%@r_)v8kjG z4TX1KHd!o|bZ+bwdbO}-xD5wYHLz=bLQ8YM{}n{RiZwiv``mBnyC_7?!2f{X4&*NV z4}FIK7lFiKOJBQFoCNM<_Drm z-8y=T&&U$u=;`K5IEl<`DBHz%?HpPAAosrdIy@l*pS>v5l1*c1KsQwtFMF8?XYihb z6Pof>RNz+j`>4Q4`+*lkf#`>0ciXMQ046pZ#>LV607J zBr6mbdF5lho)2X}W}~=HIFM%>xw&;19OT)>^}?sYxFCJ=pw%LK^KK%iiC7`K@fmQ`6eL}byR8hQy~pWagL;j1}Sd1Y0i=< zU_e*ir?_rEt3PVWyH+sCQKsqs+T&dQ5v*T(Zo{*GtFGLmz_<|MeR$bJrNW^t?v+R} zh#8L0M(Xiu?g%zeCR|yOU0J1wHy8UrnQ*k|WuZYY`{GZb^juahd;rGf#d6^U5b&FF z;UIgdLa1RU_|NuAp^QNiY-Mk$5GLRte7QonlFV`Vl`(6jFugesw5c>gjy702mBJ5J ztY%m4<|?6=7`;Q{|m$eql(y4q=FNy*& ze>xcgB2a1QjM!NyLdJi-)(nbt+-mLg>5m$2wSs;W=(yD$2lJ15lhE;5dKXqt2fJsN zFiFpPHLn`0+%0ThA&9G4dbe=Bx2zgN>%xI)xPr@idpcfroz{|Mc{ zUfLr#AlHo5!e8l3z80*QHLS5lkZ7NmHB}3HbNAE;M}*Z5uZTX!A)48teZnrbW1mpL z0{qU!@9r*?!Rz+AQP`Qw?Gr912Y?7S80#A{_IYS>6z2!?6KRqNHS+@=M>Fi{UQo@} z{et1dK>j4sE}YEE_p}ZtwMyccevEDL*hj^~_nmfS%-{^CaE^RtT=B5je!&8B<9+*u zp@t;R$m6~EaMT>mx5*o*V%Q_e{B$)x&LqU?f4bRHkD7$`eeV2$U77rGb)KvQ2d>%fMv6!#YtkLO z>H}5f`#f23AstQn0t$|i$@}ML@{La62oL_%3=5crmdZh#pM{Q*Jm^a_r^LDZ!7tP7 zF|)99PX|t6)6ai7tKA`%1^e5HRKT{N**?wwZU(FBVA~G}64Zy$140jweeVI`(AKav zrzkd%>T{ZA|9wD6z;GI`6MOE&so47oIaj0zCCy7ps4u0Je11pd%+M96?rzi>qO+};%Osd*SG z>kiE$=Kzip!(=QWD~WoP!FrJtE4%BcFv5gm!nQR?#p34os$;@qtJBCLklDA62@k*k zSU3(bX=ML6F4V7a`b-f~&K-Y){?M?u@%6g`^0!mE{4AqQFyfHcNB0x zkGAV{P~o{Lii~k7QewHLn9jA?1(DLImG7x!H%P)qVZt436y9kFVT*O)(5^yCR!vPZ zwj?I3(xIb>j^$oy6!s9Bv~pk~ba+y@1(M|BCxsD6XQ5g60g2^aXcoRt;4AyILpVu& zRvhzHl3UvTb#1$ z2gII=>u0ylTBTvWTX+(ijIAal%E)Oy_jt@%rJ7mb;#9ox_?7%};)=ueID4vB*t>cX z`-11#Uwc7A5mwPBbe6VR^;25C*1}l?HqW+`F~Y>>yd*)h8Is+IZ5u zX(-MY=>R`3(`!Yj_31vrTISAg%jC{Bp3=ty@)=g{5~@doI%y(b`u|`XPOFr_8*v`< z;m5CyIDSq*$e$b)wxqvM0H2Beei{?$Y2+U5MhgOVM8Sk1X`PTAjc*zA?4!V znOs+Xv}5|#Zc%aQqfR;WJ^4|Ges*2Rp^rY{&_^F@aYzxqIVtJTrXl(q`et8WSn@eI z`m_W0fWNS#xFb&V7w$t*F^%*8*O#)FnuHCPH>GSkUJwW4o~xe@2;tQM2mXKta}Nq% z+z`SpC5{HXs&osxHZ5;HK$1?gTe!DgDb^?)-;e#Z$hOh9FeF#z)7TNw_6VoeBprPF z3F2fz&zgjT@5*Ce^$4RSVU#;eQ3p1JYMZ%vuLp+&ZYGTn2|KFt3%5racTe!lPkt1n zOKe6hZ18Q`5p$T_h@v%%F42QXyS=$l3L*@P5V*{KIV9{W!?EEI;|kIYfD*AL;NW36 zws%URjYZl#KnyZ>I{1_Fib$OYjmWG?sD3ot;?k9eN|XquZA z6oi8kHrXT6ddfXhj2Ig5JZ}b^?Xsdz@&$v`+1n}VW9%fR$R*1+aDIzl(-yV z09y8kooC$`47Lor@UZX)w)Rn>lzs9M;S}>eD%7(2M}IM%V=mrlj1Bgy&(tjBaN^<|-k zY(w&g{|$C;I(OkK!fGU|virX(e3JC$PJd0=0WWVlR99*MZU*cJb@N zM>u?jzX46I&%wwgp@jYF8$x2;43-Y|_nFy8zAeev+2i-Gg4dqC@-5+8 zP!%5dws8Ib*+J+3vjYUKOSdIu{*<3k8QWrmKC=3y?;%^&3uh)9q!Crh^*o1C> z^|G}=n(rI6J{t`%Nt|bBkX^_Bk<_M{wE+dr`fqD0KgV@?-N2lSoxoy5Dp*5 zQSpxPq9_I{(WiU#@D&X_W>eTF#K&mI0kUD~@ZkgDtJ3VucZK?T*{uD!DCC1>s#~Ha zUm*$P%wFSyymSVCoB$bw8UUlxS@wnR3O7L#w>~LcjX`|nNg)f>=EPG%rM8oAAA)o28%hWbGQ(6=zK!w_a6X*9xa{l4%nXopw*K-j}t ze<0u}YJT5{S-tlMf)BHL{Re{EH1FW2a{21#^~1OZw61#jUq8_@E=`Ay9zB3GixD>e zoN$;9MA*gWgdzORF22Rnit~T@2D%uwd7Jzaj$)8}W*f?|sXBJz5HItg3>lgz!=bXV z6Nhnz2cSJ+ z&Vd2!l3Y%;f+p}h5ID__|4=x#`4r!-TaXdMY~hDOwN)!?GVHZlt=OBsHxLLIG+4W* zxKR+{T8cOq=J!+BHEg!$F8)y1M%HQhz-hPuzx9F;-HsL)ZhrA2p?fX=eo2fmLRXcto)?7;=d>0%ma%*-y3+<~ah=Q^%!pXw z9wZ*dGt5}HI6AaQ9s~h0go+g)3+mASiVF6?iEz`3_Uff@eZUqu5eE=g6xT`Y*%x7O zU=U(hSFIx$hTd|qE3gvyX_)E90yY7#8-Fb9Hmv-hkW}@i1gdZTSojdcwtiT`EDlT1 z4%3{&F%0nX2@Upq6Id-Z`XOGSq;}=3B_Gbwk}MJaD;kE{DbZkapZbkZy|K17j{HmQ zmsPwZSZY=d3a5JN2Z$C9K*nD7Gz+{W?6=B!y%gt37y8kJH9yZ)*H{Djzkh?gXkdap z{L=r|*0lylRVCpfw?Q@#K_CPIi7^?H30#ukgM}!NgaiT^2#_FP3?YwUGRaIP$>ati z3kW{Yl42|u60~|)~FaQQBrUYtI#!VzWHLaK2?3-Wr`Jnmg5k- zlK+0zczrO4m+o{0;%r(7T#38TjpvMKuUB3GF4cCg6vI1v(Xh=30%SHK$|IgyhsOzUs5GnA?=Q#lM#2dF_W+K7EI6v!-x~f{ zOC%c=nOIGkFMMk>psUhLkm%6yCF3BH=4(bNpSWVI(6u{aTle znto8p2PFuL&s{b$qB4eDF-l?lPc*}<|2+G8XNd&v| zz?3Aa6ais-s#N0Rsf6Ird^MGl5)t&upWlE$qKd;8^DJ1lJ~Q}_+00_N%&+Q^26bSC(TQ2uTk34}hd(I_N0Y-$=Qqv-Atby=eM)T46NX|q;P7Z@(0 zwNa`yV4@ndqq@ckeM}wi*7=Ig`RDtcpH&xJ~ z0=b~EERWmk2Eyu%q#i)VRVrPzrc)}S`{)TxQ)VoZy(eAG=O5{mjQLzmrv*rgGRTFc zdo6>SktEwmJW=tK%**XG*swsUH`s|$GCh+vAbBg31eMNZ(!@R%jt+MdpZwWK2F;TH z@Ste|sRXa9r1P@TgwRR6b~O3|CJdlq85ni2s9?DE;nI*S#52?c5sjkGgfWzdq+<-F zA=y5L9>5qrA45BltT4%wgeXR#uZd)75BP5V6|`()2>~nu*<&fY7iv^xVM`*!MHWq{ z4w5-+n^f551P>s6;+P2596DscbAHq;@|-v)JeDH4=pM-F9UVf2_Z&dDNy%ACugqiAL8IGRg!lTh^XIC>6%di-vZ zZ(aoca1RzA=?CNKq&&Yg0ceTzuM_E*5Qa4m&_CsUWgayl?SF8Sqg9%c4UfEl?>T}*+qE7Z<}Saut?%!Enuz6Ep|C08z_%D!=kW)+@~ z6}Y&B-WQvO*J^2%fm>qJMW6JF4Hu~5l6oo`*R0>**VjR?oO8o?{v)hw~cfF3G8Bpn^UtAqO^P z@-Gc!>s=vQGQPKw(&Oqn@ZA)5?N~@F)m~oONaG=QY;OcLT*mJlEM;EQ~X{Ulzv%==C%!~n$9`-`)FX0X^jetG< z7B3xJF+x3?~@9ub|`by7Gg+4r{N4o(4Amj-D0j zXJ17l;kqLfEEh5=NC%vRN_7G%cqC|k20}M(KY}qcbB0g(ziZ={`~7v46+Pe)Ptm6`yPHoDVhb$e zanI010cYbgv@$+&XJF0Mke&Qx3nixq7eOPvL|+iE>|ZT5PhL;3E7cn;MeUXpMJ5p5 zzm?amr{V-`!03G$(aU%ZnCMObRHryB>ftm^npsvlTOXvz6p&Z(>IO=zP`xP{m+RA% zD_Z9C!R7H+z?V%dL3OnfHfmk;D+J=oT*T+Fsp67p~RfKg+}t^O_UO| z+_kwSaWMNgfdeV!7dPQhUe15pL??L877*Hln{f&*=L?(ZL160r&(eOB`ubT~G`JK& z!f>G;5i)A3G`mL^E8&8zR5jKCWyOjR&b0yOD#~4EzRR5I>Y;XPa@gD0w+o~CZY$-> zBAZMI?-1}aHHaB2njXS0z6y5xKhM+H{!$)3prWXl#rJF@&-f69(ykS)vh1NOup!G4 zibu}I zlyV#KW6p44Ih+x?!#R9n2j!!cJ9bh!%1_uyN&QuML@tsld~+vS=AGo?OS=f(axYUS z^q*>O5AUKl{`6(?;>K>;F4{E6=RjcGNcj5solZ(*+D(~IC8l}I%O85%~aEa=fdjN#^7TyAKp#zNiB%I=dE+Yss<%%EKf~p_d#CEXLo~@hOg@@ zlmn`i_X@2|@;Gu4olb&7wl)fmyEW4#OfocIXI`Pj+`fl~K(#k-502Fqp1cR7bHg6` zW31&c>l~GtuzBK3dnt?8>;V0K%h z8!tLUPxo5DCl1kK7@=exrZSXWdzkVHYa+XPQ2L0%h&e8^&}Ba2G8eea0+-p~GUxTg zjzuo6eBZF~=+|lK&=RE*oR4-Qc(CbuKOB3V?Di5@D5L|xsZeC9Pc(-MqpQpf#J)k) ziyt~dV|d~b@KQw%u0D=C0N)WB%hQfxuXP=vjePAW_2FGdfPYJl(niqX>qluJh{()0 z>7fCLOlV($Po4nhW#hSZ|$ON_Tmp0c2OoD>O!w;y09tB_;42$4JeO<>-p7Iz4scybdf=H_x|BQHH>&0L_bm{YJ(yvJehr`!Pa1A}7D|E+rI%8i5g&Cz2@) zwo#znvbx(2NeAhpx+V$4m|1S0K2Xn9Bax|%KazowpU_FI&NA{{cfgH*T zF35NV?s$*#;$qyv=9hj*0|qv!=EFR9D?jle?d2OMsgplB34Y7Z=1DrpFMWhfyqp6c zft2|XDk#^7AG5U`qzt1ASFh#~ACvgfEdCgzY&k#iF)dD1WU>1IW6{_&sI~h_k4CYnK=J` zNobvt)b&Ry&cALXXVnVwef<8GSk`n;nKEMUlu0(5MVD%#{?)E22}Lq*)}_9WUQCSY zH)$)GP%DHM(xZ+DrV8Vf$rUxV#7&uMDXv7LjgwQRR`qs4AuZ~aWW76X%63OSY1gos*(h;dAk-Us}CM^)9upJuxX}wIaT1 zXVvpdH`24Cs&KDI2gc5+g5mg*bTf7Eo6e~HyH+xcK;WXWkVO{i;D21)CM)Jlo0Jv%9O zmp<=PA6+Hx>wDWb1KKyPNwKF?tz%S*@*%dU8&^A5w2WE;{%F+Hp^mPus5VZD6QzoS zqf(5|saKI^_;oZb4XW>3oxH>`DYlksc}At44BieERzUVj-n7YVZ&fUEE}2MW(o(f& zw5FzdNLythtK)0zHW!CJpH#-He;)Tr)A&q`c%^I#*OXUk=4+9_m6xZz(f}WXU;Xi# zeM?=6B_w6k)oV9zNO^(1Vq8jiO>wc>%4=M_)=-{Cu^2a-Q*uU0#j7G-Q*~U8uC3lQ zFew^KgO7T(so9MjJ(fg5rs}Br*xKGLfx@U1jE48_P^5m|dr&oBw1)&#_eGmGI~2X+ zV6kHqM9iY1p1bIM2Tx9l{iR+)h1SAesh2NRQlF!}e_@G2-bVH8y2_PduO-CkPfV)e zbr&74$pc*ML?1kKp{Fz9mi`E|=lO#Q*S`b`~^VoRy=xL2<{rBnO6UM{h@}z zNzqfP&|K(h)GN99Vznz$v)3{p`zO2nNxNsEm&|lhPK2u3)xHgevQe+8U7g)Wm$;pT5 z_DY@n8VO1{^=SFU6>jfhA{veG31-wQ%MGr0w-_jW-{h5=_~@O5*>vitA)e4zTXd#% z1nevuEcrps$Gq%;1EY~T(5dQ%TMq1r3ebKPEXT+U?Vlq-?4 z^>~XMAsFYAiFAvh(lmMt!&ibDl86%px@q-FIX>L81t0~3lES%w%PR$g7BnszwL3W; zN~8j^=l~gO{K|xj+O_#^+XTmRL);(3;A(1=VCf0cLJUYoyvEg%PAK?(Zq8J^l^m*N zOU>F|uX#4zm>HHwq`0kB9ousApbL-*N7ClfH)jh4^X39NCWCx}sctb`n&6B+L3=n@ z;x98r?43;U>It=d>r>m(-QrZ~qXDnf<{tyrz@t*SFOeqTS+jsZK|sPD`PSm_H{Ds_VAzb9ZpWvlkdp z{Ft?|1xr*bi&nF3kgug&>N=}UGte!1OU;2`_={dm-sM@W-zOe`pK%@L{Yz0 zW!yC0El!qNNoI9f9aEQA*Y2Fk4v+J-7%4$YlBTnb)mB?vlEzd|_14u%-mtB@TAmlj z)ok^lBeAS#uo%piOrkT9l~YcC8p2^-oWgJ-mY_f5muBb1G3}|l$=BB3EE&2DV4y#% z7LQMLi`}K6Or&)#-Km3l zMB0|nF2ix~_pExsP=85Kl7|B$V#X3r`1~o5cZ-~9G)1N6tjRA1Af=>eO-;0@O(~L} z*G_|DZr^Rhnl3bb4Bgi-#| zz{?GqB=%~ZVW^zQ3KZR{37D~Nu}52k9`*i>o79FsuT@{&-LrPOTkI{ZjazLn?ji0A zL1PtJ+uSWql-@L{x9mA2HF2mwt!hf8{L_YF`!0=?M!UtcrRq){5AA0c@Xe_THMK5p zw>VI$)tS{n;Yv#`zR<>?)S0y;Q;C3_PK%D9WDM5S7=xB6IhBqi;%RXvC^=B%P%|~Q z6|QtmjVYCpTx!0i4*KKIYnl7c{Xwahlf()2#NLD8?YX__YG1e5SE|sJ zm5jh>d*8xXyVTXDhI6@YvAtBQtuWTftS<9ff;wGvXLZ>(h|zMo=*dbQ8q!3h$*jqc zolHZnxFb?q*5aLxjU}RDFK9B`=#JRjk(wHJ#FC9&VE=?sO!78h5A`8)h^@~gK;j`+ReeIT}ZZRGz>IAN=be7M?nAO!|M^;y0XRT}`|{4T3=Ea~hP6I$u^ zlUZp35Vl|x&Hg}G?8{0HKKPET)Tf@S+e|vt+v=+5$?UD&!e(af7IrNg&>HT#@VB}O zx^c2wjFx6D`=sh>6I*{!*s9vB(j~I5Kt;h27j)g-qEf1x@#$PG?$cL4;?q$r<&)y- zeb(BQ5nqFJxPES~I-~xN^#Dn!mGzEIz0e&>O$2;8hgZ}@{Q-Bt2ez*GN2pB)0OuS`r?Xh*r%(>NuSimu{azm$>^Z^dcA3V7;Tz0QNtk=k&>$Uz%~+6n-A1h zw|0xMQp+))COho0g%)D!`2%9ZSqRBet?@#CQ$8sfLjO}CT{MmPq){%nr?DoyyxFJO zB;u(0=7ED`RNa5D3p(cf!M=*DPYNmVKn4_T(p9sJ)U5vU;4vt=#zV@c_O#fVT3m%2 zw(aVDhZn0sF?CyodtZwp|*c*+)X7#m;PamIw*e=y} z`U;A!PbaezK3&o6@<~m6F`XglgwJGXYaZ?Cb^F|!rh46xokEwdk-HewCys1d*6HKg zg;Ra`NOo(R&&Eez9A(PSQFj0WStFSC$pYS<2uZRZ&3F5B0oYR*MsH!=CeS@!Hwf2W zbRU5-?9U`p=)Nl?Ro9EHKDDV~v-jaDgax6-HnzjG#Lt8P)wZE7@2O>6@` zQ$0-Bd9&D$mMu$(gY+E_Ps886Jg7G|M zAKlr}ExJmTS_3C)Cb*vUCvdC8~`NJro<`JJ>2s^r>+&G3lqZRm$UxU}IA$}cqZ367La z-S6L`&NZ%7EsdwvE0SDms46a^0VenR8+jWqG#}FLCq?-hc|rwMWzpPHIjKElNyGR`Xm=$W&*TZ2?L!OX8d1%Tom)~l&0Z2` zxd63!LO^M*+S%gj8Hb4^E?nz4K+Nf!E(d~sjR%YVoUV08bGm~ifbnpoQ`G-)Hj(s& zniB^JJQ_pIJJ0!Z!1{m`#l@azNZZXtQL0Ev&V;^{m>kcfhtsvSBk+Yt{>Gpm64nCk z5biw_zc<^oA~7lj3({4LLAZwO(S{h>lY+xBd@P!(fgZdL;DBJc#csQ)OU6^ee{YF$fj^;pg%OYX4=IgsJ(Y&>V?`ewS| z#*5pg)H_;yWtko^s-ExPw0*kJTQi0Uh})y#lJuKa_qFcZ)~v(NTx%F9>8oLl9Y)kd z>-(EiJz}slc3)2Nb2#O4QkdgUe@Ze88)|bo{TWw>_hIDblI@wPsmJm{yMntk8TAKblRGCG44{fk&4G8kX;Qz{*4&Zk5tF5nII&t> zpX9M(glkZe+jMrJioDCtd2z z?VCuadUty@wBD!N4V(Nu;#jGN$((LycdMl1l9k=s;6bsIs)_w_i`-*?>v9(=BQgtp|kS7=FpuBK*C{cHDLGN|ry8>;$p+{wfJW?E|< zIWZA;rDS+H>}Ofu;KXe zQhz9jyN5GwZcZ0v(+REVWRKWcYC56b-Qyq$_18UBmROJ2R(g?k>-wYLExGx!M==IY z0|B>g0VwJvp8M7Z+)zv-Vts9*)(<`g3Gp{i)iM~OU15`=?&>v>wA$Kx$#zA`*yU(i z=Iak{7ax^auxNwo%e!}|5B2WY;CCY)p&JcRFsc~b4*%Qh-lKqjX?}665$A$QNzq~% z_G;6Vlq~Qq^^~^`#`1OEeyDvv^}c({RF61OnoO{;VBKLUc;PZ%AK7EnmSU_&bd@TF z+&T)L4QrCr?h31q-&Cnq4J=bP_uHTud+|H1-q^ooEbNxDn)lePlcOUn^>WK1=GKMn zxLfMu%x=mpHFJhB=9WhJHS5>A@cDJYFzuFF`QkRabuDDnEgA7_pDA|TW=jHyBaj3T zqW%Sw4Ne0;xISDP(FFt_)3s)=6c`>GvBxfaY@mrijQ)IZn7a{9ZWG7dmfmJhUt4eU z35Z$^AXNZ}BMg-W+Km2rNQ%qZDn-5KLIC`nkh9n#620;4sFBlT|=k(gIrry#IV~mJ04Ap`J*;Se!w;$ z2W?8ViWTbmQ@*k$;JJEgWVd?#si%ok?H^%{ig`BcHU-24_tfV2ovuNEih{OI zcWtd0N+o7w2sF(ehr)^V;5vIvjSuUgil?hZH;9gV97`|>aLUu67nRZ`_>^K&N6O^o zA55R#xt71R#fGDnNN_|7s&}0}064sKy2=~m)dD4m!OaA`XlzMwO(G~q<&2ECHh5P0 z5HA!Jd=KS7e^C`2=FcPZV%VRym4>3I-#%S=49bV@Ypt%=FeJ+RS-^~Fsh^TeuXYiSf}q3ZfTs;Z&!h?+uYIsr`w^h)XuMiDV?s{ zQyL;!ik|yPw;ssob|dhnzItYNnI|lI)MftU-X6D;5TZOA){_D^)uAs-?P~Mb6}$*KhaTQQ(Tii}{=LYi zejw1lxHuxU-xB-~$*La?igd!Qo{er?-|B`AaVlLqhyRWH$HlYi8^KDeihnu>sc%NB>400^89Tb7ADq?=506_t6WeY*2??Vq zc8l0sABfBipRTT5iXCn}X22c#E$Z@6743Jc-->O5v~CJj3IMM9R;)q&uh7x54!0?) z&cw&yDStW6{pHINhc+wX3CUL%6uTw4uCwS|jy1NZ_l;MtE4(ZgE9$G`k1q>?s3TB- z+mrj%7sGZSe{9dfHbo!C|G;ld5b>@Er| zNt#g0ClB)XZIjnKTismW)5sz&xiCpN%~jfB_i%I>ONEUNG-gRhBhV8MAhOzcz;=n* zc2?aS-M7})BZiYjwOm$vqTH@aM#~4|O>c);gg+10)s9$mfY4EYT}TH~ikB~fVnTaf z1jTgI0u=pCB`A`px+7*=9%z#MO>=Wmbu_lasxP}2eo!|J`UDp)$t z#wxcZo3tQnTu0XkIAdCbUtCir08|928{(Hh`ex(uVwi6E|A1+@N&W12)qetNz50d3 zjf(&~@ozxA4bj`1&Wg|=UY)2ak6~b(-8C%$GNZnJ_8?vxQ#YK8H!Xr=3KRV|I3`Mk zjcK&?Z*Yt(ybmUAwVC(639to@$Z|&8jb2uAWHm+B}A*;e3~S_?shg14u{paBULt4KM!MBYvyJ3qa^a5LdTwZCtw=kIyfRaz_2hY^D0l z>_+v%>}K^Zv+LAXaNRk#j+|8Y&)GIRn(R~mVRSR<<#QX&1Mhgr$J76gGCrP@>Z5Z5 zt$myhG{b2$iMzSE7GCgn0WV7)UN|?G_aUm zK*bv9A&wYCQRgXvqN7QTPi#>)T{?htrud}>@Z0m3o(2?NzqAv6UFqCC>s@|}6TW=} zxu>vN@p+f}*K@}YXoJzjpDbyEZgiHY*k&*k`;`$@#5&_Ltwt{MN-lqa=w>vc>+KaC zC8G0NrB<%uN3FWgCScVgAc|GDi;QZuf9E;mxxgGsm}mV~Oi5$%X(?*e^|gT2l;+`i z1=mu%1C~)^pswC#g*u;U#7(^xOFVW!^Z3~9+R7{3`Q=B#7J`Gyh^W*`LHk$=*6BRd zcm5=18kJ*KTUp>3WY!xumaCt+{NOpIL9*Hc4T_!DFzao=#gLHVri7?BB&HW!6N&kG zyJLX^%|_)=#%4^Ks_PMGj*N%(2Wu$^(XTDiM6WndTBM*=ckYC&(3>{EkQrELFK9J| z)YKJ*C9YnvLsPGyYTS3lZv4J*1*`P%);#_~D$-z5H;~%Z%im>$kMiUAZC9Up*Ad(c zSMJ9B-YZX3J9@>TQr|wS_KAIHp-Z27{gv^`nO-pyD+hno2+_F&uP`;1f5o0y#q~)C#;ejL!B=5jjO84+N@HOTJi2* zc2A->3R@iirx3(CVV#@nvf3xDSofht8aq4@+;WOboDV_K=;Y`EH%r8l@@E$b!1k+C z2)jOd^?uTVe0inVJ&%7kG!3|nDj;b&2iT7PX- zl~z_rT(HG0QGYt4nKHl@8EUwqp1pSSIo=%1o7zQ{Z7?h{yaJCi>)$uPo7HA5*x=|2 zlDkN~V6xm=4@&j+;IWd!vhZEq0ei5v)?6wchG8NVVvtxWW%v{dbQ6d>-O*!6qX)$@ zo=PZ5(3IrFlJQ6k=*Eg3LWP#Kc4JZqEsD|M7{gFto&o33{Md9aOKoe&;4`V<(_caAUu; zfkY_G{OR=$v}U})x|u@*reDm2+?fO*Tt8x-kJQxks?i&&mUQ%rK@E{ft9thh zhw=OJ4Ldir^opSp`kyTjs7qacw}!Dg&G1n z-X{@&CFd(Jsmk+*@!NI2eRIrdSE4Y!QxShOg8a&Hr^Y@*>V4-cD#K1mjt3G!xv#g) znTREkf(j33QvK2SV>ISe-?+3~y>D_kn|M}OsW#ryzCP)M=ma$dW&Y9JTu8nBmTFEy zI##<99V(SoVQ8DI8~rGzAwFu_(xjj~adi<-*PnuOe8zeRo($@WRPEYG`#( zbODi)$Q=kGE6MIcXr-DvoTu~>$)(=$(I-~AVqB-}nGl=NQTc^SKlUJ5X4J}hRq3wB zfD!+=D|?Y9Fal;SDBw|2E?n{PGExP2xdMGhe81A^}>r{gah#k)qRg?ec=s=7)Z{UfO^e6RqOf0 zC1q}|-fGqGBd9)f&wI87E&gOOI&DfKejyed6v^Qzyc3E#`pFkJF7^m0wT3!FZBT9b z)Ttc`a3qsB;ZKL+5teTZovWq?@EERbDS3hOO#8%!bV#$x(%PDf^ zUN3&Xb+2!wLtDLx3DJucbzVq(`WJi=`tLi3_usp(AHQq=XN&6k>^1;u48KIVaOwS* zZ(JWNrPs}9Ewp}Y-B-y{BNr?Vb<@{wTBj^{dCc(k1uuNzYh{4>g@>QYuY%nhQrEvw zapBq*nwFx)XMQles_g6pQZxA&Vn2VDVhsM*%T@RPu4#qWb$@`Pa-Fg#zRS4tHeXEV+rNE)R?kd`eHxGKiry}Q`qk*e`bPaWrI^Q@ zDc~GW;+p5~JVl**y$F%xTeRY2*0|VQi5NND3=;CI(uCJfaAi>zr951jPt_Z{!gUKkg?tj zoKtK6IYa&9tmj!_i~7VruY$yL{i|WaSw0C{W?|}4_11qKh1=r!e|5m=u>GxOI^a|T zZ?&vY#!+)NBL&n8Z#4mv{`}UfB=o-Tk=Zi;xJg!@|3~E-I6QE=XnFfmB9(>U?PeoO$t@(nUSCSygJHAzW#sR~#j?xE zdzPKlR^P~0tROq7F~YudfNW&PR*?R+#t5RLx>LZ=Xk>S+AT`98e`*C8U4~5@+}7|H z^(Ez4utbcOP#h8?0fr9B#Dis2e+=l{8>K9;o}63FQ@4=b#RXB2{cb(^7LsL)WZ~Bm zyMTju8$r%vQ0|Pt!?jQ$@U{xNfxGCnQhcp`QlxRDH8ROqfCTZ8PnjiiGiHECmRH`&2BFzJ4i79U^#i6aL*z8)F#p( z;2ZYAjbt-8KH0PdPnaL*px96qyz1N|#ST`IUi9Ob zXE&4of!M!wzfh4syPaIQN&t__|7|Z(msYq+?&nrZBJPA&WYXkf5azdIeD?ihoKly> zepyed*jMVwPQfL)*w5>MCN8%20J)yDO7!t#Lq_nsURA5h*q7$gep> zezX#=e$_w*w>ZF{agq>;Q-)Kg42|C5R(ZtaU?;oC+&PDskj0EFx(h*ClLOUYHdQ+c z%2_Mo|Ip+HMUOZsOAd}+4w%JA(T@lCRt5xWWwS@)oBY<~;F-@j&;b(4eJ97P3@4@H3!K|(r25c#iW5BpX(IkI|j zBW%5!Y}x9OQaTsVM<_NIc*d-Uop6&`($1c6lf!`TU)-b$m&zV;@p^~ZJ{kvTqZtHe z0ONLcx`)^280w$-{Pd$Vi2UN5NXLeIN}@So;rIGx?i5q>OHNNICo9F-MVq3__4= z#%uymVfT5-2@L#CUh*!?Cg&rA_T-e9vIL@uv@FH=?Pvg=i;UbHm3ayad>1LG>0kqi zX=$9lkEEO9{;{Zx#96kgk8IgL%}Y~q5T>y-8O1&KO^xzG-0+o@y!;`P7$1)UNtm~f z{Cr*W6r@QT)Me4f@3Q&Ue$quY%d*Lki1*6=AmFq>wT}F~1LS(Ty>rSmuDflT0jg2m zqSp4g**(lRL>?xd{6)j$&&x2mB>_yeowWyu3zzc&@-ZsQ>~N4ghQ~hzNp(dIJs0&| zZtjmue$yD+CleVpZkCCMwwu^TQlxyfXqc3zgNZCiYKjRVvKJL;o z8YfrdzAjAQMq!>XG2=2DCRIya@am!RN5Z5BMSlpBFlo;ZN61$QpIFr-aq??$5^cHi z@0lbQb5`-?C<$&3VxnL#ungiz!N*wq5!`?wG{nFG!tBx*Ig7fl$H*C6JaO{EvXJ(P z!kPw2IcrRy&BFam0`t}H^YAC-FM<4%Gl0k14rsG|$D}rUA0oFM z>@TyBLLIDnj+Ena@f_B+gPoZp7q9KubEI0bf|6ye;+=EEvbqC|iK9gbM38?4wUGRk zueg--Q%pH}8KGpgf>qLpYaEMt!@DuV^OutZ){tC5thgMwf`}Z6-nfEnVbfQTQ+&GL zyMjEzS53W=AOS#OFJ49N&_Fa>kf00rqPOZsOE15h4C8Y2YVsp|QgID=4ws)_L#|rF zAvMeHzn1J~w_Hm`QRxrY5ya^Iigzl^uI8Ifieh-%AFIC|0>_hGz={&wl?tQcaTi^|z7N zBU@SYJ}4I*`Rnc@$0*94x*t#)V}HA!e1=T3yFW)B;9Z`+Kt9eJSn_#tf}-2@2go`? zn)AbXas%1iu`p!IsH|~9sTZ3FJ_I)8Vqbj-?8lYA@?nkw z9jxgQvJ2z!KLR2y=O61^FpFz4+`*1MMt1I9)a-BvtbK44ebMkVH#a;%5_tuemM@bBG0R(@Br_Yjro^_XQtw?; zwQcPBC&`N#;MiB7!4xR}Ghe|vEZi?+Kl}>$N1@=>uahS-vCh5M&JP0Gt4 z2ldB%!#7APt?Lj+p^p?+tz$u>s2i0R9-#km?%`n>E(~ULQDc_XJbDlUh~_j0z|`Qo3lU#i>SoSe*9Bn<2_dXjC_`qH9^X;$A1ab z`~82Do0jk)pZgWrMx6Qg|BBQR(wTqy*W?o%e^RfJqqyAr8fnME|MfM{{Y4JwmxCjA zo_&v5J6O~2K+F!7{2i(6bXZ11QGX^0RkkG6LC%Ydiz?RJ3%gAYF*i4dy|Cg*-PmyC zU-%vAB*3Jq*U4PD3ra%BuSDS{;MB}hpIq#-uM=aP%Mwi>KaBefi(0s8)I5vgdTTsg z{7lN2v`@$zWMjmoX%2dO>34kf_HOjAijC-fi=$AdT?>kwgfu3M+r>AU#QeHO(xeyV z=idOV%UyU|^7KSg+;8k+&-{UW5J(aEBe@>{-S#K&Un9%@2>@`gyZ%JFMF(^pIV3|q z1!S8E7Pu=>2*mJ*w2a^&1fTY%o$dNF*|WYqB2Go*EZ^#_6?<6UpUKebcA2ZYsB3_D z|Kgv?4vQx)8HzOxq9Y?2l8IzeP8md3LNb(kMg52r%dp|U5Yt7P+pJv{krNL8SUZ#c zLb{9#&vAI^M6e3c7?liTGS))oY8F~&pZN=kEjMCwqcJKv^1J`a`6j64Z{!@L@xi|X z^VosElZ|=*|B;W7insadxOdfH5EWMW57M&<&c6gS!B}ESZpy%?G6uH}Oyv20kj7Px zDNzoEkRNQD$v^cEfEisp^e@=X4%Yb=`3~{CZx@Y{fiGN7K~D~L9i53Pl>oXr4jVv4n1c(2|$X8q~QSPe_o5(m!ozjLSB^ zj9pC%dteE@*r|-vaRa)JC;0|4X^XzLa7T(^Er3^gQCeu!#;&J6nFk2H(paOy_AjMj zu!P%|(k9wB#opLUH?eG#H(+FjTA{iMPgDks(&R`sWSV8}< z3}Kj#{Oc=eIiZ7#hg7qg`cP+fHHCkZvDNf(j9~j(YGLuac2FZbi= z(zWz1GRTY<(VMow3oDys3lh$>r1rvk!5hoIc@gFoWH+s&m8^Ch74hM>+vr9n4GPOy zdY#rEUb;}Oc>|6O3M<&3)&bBS=3h^*TxpF#!CVp5>x$(8Gf;PZ(cURC|l*lYmNx!?-6V)QtMNz=5Jx{#R9XHJ+;6gIn zaPEa{Zj{-?X1bkZ*wvfq_i#DAg*t3NTHbq5(%M+af(yW6NlGr+6y!J9u8m*5Yg_YM zN=~>;*bleR?be`WERo41VtA$B@JigrsejAw3I%vEFM3$bR(cxq|NTFp6Mkqb_2B;X zt@Jo1Sig;K#HD!~PzloCw9>!Ao_(}^cqxasG?i2my}a&p~WE#5q)@Meq02c;*EjE_J|Gk zP!+)FVYgKQ<$~<1RrCiW&OTfXWQ?%a_W~I~ikZy{#v8(k9b^8$WDif~*U%Sed!2#a zu@ONElNVc8rW%KHaZ}hlVv93rH$_N*J-VA-v(+WJhg(O)Q7J5&ARuyl6fW#)klNX( zkv3u2A2HJUB?u9Kg?`^ica_8Pj+@{Qv2%Qaxy_&0$~_pPgB{sJ50$%+=Z2I^JvE?{ z3pqHH&h4R&_3Z^Dl%-CtxR~16clOYsEgtw(?4Aaz-BVYm*MM)O6)ptUTtjchWIuI) z?qGkcq5ojNsHU6QG84Vds7OOmY+c5R1^FFMW(C`dN^$^cm{>(zgX{|?y1!Bpb=KFM z4)~LDf$;!_Z<^>I)~3eh7aY6^_NJMB1CGz)>aJtI-G}8)u*0=?Quc52h&9!tF;QHBGYQU<#P)h@4X|&27*+;9{ut+ZuKf9=o+L%+s-~qFB>}(xv zSjz!7#seSz)HJ~LXdRtH##kzm$T$k67-XA;?gtJ`Tj)!GMelyvzQ;8J=a+BTJyI7Q z9vy+pE7@3gViqxA`qds98#vlJX-5W3I9^>h$$ z-*|xTS%o4;M1=hgl5ifN<0P5~;9erEt&Xl{Hyxr6^SxXe;CaxlrX~TYIdqtIZEff5 zOQU013XI|2UQl+8>`RB~nbn?!_jbNFEbqFQK1%g?8W$fr9l<*lWSfoxw-t8lQTp5} zY)jXW)GM{;lQ#M==e4!R=$5T6ZO7M0F%krsb0QVOtN|hgnf*4IG`nj^G`VFN8s^i zV<+gZNs@In({Djn+wP)gNGw0^qKNEHG1f*OSZZISLB+EYFrjxJ+ zmiN$FO1$h)FZ~i?u(1Fj1#(ajHU^jB7wD9n2FE$=Re0%%%CRw?pNNAQq;#VXkmZ?4 z*dNQ1Ug}wnyr&_EYGJeY}s>hr!aS z6~sCUz_G(Un1Nf|kBpTB^0zdhP(Yx0WQ`Zndg0pQDc;3>*rpg3D91;nh%r$vWT(`_ ziH?0*h_*zbMm6@+gQS_I`!$h&M?bw`DI(#BTd;!zR02@X4rsFfh5^7wVRTRvVKsyF zJ>UbM8>C~TJzsGWYQSpum}EF?v04srMf8RtdU#noGBLpc*+;5{jqJ>SgFqhoIbFNS zJP&(12@e8%2B@NInTu=H4$}uf);}Ajw-Gm+Jw?BHxCxE>qcT^lowyxKTMAE!v~Ab zx_2f4e`PDbPg{zQXEAKdyp@mEg;$GaZe+hcL-*}!(g|z4|eF3g+vLV|1rLX4t1?T4|r*?PLRQ>v8}iK(cBg+X*enAodp@ zdwD*aK|DDo9kyDp2gmpgCIMz?M_S**Oh{ib0IAJHvEu**LHB#V|E2tBS!}hd*bgS?ZvnTbCh7dDh1X+w zca(mLyXfAG(O=;G&*Jn3KslM9cGAc0P0$~r@aiPpONYktpH0%Msc>?v_SR`yu?F18 zuD4{Gnr5H=4Bfms0k+izF@rp<1iLIv9qdeoE)|fs#hy0`n^r=VK$Qp-W%lh|!cIeS z8g_w6`$ErgDA+fmtW_+2w<4oGqFJpO=$ZZu^{irC|jR}l+xB` zTfQO(eW%#e=rrvCWBu$j-LehGS3%NyG-bRoIBip=q2vV{L({e}`=4q02jw$&Q@1oBULt(aE z(|Q8UDE7^Od=@vuj^760w_OFxDaf5|T;nU`??nAcZU3;G3i0j9I7Nsr zB?7$InNkp)LH7A~(}%Y6Pq8(#=v6I>%y5cZ?)Tq7ce1;$rjIUlPs1?;Yx5d9vq~A6 zPv*|d^A_3tS3-?neJuvoqz{Z$RSQe=v1{oPTIR$@?DA`Ywl}_qei(4KTu)EH=bOGB zwyiP$o$KjGDd}X&P4okp{f3+Am$yPbbt0b1Z6jnX8G@-qQjdeRvESWHD@hw$dY-zM zPEJb)#BMfxo*It#ln`Yy13c=u;o>YJ9F&_NmzOitGE z?`_&$;o}bW#x2k&Ti8V^06xeLs=!PSn^8enLm+CECg8GI`(D^m1>Ak}z4R0OmG~;H z7Ubr6_N5A8Oa5o?gXKgf@|4jC75b;m%>Qe;pMCJZ=od+A{^afOB`rHUEy@5f`}RZd zxP0#Y^k`WRN*LVw75TCcfHs%(;@QkBtA(BU*$;vb0Th4w5R@+m8~8BYvw8*`uwYJj z*fk%fsSUHBCE zrIBX#(+j{v;d68YNwH0zqb{0hX4W@BerFPN8JqqbBvm~B(a+Ig(&T~$qP1iY6KE8w zQQ>Jq)-M*b0%QKJCuqG;7Dil_efkNyo&DuW{Oo?1ZqHl3LZ^hXP_sDB z{_8v7GGBd)-nR|>p}VkSc@T?}2!#Uv_>>vZ$sF}`l?c(n*J(^y+wiE7SZ zY1_FH)rU64vG)cm~+Qu6Tx4u>H@{Q`DycVr%~HXX&2_^(+F!szDBj(Po5T=h+hv-~;*y z-J0L_Jh;sgceChhW?wQ2dzLmebKbz#CqWkoH zKHaPdcO(1I3-prJ079x@M4Vthdw~vBbTwleq90k`b;GB!btC)uhSE0X`7SunBs>3I zdNs&-%Zs$he4<&~D~z2&jzj`FcHGp~tZyyiN9zUUe(gBoxw%R9t{3U$q=zl}9yOD$ z{QmFJpAck|mNMP~#}s>jXRUC31DSBJp1GfqSzn?&t?blGaPSA&yI!KlR(3aQDaAO0 zhCTffZLRAC=t0pL3?LH$i$Ed@w|06t=A;zH+ks5jZRucKs{Ru^jn( zUP0hOFc$bZTk|TUf`iq*3d}g0?|zjwEm^P37u1h{7BWMQ&>cUg5fIbLU(m;C5S}Tn zSp4J{w6hY@ADc@`k{r9FO%{tKn0Ne=UMhe)f9cosyJQhz&gAcSjowCK4j=tJSZjO! z{lBO06)-vLAL)~%KmVsc(tj?6Ao;`pLBNm+7F!~8=U@6Kdba`tG6ga`iFv_62qOrA zr#^jYOV3CR;!E4d&b|fK-@zBNl6~+kx)#Fk-nZa~8T`Ijd%=6-Y=#Aw3A=ZsN2R^PaAW!FfKTY9*r8!$ zFL>mg%Y;49p}(I0IdFfIxz=6~gC$;q5Dh zyD{lMtQ0J`RId`YVOK|hRsWN0SpzS>qVE~NxsrK#m2fA4gj=~raF(HN_j)rgVBy;HG@b+Nbz>R@#@QtA zqnX`VE*w~gO{I$g<7K}n7aCy2?5PmCDt(JzmcW!_*H!>~3ND)v`%#7PMcBV=lW+`6 z{@qQ&ejzq0vU_ibK-*m@80gd}XYn;GRw)cKp;D-3zp50ru;xlw#Fd+c$+d-*8dq?1 znGJ^2X5ssbXgHToZ4tVVc*368DwJO|I%>9KCraTPU;e+h3cE?!^r)C+E4K^hDzVV% z0_UCrr^_U;Z48GWu{*X4<>%ad>P>t>_ME8%#VZuGp-6|BR%23s+@}FeE9pjw4m9ntr|?*8vX_nQ04+?gYjy}@ z;LX3;A-qk$VIw<*b9nvoPGJ;+N7^L>h2Bv!+f^+X*{csj*?Vpm;L*cYR|$WENr4L6 zxZUDhGKs7JJA3t6x+{NmwQyJ{YZ?_>*%LmJ-q^1E)_ zP{=G&p92+d0HrkHUclmDA{BP+H_=MDi7(7nL(1|dJW14uwsjw8` zN7)3~-6o-B$7ta|E^pyDx@=Cu-ylFf9Fjh~Y?WEqxhI=R~Td&2~dQ`3)aY1Qr!-+~41!sf%~u-V?5w12U9 z>iHU~P$z~L3$ z5AhK@C_I7|_Z=Gi22(982{QVTW3V7|^Sh{&R_@_mmiE(xE2O;y<- z+zJ8snFe9Y@`zPBaM%hn>bV91#%6;3zESuXxVm~AVkpjjb6og7N#&nx628Y@-tH8R z)3g=Gac#{1-YFm>F8x5Wa0#I|DzmU_OEWCiiFrMx3In5 zXq8etgV}+h!zCcj6!w}ASS9T|x_+ya;P1mRDUGY$$f9m&H3RGpH=ux+aF5WBxCzb_ z4>Gk!sO!|?^D%%NNglA5`8M>@=HGrRV!AWh*7RZ-@(BJ@gZ##Y5*)L&s{uW(o0khNH$c)mV%vLa1J-y@J-oCpIp_NG@jxisPA+dB{` z6FQehoY?gyvwMBQ;6^_-fTBAmwq9!6rqL;@>=SBt1W+_QIa1&wI2*UHfmutVV55CP zB}Sg@6B@UroF*?KVojo3^dX*ZZ)%Xj0MT?UsQcS}!k!A8?G527Ba>hr`x>i*P9BV7 ztNMjI5pFFerYQM``-LZ0q0vjn1QV3JGh;$!{;@HrjDk`ZWWNXqrv;@pm~RORzgUK9 zmZ*U2nYS|P-TaQn?r@09#EZx-T|DiVA4W{^OTji|z9wTeyipla&FU>!C*N->V_kh?v@U3iVoJ;3D_p;x4CQaV3%mhow>$0@o+M-WpWY3cKsOcl2(?(L##tEw zp%f6f;~rsWxl(#Yk{|Ncqw{z(e~)mgY#IP%H{Bx~W*a^!oFLQrflmti1hoIIPYZWb zs770!|&_My)TQ)`=@i}Ec;^M?Bc2haSJF9??`gA#Z7Bf@F+kB0>tR6yL{%`X0; zAT#G9g2Z0ocklnA;AEyph21S#CkR}XTsXk~@R(4;l8+1c6Fd1E9~YV^EB~^v zbs6><+JR}zvjpD>UlzF2x%x=iR<{4k!g3*4eAA`9$&Y?ns3seb^%3BEir}qu@{D>@ zFn`OF!V)BsvfI8Qe1@KIvJc*0X1qK4Re`K&a*9r;=yr;oPDB=`zXn*hvD?2Ud>l*Z zcuF`&CkuoA-cv$qW1Ca^>kNw)x0`+N8$uPE{kjmw7G-Swi4+ZaM6I3?4ps4OJi3n^ zVhu-w1SYYS1czAIxw+4S%bE{;=xM>u&R<0<*e9PBRF@N6gTW zwA`xcDA%6C*!hUR=`+snW8jk5L0e#PRmpMo^f!e%YK*fVJy>R7Tb~sUA^Xt%EC|}j zlxGFLRpV>V3I~rU(n;rV@jK*6Xx@#ebok&U^*mft+^)lSS=({`Yg9UTa9{K^ydK{Y z>gp7;_Gh1}_&GDixXE8k139zXIIGN`!QU7_f}pm5ajB18_buUCSmfo;!IExa>T^O4 zp6J!jgLm*Pa(kZ_#B<4UY#hQKZAcJ+Bk=a-Vg5reEmcu?m3NGwl!!76KZM80|55^W zK~6Z0v{fgn=EzawD;I zoSpifa2rB_mhTI@So!ybEtnPV8!)Tb_XR&@_1yOb{Bay7KS9f1H=`e%HLi7~?IP9^ zDoqCtAKnK|%EQ`U5)RVwadz${p$~t1i|@0v;`m;^k1c`C)~0}jqY)&(*@iM~ZB86J zz{~t7LjoqsZ~|=N*g>4>fqHOEX=Mj_*&$xWPkhwt9pYt&^>2%1hk4n>kQlrZe9XK* z5VmYrYH_4qZFEFS;2$4{iBd$7sv&mG4*NM6f(@`-~U5l16i&m=?396`@)Ze_*Oh20bmT95zsZlx=2`#@~sKL2IJDrUKUKd(2*F`wP2k1QTV#loWig}VhgKy zML69Qv>|^vihPQBF#(~#6U73Ha?2MtR7iG#7qO^Z000P&y&`-J@>)NWU@l;k2`w!L2^rY4EhO6`{kuL_px#e={} zn);!g#bb!EF+9k6UlsORm4aT0Q=JQ-)nzn4Lsi!}LxsOagY0FjI=kmpFm#1I^Qthq zvXr>0utPr;hWGKqM=(!*fu7sP^TqW)x6%l)pdFjRsjD6bMzV)~DtvY$c#z)dEY7wJ z;;=}h6VLtZ|J8M+(NR@N_@rO4h6skR1hbihEb<^BEJ`4f&9J1|lCUJ1h@HHo(@8HR zoz8-0RB!>q;J85HphqH$NCXs-7lhF>bHp=eW^_b08PE}hqsS(%$I;RG?tT3_oiOvK zZ{PLZSNB%ct-4k9l=N(TxkKn`(M_g^$OO+%L|KOvf&|4fhf?WJHoIh=Rdg4=W#T_Z zx%{aTc|RVC8}%y4Cc^y7{I3ct9;dAhJ^cRyO9L&gSteRZA47afMi$D97{{zoxA z&1XM@&Ol@3ypj{)2T?G5RKD^lGzXsZ%1pG{dtNE(C!!TPx&9)A(%1{ikVtKY)1>zz z%#IZOfJNn+o__(-)-AnOMnV9S1Gb5$Px)M#w)}rRHn0G}i=mouN1^$*2e|(SoEpFT zxso`?52yJE0S{M(HfOaML!=X`yxtOF{Tl>2%&rg2M5TYunO`Vgz+%rA$~q*|FG5_? zxa^|Rh2%G1DGT_}Wu=`z_!8$#jp-6pJ{nKHqzKzy+a;wk6g$We+km_8T~d;R3pQR> z;2)U%LWL=G0 zuPL7)!CYKO{`!^jAQJ0!WlAKL0=!TPA^@PRv8q$;kq&#-r^cxxQ_Fz#?G&y zs@C|#*NSkWocfI%c>Om@OBmeRO;n51IQ(1iV~sn$RZ<2Tqu;9Wp>F|&Q~$zg=I~|3 z9pOu2XUt;#p z%H!~#=l3HCghKy1lD@QrNg{3%7`_-q;{E$a6RgbjanV#E1ctToq62ru69P5!>3E7B zgBVlZoI1qxlxKXQwJVko0M#3oK*9@jQUWC-DNmr(k%9%lJa2)!SfV}730lYw?#qo=5^Z z-H9|7$;XNEHI6Q)2#y;|F({fjmLidqj+Oo4(k!QGwSkw2aUVCzCB^eD_EwwY9vMp$ zM&WsQd&9`J#JF4%f-4VB@=rz(y4*GyA4QaBCsD*;1mSYH9CFfkCs8~C<`|PMkQOMS z*%L`}&Tk~i37$@(rI_GE7-fkCETw2ZkW2_+!-G=@4xL6(RSHEYf>f_eAx87?R9cUu zGnE9oyq8MTLbQ$JC;_C&1vg(L0wM3^PtGVKc~}Mwcx02s z;B*oAFStgNBt`(Q$!}6bq$(qWw&q{p3-?b0$Tdz&BjF2Em_~OXc_@u05B8HgL1SH| zl)x9#X!J0HAtWW4-ZvbUPSO31QnnQ%o=09frN$ZsG7=KRYM9dmi3lR7MMpZNVL^8Vb(ovCr><(ZY(tXqD z4T0~j8FWU}&&eh?Qtr;9He@sM>DK|sW-p)!y}(L8O4_i1I*{_;7ZJ#@o?S$#{YD|m z1$^aj(Sy@lzTt8Q{Z9cuu#~<)BgZnT7#If4K;T@e>BsJ&Be?X}pQ$3;KIZD9?Q|-H z4>;&dVi_DYq-b+*gRi==ZK18v=F0Topit%XY1Q+EX3EPu>nQ!EK4l-)f&7&5Pj!@A zh6qtL!j(``+DydX3+ioahe(NFv_e>{g6eE~8JwfAg^iG>pjRUFDT-#I5~4|!iXU%9 zCURc*zHweX5Voq%h%iua)>G0DL!Cap#!>Cm`1yL83RUe%bbx~3r+P|`on^$Tj;|Dg zKQ#aTtnr=4>9aV;Nh4~^Qs}gy8Nf$(0_~s+@U#?F*Wg3EweqWPff{kec$YNG>}hTI z67?+=m?(zi!%m70OvspcftWYUC4`Vz=@gAdyC~{@b3(pGCjx>5%}1`#;=ZLe_*e*c zCd?2rx!KtTcR`Gd_3J6arYV-X=iW`1dkkNIc(uI1QIR@5Fot<_^ZN>*9<}BJ%*ue@ zAYP5$1ju|u80K;9M5bktNaTX3u!s(Y~1%b%&??W*&E@3z^xdLOlXB$psqo3eG3 zra6Pw(^9h!P!c<^lADBJP!V}4Rgug z0PvM?^9EXiZTs#9dI{}XHd5)pRwy}e&)d#18z`24*ho(p4GQ@<)AHN$pktB&IR#;B z!oDtVv8xO0YL4B|Mg_=k6IX1ad+@4nZ6ZVz(EC3_brznqiEids4l2YCZ>A|jnuNB% zP`u>xJDbTl)xGYI7(!mwJ+ z=T6X=er{M5xcP6L6g^7zMeH2Wy*=>O4*s>1p5$LTN##GjKhxB6GQ81>NUaDB73k^VxAsCk%G)TF=WL}BoV5Yl}zTaatK(rFx*6!-2N>A%} z7}$Cm$HM>*)Xr!Y)*4bf5s5!m(7flV<0J;E(8^qq_ z!zU>=p~i}n0#ulRU0jRD3vFtk@d~jX(frR{prr*|*hO=OuLvG2QSJ=Bxymwv4|kD^ z=XO!3KKW%J6f~+GyXp2ZXyPzvIxb>xt__Us;N;1PP$9-14%OZHFjkhi}^ z<2Y(R!k*p5AMd9~p0}TV&y)6JOaFj>RE&KiZb8!cjn^p&bn4veR5la=_)_l02S5ai zsDQ?N?g4VA3Mo*QYbb&@nj>{OdgE?WdhCg>%r_utU+T;PK6il9aY_%@u|}1=TBkf- zr2}=&=s==(#J|XHAR={xB$jql#!W53P+TPFBfhrYK>UlH2J@kAS`adeLl4p&DJISF zyCd}w9V3#8R^(}sq)D$oNJSP{L%ey2d?AbU$%ko)#ZqSH^<|ckx#lRaAc6ta4TIzw zu1`JX=Ha5JLO=f|%|c|2W@ArhzfD7ilws!?3)9prpA%<3r_X^8w%+2i;~Rh{8-K@HsRd2IuYwm-~t=w>u>cHX7 Uo+MbGY&-?#=hQP!(Ifr;3tf3z8~^|S diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index ee27fd56b061756cf85435f95eeee535f5c11d25..15c4dcf8603cfd787510edd851da58a6f4d0e250 100644 GIT binary patch delta 4490 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 delta 3289 zcmZ`(Yiv}<71rYS&fUvi41Qo9V^sk=zO2K;HpUM!d%a%P_S&Y)VoagRjraP2*SpK! zwapqFr%Dt;W0HUgM~#&N`B9`)RfA}_s)X94Rh#q?AQd9%Q}jolA|X;xnyRYOGjrE? z4eCm>GxyAybH4e`H|K+k?a#d2{$^vt+@npkbLY3aE{7UE%$d8|;F{}iN=Snv(Ug&F zA2Fg4bB;EZKXbR~_}wN4j%7GR>dA&O7Kz}6(^QWp4Ly?Hlr+qEbi#)6UH$V@M8_6_H@Jt5Q-1OM+Fv(1eROZ{E;nO-&z6@=5;$%IapO_*gu%@1r ztUwf$PA?qdD_DNbSynFYmj%r9h#Htx@J5HU4*uIA$wjoqCc6nP50}GP+QY1@CBl$o9>~{(qfB6uV3h`oR3N#ASUx zOr~8+3N$l;591dTWc?EBy$KR^sTnyM-Kax&Vv@9!bE(+BkwIEztJU7Vkegdvf?Oeq zU-Niuc2%L^gK+!0vrNKQgGX#q;Xi3YU7`XD{_xom@)bD$gPc5zBGHa)IM()~WT#6u zji@n@qWC{%#8U9$^JELW*X@+xH_sC_(HUYoS?{z<7aR|p zqr0SA51XN@Lpt_wL07>aJ0!JuNulX9mk!*>$t#256Gbxo;bNW>c6Lf3_(`)=2H)+J z>g?70O{o~9F6mb=)FQdyFI`e|g}Tt=1oS0=p)x2{I-^F)e&l=MsY$Xvm#Ya*&647R zOml>%PLfjoj(Nl2ZDGOQ`VF!c^pm74+p`>wU2_)B-1o9yFg9rHHB?U~^)$9w-g&T- zW`tw%M#}>W{bd?VM%6eiKKqO`7Y!;!WoO`s`yI@5Pmt532tK)zlMmOJ(*=p;dAX|= z&+$MDw{WCU=AT@5I^pr46n~&3Z};bwp8ZbY3Yb1r=!D1K$9Q*w(%L1OJzml+;c!_c z-E#95hu;c4ExRS0BeQa2z9RtNJ}uYb=grgdM};_lZ(gqA+qW*tAG%8XjOpr-muXL` zLFVgFJDJ8*zxC6%wYP_^KrZVhI|S? zKe{3Rkeg5cUVe6&BVd*ON&a=A5_lkk-@5l#`CoX@f_SK$2SCHL#O6-NRcY7b+R zUNywDBVHAQ9>%p6OnR;G757zVPVaD=rM}~irB_Y*l0LPcv29+}$5_3lYD{Z{@9$9N zHfjlfyBhbR)vF@z?OxpLnQBw}80!7I4mqvaJgBj z?7`R}jw8&EBn6}(E}2i&8S7UK#)dcmEunERojhNF=jh9F5}v#|6B?AZC>tC;Xl+q8 z&y4YKJSg5d+aCk<@yG+bq-;ba9pL$EeH==F|LOebA+O3Z`9hf4w~r?cpafV@-Obo; z^zjV~essR0@q>PjuAi?;{bFC^hRl9|1)^R!)uOB}5TN5;xYVMobeZU4`qa=??^N3` ze9)o5Z@kWyX~)0PmU?X$xO zxC<5H90Zb@)x?y^{I!{kf-~eufE0QOF<+x~-LC*XBx@+3g!99`E}LD!v+%^fpnWEc zD)idhB9Yi05Tz0oSv2a4#y;l%s`aBDK1$7~%W(I9ps;m>m>sQz_f 0) { // If the field has a value, add it to the map. - this.filterFields.updateValue(fieldName, elementValue); - - const current = this.filterFields.get(fieldName); + this.dynamicParams.updateValue(fieldName, elementValue); + // Get the updated value. + const current = this.dynamicParams.get(fieldName); if (typeof current !== 'undefined') { - const { queryParam, queryValue, includeNull } = current; + const { queryParam, queryValue } = current; let value = [] as Stringifiable[]; - if (includeNull) { - value = [...value, null]; + + if (this.staticParams.has(queryParam)) { + // If the field is defined in `staticParams`, we should merge the dynamic value with + // the static value. + const staticValue = this.staticParams.get(queryParam); + if (typeof staticValue !== 'undefined') { + value = [...staticValue, ...queryValue]; + } + } else { + // If the field is _not_ defined in `staticParams`, we should replace the current value + // with the new dynamic value. + value = queryValue; } - if (queryValue.length > 0) { - value = [...value, ...queryValue]; + if (value.length > 0) { this.queryParams.set(queryParam, value); } else { this.queryParams.delete(queryParam); @@ -674,7 +673,7 @@ export class APISelect { } } else { // Otherwise, delete it (we don't want to send an empty query like `?site_id=`) - const queryParam = this.filterFields.queryParam(fieldName); + const queryParam = this.dynamicParams.queryParam(fieldName); if (queryParam !== null) { this.queryParams.delete(queryParam); } @@ -773,17 +772,48 @@ export class APISelect { } /** - * Determine if a select element should be filtered by the value of another select element. + * Determine if a this instances' options should be filtered by the value of another select + * element. * - * Looks for the DOM attribute `data-filter-fields`, the value of which is a JSON array of + * Looks for the DOM attribute `data-dynamic-params`, the value of which is a JSON array of * objects containing information about how to handle the related field. */ - private getFilterFields(): void { - const serialized = this.base.getAttribute('data-filter-fields'); + private getDynamicParams(): void { + const serialized = this.base.getAttribute('data-dynamic-params'); try { - this.filterFields.addFromJson(serialized); + this.dynamicParams.addFromJson(serialized); } catch (err) { - console.group(`Unable to determine filter fields for select field '${this.name}'`); + console.group(`Unable to determine dynamic query parameters for select field '${this.name}'`); + console.warn(err); + console.groupEnd(); + } + } + + /** + * Determine if this instance's options should be filtered by static values passed from the + * server. + * + * Looks for the DOM attribute `data-static-params`, the value of which is a JSON array of + * objects containing key/value pairs to add to `this.staticParams`. + */ + private getStaticParams(): void { + const serialized = this.base.getAttribute('data-static-params'); + + try { + if (isTruthy(serialized)) { + const deserialized = JSON.parse(serialized); + if (isStaticParams(deserialized)) { + for (const { queryParam, queryValue } of deserialized) { + if (Array.isArray(queryValue)) { + this.staticParams.set(queryParam, queryValue); + } else { + this.staticParams.set(queryParam, [queryValue]); + } + } + } + } + } catch (err) { + console.group(`Unable to determine static query parameters for select field '${this.name}'`); console.warn(err); console.groupEnd(); } diff --git a/netbox/project-static/src/select/api/filterFields.ts b/netbox/project-static/src/select/api/dynamicParams.ts similarity index 51% rename from netbox/project-static/src/select/api/filterFields.ts rename to netbox/project-static/src/select/api/dynamicParams.ts index 75d79d849..c31c1962b 100644 --- a/netbox/project-static/src/select/api/filterFields.ts +++ b/netbox/project-static/src/select/api/dynamicParams.ts @@ -1,20 +1,19 @@ import { isTruthy } from '../../util'; -import { isDataFilterFields } from './types'; +import { isDataDynamicParams } from './types'; -import type { Stringifiable } from 'query-string'; -import type { FilterFieldValue } from './types'; +import type { QueryParam } from './types'; /** * Extension of built-in `Map` to add convenience functions. */ -export class FilterFieldMap extends Map { +export class DynamicParamsMap extends Map { /** * Get the query parameter key based on field name. * * @param fieldName Related field name. * @returns `queryParam` key. */ - public queryParam(fieldName: string): Nullable { + public queryParam(fieldName: string): Nullable { const value = this.get(fieldName); if (typeof value !== 'undefined') { return value.queryParam; @@ -28,7 +27,7 @@ export class FilterFieldMap extends Map { * @param fieldName Related field name. * @returns `queryValue` value, or an empty array if there is no corresponding Map entry. */ - public queryValue(fieldName: string): FilterFieldValue['queryValue'] { + public queryValue(fieldName: string): QueryParam['queryValue'] { const value = this.get(fieldName); if (typeof value !== 'undefined') { return value.queryValue; @@ -43,43 +42,33 @@ export class FilterFieldMap extends Map { * @param queryValue New value. * @returns `true` if the update was successful, `false` if there was no corresponding Map entry. */ - public updateValue(fieldName: string, queryValue: FilterFieldValue['queryValue']): boolean { + public updateValue(fieldName: string, queryValue: QueryParam['queryValue']): boolean { const current = this.get(fieldName); if (isTruthy(current)) { - const { queryParam, includeNull } = current; - this.set(fieldName, { queryParam, queryValue, includeNull }); + const { queryParam } = current; + this.set(fieldName, { queryParam, queryValue }); return true; } return false; } /** - * Populate the underlying map based on the JSON passed in the `data-filter-fields` attribute. + * Populate the underlying map based on the JSON passed in the `data-dynamic-params` attribute. * - * @param json Raw JSON string from `data-filter-fields` attribute. + * @param json Raw JSON string from `data-dynamic-params` attribute. */ public addFromJson(json: string | null | undefined): void { if (isTruthy(json)) { const deserialized = JSON.parse(json); // Ensure the value is the data structure we expect. - if (isDataFilterFields(deserialized)) { - for (const { queryParam, fieldName, defaultValue, includeNull } of deserialized) { - let queryValue = [] as Stringifiable[]; - if (isTruthy(defaultValue)) { - // Add the default value, if it exists. - if (Array.isArray(defaultValue)) { - // If the default value is an array, add all elements to the value. - queryValue = [...queryValue, ...defaultValue]; - } else { - queryValue = [defaultValue]; - } - } + if (isDataDynamicParams(deserialized)) { + for (const { queryParam, fieldName } of deserialized) { // Populate the underlying map with the initial data. - this.set(fieldName, { queryParam, queryValue, includeNull }); + this.set(fieldName, { queryParam, queryValue: [] }); } } else { throw new Error( - `Data from 'data-filter-fields' attribute is improperly formatted: '${json}'`, + `Data from 'data-dynamic-params' attribute is improperly formatted: '${json}'`, ); } } diff --git a/netbox/project-static/src/select/api/types.ts b/netbox/project-static/src/select/api/types.ts index 304054336..a6ab12794 100644 --- a/netbox/project-static/src/select/api/types.ts +++ b/netbox/project-static/src/select/api/types.ts @@ -27,6 +27,40 @@ export type FilterFieldValue = { includeNull: boolean; }; +/** + * JSON data structure from `data-dynamic-params` attribute. + */ +export type DataDynamicParam = { + /** + * Name of form field to track. + * + * @example [name="tenant_group"] + */ + fieldName: string; + /** + * Query param key. + * + * @example group_id + */ + queryParam: string; +}; + +/** + * `queryParams` Map value. + */ +export type QueryParam = { + queryParam: string; + queryValue: Stringifiable[]; +}; + +/** + * JSON data structure from `data-static-params` attribute. + */ +export type DataStaticParam = { + queryParam: string; + queryValue: Stringifiable | Stringifiable[]; +}; + /** * JSON data passed from Django on the `data-filter-fields` attribute. */ @@ -109,3 +143,47 @@ export function isDataFilterFields(value: unknown): value is DataFilterFields[] } return false; } + +/** + * Strict Type Guard to determine if a deserialized value from the `data-dynamic-params` attribute + * is of type `DataDynamicParam[]`. + * + * @param value Deserialized value from `data-dynamic-params` attribute. + */ +export function isDataDynamicParams(value: unknown): value is DataDynamicParam[] { + if (Array.isArray(value)) { + for (const item of value) { + if (typeof item === 'object' && item !== null) { + if ('fieldName' in item && 'queryParam' in item) { + return ( + typeof (item as DataDynamicParam).fieldName === 'string' && + typeof (item as DataDynamicParam).queryParam === 'string' + ); + } + } + } + } + return false; +} + +/** + * Strict Type Guard to determine if a deserialized value from the `data-static-params` attribute + * is of type `DataStaticParam[]`. + * + * @param value Deserialized value from `data-static-params` attribute. + */ +export function isStaticParams(value: unknown): value is DataStaticParam[] { + if (Array.isArray(value)) { + for (const item of value) { + if (typeof item === 'object' && item !== null) { + if ('queryParam' in item && 'queryValue' in item) { + return ( + typeof (item as DataStaticParam).queryParam === 'string' && + typeof (item as DataStaticParam).queryValue !== 'undefined' + ); + } + } + } + } + return false; +} diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 6de51a29f..e22311b71 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -1,5 +1,5 @@ import json -from typing import Dict, Sequence, Union +from typing import Dict, Sequence, List, Tuple, Union from django import forms from django.conf import settings @@ -28,6 +28,9 @@ __all__ = ( ) JSONPrimitive = Union[str, bool, int, float, None] +QueryParamValue = Union[JSONPrimitive, Sequence[JSONPrimitive]] +QueryParam = Dict[str, QueryParamValue] +ProcessedParams = Sequence[Dict[str, Sequence[JSONPrimitive]]] class SmallTextarea(forms.Textarea): @@ -138,88 +141,132 @@ class APISelect(SelectWithDisabled): :param api_url: API endpoint URL. Required if not set automatically by the parent field. """ + + dynamic_params: Dict[str, str] + static_params: Dict[str, List[str]] + def __init__(self, api_url=None, full=False, *args, **kwargs): super().__init__(*args, **kwargs) self.attrs['class'] = 'netbox-api-select' + self.dynamic_params: Dict[str, List[str]] = {} + self.static_params: Dict[str, List[str]] = {} + if api_url: self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH - def add_query_param(self, key: str, value: JSONPrimitive) -> None: + def _process_query_param(self, key: str, value: JSONPrimitive) -> None: """ - Add a query parameter with a static value to the API request. + Based on query param value's type and value, update instance's dynamic/static params. """ - self.add_filter_fields({'accessor': key, 'field_name': key, 'default_value': value}) + if isinstance(value, str): + # Coerce `True` boolean. + if value.lower() == 'true': + value = True + # Coerce `False` boolean. + elif value.lower() == 'false': + value = False + # Query parameters cannot have a `None` (or `null` in JSON) type, convert + # `None` types to `'null'` so that ?key=null is used in the query URL. + elif value is None: + value = 'null' - def add_filter_fields(self, filter_fields: Union[Dict[str, JSONPrimitive], Sequence[Dict[str, JSONPrimitive]]]) -> None: + # Check type of `value` again, since it may have changed. + if isinstance(value, str): + if value.startswith('$'): + # A value starting with `$` indicates a dynamic query param, where the + # initial value is unknown and will be updated at the JavaScript layer + # as the related form field's value changes. + field_name = value.strip('$') + self.dynamic_params[field_name] = key + else: + # A value _not_ starting with `$` indicates a static query param, where + # the value is already known and should not be changed at the JavaScript + # layer. + if key in self.static_params: + current = self.static_params[key] + self.static_params[key] = [*current, value] + else: + self.static_params[key] = [value] + else: + # Any non-string values are passed through as static query params, since + # dynamic query param values have to be a string (in order to start with + # `$`). + if key in self.static_params: + current = self.static_params[key] + self.static_params[key] = [*current, value] + else: + self.static_params[key] = [value] + + def _process_query_params(self, query_params: QueryParam) -> None: """ - Add details about another form field, the value for which should - be added to this APISelect's URL query parameters. - - :Example: - - ```python - { - 'field_name': 'tenant_group', - 'accessor': 'tenant', - 'default_value': 1, - 'include_null': False, - } - ``` - - :param filter_fields: Dict or list of dicts with the following properties: - - - accessor: The related field's property name. For example, on the - `Tenant`model, a related model might be `TenantGroup`. In - this case, `accessor` would be `group_id`. - - - field_name: The related field's form name. In the above `Tenant` - example, `field_name` would be `tenant_group`. - - - default_value: (Optional) Set a default initial value, which can be - overridden if the field changes. - - - include_null: (Optional) Include `null` on queries for the related - field. For example, if `True`, `?=null` will - be added to all API queries for this field. - + Process an entire query_params dictionary, and handle primitive or list values. + """ + for key, value in query_params.items(): + if isinstance(value, (List, Tuple)): + # If value is a list/tuple, iterate through each item. + for item in value: + self._process_query_param(key, item) + else: + self._process_query_param(key, value) + + def _serialize_params(self, key: str, params: ProcessedParams) -> None: + """ + Serialize dynamic or static query params to JSON and add the serialized value to + the widget attributes by `key`. """ - key = 'data-filter-fields' # Deserialize the current serialized value from the widget, using an empty JSON # array as a fallback in the event one is not defined. current = json.loads(self.attrs.get(key, '[]')) - # Create a new list of filter fields using camelCse to align with front-end code standards - # (this value will be read and used heavily at the JavaScript layer). - update: Sequence[Dict[str, str]] = [] - try: - if isinstance(filter_fields, Sequence): - update = [ - { - 'fieldName': field['field_name'], - 'queryParam': field['accessor'], - 'defaultValue': field.get('default_value'), - 'includeNull': field.get('include_null', False), - } for field in filter_fields - ] - elif isinstance(filter_fields, Dict): - update = [ - { - 'fieldName': filter_fields['field_name'], - 'queryParam': filter_fields['accessor'], - 'defaultValue': filter_fields.get('default_value'), - 'includeNull': filter_fields.get('include_null', False), - } - ] - - except KeyError as error: - raise KeyError(f"Missing required property '{error.args[0]}' on APISelect.filter_fields") from error - # Combine the current values with the updated values and serialize the result as # JSON. Note: the `separators` kwarg effectively removes extra whitespace from # the serialized JSON string, which is ideal since these will be passed as # attributes to HTML elements and parsed on the client. - self.attrs[key] = json.dumps([*current, *update], separators=(',', ':')) + self.attrs[key] = json.dumps([*current, *params], separators=(',', ':')) + + def _add_dynamic_params(self) -> None: + """ + Convert post-processed dynamic query params to data structure expected by front- + end, serialize the value to JSON, and add it to the widget attributes. + """ + key = 'data-dynamic-params' + if len(self.dynamic_params) > 0: + try: + update = [{'fieldName': f, 'queryParam': q} for (f, q) in self.dynamic_params.items()] + self._serialize_params(key, update) + except IndexError as error: + raise RuntimeError(f"Missing required value for dynamic query param: '{self.dynamic_params}'") from error + + def _add_static_params(self) -> None: + """ + Convert post-processed static query params to data structure expected by front- + end, serialize the value to JSON, and add it to the widget attributes. + """ + key = 'data-static-params' + if len(self.static_params) > 0: + try: + update = [{'queryParam': k, 'queryValue': v} for (k, v) in self.static_params.items()] + self._serialize_params(key, update) + except IndexError as error: + raise RuntimeError(f"Missing required value for static query param: '{self.static_params}'") from error + + def add_query_params(self, query_params: QueryParam) -> None: + """ + Proccess & add a dictionary of URL query parameters to the widget attributes. + """ + # Process query parameters. This populates `self.dynamic_params` and `self.static_params`. + self._process_query_params(query_params) + # Add processed dynamic parameters to widget attributes. + self._add_dynamic_params() + # Add processed static parameters to widget attributes. + self._add_static_params() + + def add_query_param(self, key: str, value: QueryParamValue) -> None: + """ + Process & add a key/value pair of URL query parameters to the widget attributes. + """ + self.add_query_params({key: value}) class APISelectMultiple(APISelect, forms.SelectMultiple):