From 1c262c4804364f125bffe90a7d4c0611bb145366 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:43:28 +0700 Subject: [PATCH] 1. Database Migration (backend/alembic/versions/014_add_custom_registration_data.py)- Adds custom_registration_data JSON column to users table for storing dynamic field responses2. User Model (backend/models.py)- Added custom_registration_data = Column(JSON, default=dict, nullable=False) to User model3. New API Endpoints (backend/server.py)- GET /api/registration/schema - Public endpoint returning form schema- GET /api/admin/registration/schema - Admin view with metadata- PUT /api/admin/registration/schema - Update schema- POST /api/admin/registration/schema/validate - Validate schema structure- POST /api/admin/registration/schema/reset - Reset to default- GET /api/admin/registration/field-types - Get available field types4. Validation Functions- validate_dynamic_registration() - Validates form data against schema- split_registration_data() - Splits data between User columns and custom_registration_data- evaluate_conditional_rules() - Evaluates show/hide rules5. Permissions (backend/seed_permissions_rbac.py)- Added registration.view and registration.manage permissions --- __pycache__/models.cpython-312.pyc | Bin 36353 -> 36509 bytes __pycache__/server.cpython-312.pyc | Bin 361724 -> 388200 bytes add_registration_permissions.py | 141 +++ .../014_add_custom_registration_data.py | 39 + models.py | 4 + seed_permissions_rbac.py | 6 + server.py | 807 ++++++++++++++++-- 7 files changed, 926 insertions(+), 71 deletions(-) create mode 100644 add_registration_permissions.py create mode 100644 alembic/versions/014_add_custom_registration_data.py diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc index 9b570f464fcc36a4ead9a47515330b67ccb28144..1527758b533cdbe49740cef9fad5eddc54706980 100644 GIT binary patch delta 2774 zcmZ`*4Nz276y9?eR$T!V1Q!B%E(RT}UVgO*Fxu zYEENie`G&1C7JZHrlv+rYf`Jper;zevFTrNoUyVQ$GYb}!<8Y1`R3g7``vTzJ#RNY z$)0+OMH~+gHyilh;|1NG@cRZMM$D{r0uC=L*UBbWnYyEu)pDquIxuB#2zeO{)z!?P zgeXz6t;aAhaWA`~U2{d7)?<+8a7+4L;}uH{x75lRGAPMz7|0ye*9rYxdEP+QXcmXi z&6l(P)7g2!wL+OAXUjJPg*%1Tpxo&Od4bFZaz_c)3rimNUl`=?>Re>Y*Qz7W8*lAe zUfkK@Z1%V$RdIWKe%0yscw3|fkJ2P_m9N#?;!}K*&+kIChrB&QKRz`>#@ zEi+{^gAeg_+Z(2%yxr=@`0V0de9|@pj^eaA=izfSrax%RXo8PCm{!hXI+JX9OQ)c5d_`Nv=w9og1sJ(dzoA!sHzjJvYaBfjFI?=*)V z!^!FKIJ`a%rTQ>DwmR%NmSbdp>3PpBh+-E<#kh+UuMi{=G!Qfq9LBYIWt{rHyuztt zYW{-%KFoL%i{53ZA2*QLYGXSqZa9QiPF%lR6 zp31andzMBR;Rx?({#4P(N+NQc;27?|w+A;A$C#E-XgzK$+eu3(;6O3%BK!7gQutNGgFu@D3F-5m;%dv!Z1Z zEhlm*5ZdDPD?W8h;|0rb@;oitm`45t#oABh{lL=nHukSv2yg}iw@Yvlzu)j6_Ep3{ zCq7$Yfj{u=iXym#mn$kKi{#=G{epF?63k~PF@YC1uX>Fq_&zgMTUlvk&?ioIpz1o< z79 zq-1(~xiVmC_R8%|ihh3%;%;}E*+m`%@8T!!&CI2jYi`V7rZ32(zn09qBj8KXTU`p= zosfEnXY2~A^eJkl>TOaS9$9}w4HPy?O+A5$O))@plBcPSWD6pN%}q0B>nA{@uO7U( zTE(Qhi$a>km2IRo!Ex=QrYZ&vc)d3(k{nj4H*=|S|Vs!@jz>Ark~1Oy%59XBS;(~$xT)*eJob7~A$8YXDq1$$}~7 zB+OAuQtzRVcnV%el9ePeCTEk>K{|pzagREib?9wBrRFkz%9i&1>EZmdtk#bP>74n} zZj;74>ST=Th=&f$?RePSMY#zsX`gjifOYA_Le@RL_yIC4qOAD@TWQ%#&_}S1U<}bM zq$7yKZ`NhAEjrQU&RNFvMgL1NWju;0xck1U>qS06V;eCDBiO_tU{X}otNPS;`2sEL zUp4Qb$^=Q;iS;Xh?a)gv=&oe247YDQlvQ>u0TB??sMzsXSouhAf`b^oX`XoxRUo(t zOE(>6d#F#jSk#lB`YKuW6ATiJ>B&CQ5me!eJ&CYS`=I9lV~tGwa4>JRFwdwM=OK<) zzmcEPeszxc!`9iBA-=ZTW{$@+1xtF97m8X$ClKaS{C-c1+u`qQRn!qmMDHbFZg1s3 zKor?g-x+j)hwz2oG|)%AMtlkC@NDlH&@j;VL$~ JYNxj+!oM?;`fC6H delta 2629 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 delta 53026 zcmcG12Y6If+W*`#lio9_B$;%E5_*>o5kw*aDosMj3=q=rPC|)DP!Jm{<*G+qSi!cU zs37X>s;j8G?%F9TIBUVSb_^mGT=oC{-aDB)GYshWeBWQ6yqtT=d)|KPz4Pgj%>Q1O z(ez>-6T0X=>=YJXg#ECHz_jD~)q~3e0l+7vn(U{D%m+zO1LfPV0 z=^5U7)xQxpUn+ zvI(=x=}vKJ?%oS*uGfPQyaojJH#%Fm;sb;o)JGK;6bECV*1$p--VDO99~g2En;2a2 z_&2=L_Ef+%?_K$DMB2lQhDGLzz6{|>@=klA`$-(H+AyAJ9(g^onmJ3-}#xcuFa z^7n$uU)Pt6$&y`u1V{C^i?a2EqPRrY`$3i7K&5!s2LZ}I++3K^-}O=FbLYP##r5tT zu8-XZ0y2zt{U->~n?`S+8036Py_F=oJ`1XFSf7`Wo%VSr!hmQ#BGHs|aed)F0?n{& zqg`JHHGB&VrLPRc$C!Yq5R{!y;~ zBWubmeS%U2PMZX7iemz5-lV!;XmY`nW*OQBL z)1(Ns_c@F6WKwgdB5WHamlD+S1AWwm*>=gLMsAxD-qxxg%`cT~`lv41+0BF(j-wLJ zs34plff^}V-#0NQ%e^g7B_^oK$6O_r#F?EL}E8r1SrCN>Sa z*H@KBNpYSuSiVkh-OxmoV#31+*W4{#Mr&pAQZ zK1ZL~xt-YREOnMnOjHxX>BQB=eS~uIf}9Pw@`G@F0bH2{ow!^dhF~HyE?209L75{= z(uF~IzGMj&b>iv7LXj+z1dD@Oe#I?IerL}tJV4jp5N=5qF!?md&G zuKJvka>=Q;B@EKH^-NCa7KEb%y=ObmfTKI(NUBIq=n>S0qKqS3zpOA_stA<#3M&7G z!cBIjWmmt!fBtRf7-oVI$uxj zm#Ijuv3l|Vng#?WG=#oVpVcpqiYDtbdX(F%A_|2pCPi{xU=p%XnkD<0nG&JJgg*iO}hKqn91sFObF@$-s{$S5?=@LIPGa@lFY52087l zh1J12<@A*_i$6NpP8zr>q>Cg!ziYxe$(26z_plZ{T!McYx*M_uDfpf%M=gx8842|mhM|RbFkB=k%t`Dh~%1(5HzJ5fG?E2a7 zXX`_%q_LE*(_bCgP46)hQvQSfXe=F#O5&~FZ@oCyx}SMn0{_|dr$YTp!6I}60N+dU~+*h$NxM4g~ABZXR6*f$Hz%C+Mp<(A`E^4%b^@viRfa-BWYaE#bNi z?wdk7-x^Xsn`OVv-x)Rhlb%1Qoc_IC-xux7`!3KF&apkDX&yJ-K`XDEb}GDMt)_RpTNPiij}>JOG?L6<$*1K%&rkp4jgl@|mH8K|ER zK}}O~9s&Ou0r|fowtpd{eJ_wEy@&;B7x$^a0`;YkO1<4n^-*23rI%4n?{QvCyX1N$ zq(+~Ova1hT6M?UCn19WEfYM~9uK2bn@*j{+`&z_c-x!PWKeg!_(&hlwri~b?d61zo z!8pX2UN`W$;uoZDl%id4FsgoR_iy^$pKRrD2vz-=xg-AKo|5aWkm3P;U-PlA{X4|h z2Kr6?ZED=^Y9PlL7%(b?f}p)BVrdZNyi0%Y(ce)Q79H=?-v{*fA^m-{6)irdzyHu5 zmcpOV->3BV8T}chpHu1!`umdpzM{Wl^!GLWq4ZM8)j`SQ^!E+@ouI#O>F*@{QSm~x zP9hQp`R!Rr<5ipN;;a{QpMNzcIwO z?24toxUID9F>9p1e*9LMo<>oA0{uJMZ$7)+M`-Isb|rE&97Cxjuqw|QtMBWbEhXz4 zdlx39gzz#BCHBihVEsZfks75yDwa3m%3(Cy#S4gPPqpO+a_FCTgCEF!W1DN=j3%dsn9tFv-0b}Lj^#7z@sttJX-h?HTY*eu^Ki|yx0`4e0!(gx{Q zSXU~Zl$EL{X{FVZuu}5GwfN_)q)uxmLsv;FrD+zYt*zha7Dt1dD0DP8xy8KurHm3s zV=Fz*cRM`p26t_X+ojz=s4KPg1hqd9Xd5|N40u55*?wBhow83^=x)(=P%WoTgL7+l z5%AfT)-;f0d@75?x@1vvOS3O}ex25`$W^n#W1~6_{!g2Y>IGE2&Q;Umu5NNK^E9|y zNVe5WH1`6x=5DHWd)j(I;_X2qulS7?j}cnWg#=bxI&7BYj16tqwe8R8xBH?)NyDyH zk0fV^*iEtfa|i5hJ(N8BTI-RtY%z4x(Ea(t{&LZww6WJlzn`4G#kwPF=Y?;lcG;gd z_fTr}{`iFIBT32EMr-wi)~_hNS+nWi9?M`w#jaE|zXn7>y^_=3)@R}Zho^Q?b3={h zSyZ>gp&9n#cJ-iSjicm#p zv@p06?aze4sgg3<{o2qF>3lJP&OItUL;8mBa|5CbBDLC}udRESshe^?z49}J*3+E; z%iNyZU7WY+y!{0O|6)CqKH}P#x8l;b7~-6HD0SBU_*tejoesUtT{=qfv}nGh>gt-N zrskHKmb&I9Pj$71SfJr-S6c^=32+U-wFG?X{O0Bc?K(VPuU9=hupkY|bO4NM+MfXI z0CxjCpcgh}>Hk`oICL|e2w!bEnw+-gf+I<(YpRaKC9XN|NJ8?O^N++PteF_}n3j7; z%{`KuvC*^1iIm=_wLsSwRZPw+Jfs#L$uByj79A-pJ*1W%F>*@F_p6z=rr%n-CHs(C zex!&Vvv2K>WY-@1)r?!Cx2J7MIHdL<%Pc6_ui7`Z98!z*k#(0?DJMQ%KejM;>igEj zYp*zD%{wkD)|3;9#g^ZJ)bUu0EopyJ*F#q4aXH#3>DGbN@pLXJK4dL9E?bR~QckhK zYAf{v*Nzzwe}41Ax+Z_z(zZY(5K|R-?;bon46qk~<@hL4kLY{q(@Ot>B)|*w=ZPn9 zwdF`0q?eeocCyjOU-SbF+4{AMUG^|!kD&3Rx>TPX`#6#SujnQ94*kRW9DQ#6z&wyr z<+Mho0t7goMniyu^yi_~i$TEJ3yi*>roN}^gB$XO-iC^p_W0r{oK!EV@pzUsYp$xD zQQ_DPvQV83<~j5e3p3UL-q3e7l;}GebM&9+d#JUqzrwBlosws{yVAxC&qmz*@cLvW-G|LQa=%&|mXp=`{}*>7%qKY|QQ*y5t!n zCFtWky=R9JXNrSyv!|^F#_$f1$8O1-yoSg900#h|9_^4`_;40?pK0@{4K!Nn^Byj? z`{Em$UG9eJ+UBMObqfu@e2aznGFkx~-8-wLL!wo!V`cVU*U}={9+A-ier8XZuDzij zU%p(rLtnh&JZYf*=!){;VcfCkm?x7@NHVe@34dA~v&XT81u6OTOCK&uX3lAZ_Q#d? znSSU`14!R{d)sPBdS|a=S|YWEycu-Q74R-{~+%ZfKLH713acrdL%D0!5^URxbMoHA z8)iw;F}-g6Na@(#o$HTFv_}5pjTN@-Ky`=y+KrXcRy|`wxAri9O-Feux-!L*fk)O@ zCQ@v7S)2mkaF>G=t2Y;^E&zD|cHna$6~<07@{%RRV%elWyWt($*x9~uN2Yck>chr; zHVXQ!4Q{qLy@$J}zHU=NIJL02FvLFFSYwzdqYfUVzY`GETFa4~{OghoQFW!Y25n^W}OD`WI~^&PpU91`n1)t(i!ifOFYnjsI}b<^VZax@JSDHA)03Z6xw z6M%i6`E|zq2EVLF*6jeaWPOLR&)oM#sTJ@q`qZ1J%Y7xinr6rQZvHxTEA$rIShKvk z!QHg5Ws!DFf995f#o(o?EqN+Y?{K+2wOZX0Ow+X#V1lmOiu89j7wPAS_;GmDAgt5l zNIHsI08C8<#AFzsA!U~B5FiKmhrUzPNkjCsTUSmRip=2zcFG!v)y_di0EsguTt?g{ zmxqU8?qn321H7bvf9n8wRHr2j(c8BajOd9{W)gH&X}$2g7~Qg3Sej#z2k;say#csz ztbyv=EgkJ!s71wRh-f@NHC8BrDV;G9xr|DywAH9{CBRhx#=H(GOtwxRUd+ilv$+j5 zS+uK>VulG>>S@@r_1&AYV#kA4fH!sV$IbG1BZ7_Hx_lrA)(nb5$#lks%hx2~5&ihq zG^w4%IsvEw{-YOdtCS|}t=skwsUYHv4qdGUc#Y;>T{>q?`t7%m$U92eRffFppk&z0 z>D28#=Swt@1Rimh?#{j?VouDh!#u_dlHCBv$JRMgz$P4b4WDif9HWwfUose zxBn=gXDI#s9skM+$WTN0GKesG)t|d#)rDtt|7nEG4z|MFU+adh!i;Y+@&KY`ee<0Q z<;h0>X?GoIf0lZ!&?-^kk05}JBUGDbwl`;DxFk)FHBD_b@OD~Rn30Ksg-S;3Md%(N zOTYf^)$&D#iCw!mN9^99M(IP=B+vUuaVqThRM|svtv1z?*7gNe*nmdR+LdjHeR$9c-GC3XPxhsW~Xywkk=+ErR zuu)zfm8T@i=}6~O+VKw~T|nvg-&WE=fJ4-H4N?mAqz9R%BE9;-Y^hkkonQ&Uj5w{h zc%UxXwI#(*Xbfa{I zx>8zPEebD_(#5E3HBONh*BCqd`*+7nwSj_Ixy3CuO_vKKMbF!l8@C|PL7gbP0o4MZsQ% zDyspm1h@*o2&a0@_9A`6!W{zNkjG z$5XS=t*7ptQZPW%P+SP$0bo~Oij<9jet2)bGlI9Bof_#+`uayMRjk?!Q8<9C@z+PJ z`UQ_>iveSmq%P5#f9F|iCRgZ5(ZN$n^R7=VKdiQhU;ifOh?h6Y<@z~~<+Kluw;fHi zZ)m)}vBOH~Z*7*$uJ=YA?mGBD*TJu64C#oX%#L_VdhX%0(t~NG9SL|&w4~)6PAxu| zTHKL@=VVJ#2BS#9Q>rB;OC)Vh>PW*=x`oisxqeQE9ZwmZH8b&)Woh?U&BkMnB{TnU zM)!jm-8*ve+{KcSci8ScXm@tx;kn4trTB2}fP=XMI*RdJV#zH!oYU`MPQQ*)JeQfz z<#_IDDX%zOHtAs5qz)&ZD=dXw4?9L4bd2oih3Ed3!uHOL0mv9=DRLez9DT5GbjKh( z54M!{I9zh>!IE=3hTwUqiC`F>X(EtUa=6Q&gIxx7jFyl?@w1@paQ?7^`NKNSWd@0Q@Sm+S50fnqFzhgTHS&bJ>88pbq(6Hd%HYwky=EQS=w?YHu1i)gKN~)@^-GH zrMceSq|MRwr^nG{#v@N(Trr<2YXO&GSJP&~gJuOBiigcsJYKKQ**7+!9!-d1>~cS= z@7mWtZW%pX0cThy=`X+9WpC6oHYFKkXqN%Z0jSpV{$5I*j{W<%k#J~sQO$)SHvkjD zs3!(`4iMs0wB@MOjG`v}`M>wDw$bzbd%yqt`KmNWANRskW1Kg~Ri@eqANuYW@&-JC zrcVMi5^(06Id+UdC5{{Obos?@b{=t;A(QDg2-QcwI4JWfqJj2I$vFI^0k!sozTw3@ zE0(;^?%nfZokC3KzB)?UqR)A?EIF+1v-;*&ds%O!x}WZS{?+#+x&V96{*q`&tg2G0 z)er73vtt5bNNYG!&jHNS6AtuEK>r$KM)H}VpMRjcbu&GDw|C`%JAkR+P;nG60nx-m z-MasiTcQoZqh?|Y;MqxdP`+IM)1h+f&6JGV`{JQH6rMT%uAmOYL88aws{jW8jIBwe zFn#LOL04pO{EkN7ENMpJ`-Xy0q)R8ywk;c4<*O#t$rt8k&f}c-NK+R#*EKn#NTI7f$SB!`da1^KZ7Ur;RX1yObb{c~_5k~%0FQ~wswBJq$_J^| zee^ta??)e8BMpJDe2K<(Z|4b+_VnW^dk?Z$?Y|XtjY zD>ZEI6pJh}8}UrOoSw`Zsca7=$oA;7PUf`VL`h#Xohr~l<@^;srKX|5une{yc6(Mk zZ;R__cQe2(00M1e>O9R0nzcrnXz*6@Z_z^L8m0Xc%s}`0iIcOe2Pko1@6_*}l%?ps zNk7)Axg6-20@F4@OO?h97yL#i`l~>x9F)ch4`G)(ihfZpNaHq)p}sI&enmiyzLJ5eN?y0L<9>zwe6<2g*|! zIZFf{Pn#3V#otw_yY(<})!qB8Dmkn)2e!vbJ(I&}Mf=}n&0<@u)Xn-9VLadaa;)@P zl?b4iN1~^INZ7kbjtsWn_tD@3fDZ{&wH1iVx=0yuk6UwS zwW6VmG$HpJDm|4ljxH@=3(B#8W*~0YP=_;<_j4E05}e?hq3#r zD1~AAY&A=k(2zq%G+C6z9`F?!@P^%Sq`na^6ielC2B~PbT>Mllb(c124)eP@XrP4oSQs*;WB|Dkm;jUC%E9N<+3DP%Wr&F47X*xs| z0}x3Uk<`G%1RwVGDNStZMZDFDzx9$9#lWiCk}hiUh=-g~k9Lj8#WsvDo(?YEbVbFz)J^AI|2G`i z3{?ej0P5(SOuFnqgm}bTy``SkF7({r8`npgWKA|T#o9kE77Uas+W!X5x+nUp z=+w2YskTL9>p*p%%A2-qC;=PsS^4D0m?p8QM5F%*c0q$yBdvX_TeXH@Kk(T_+92}K zI5eXd&@EcN=s!prZY`tqCElfjB&V7gDtl_q=2Gi%_A29a9v(>SSz4aTXR5Ix6}%FiCfzeCOM z0XXb&1m`Ho(v9SK90kqTXF8oJ@c>2ITIpg!ji^W)ge-Cn^IRd!n9U&hUy$=FKpGwp9>tQmlO;zoO9_J+Eihq=V#@c5UEDcY zDl~*+6)#SPaL@}I2(u3y!o=E7%$O?m zu@0lgao!uJN-wIvF^=$HVF7A(rob_+GdJR}5(pO&M-(1M$`DMH6>g7K1fCkrVTPwZ z96kFcvr zL6OlO=EcLQ$eAr16O@v6w$K3K+6Z*OgfrbtawKZa@zo?bRF)LS!UGO)vxG+?g(2J{ zKFU{9MEhJR&N_xN&-ZSaD@{-fLkXtEAERQDnATeH!+dGnd@~nP*SIK>1M|=sn9Oxj zHUnxm?G@qKm*8H`L&HKtpb6q=zM7HEmjGIt{o@-zP=U3WV?jw!!rnsbBTY)+J zut-b^F^%Pp;^#C5RvDsjq!vc!P9# zJ4=m+lt_a{Bykl%m@Hc!^TG_(7D3X!nwanp(FOEk-s< zS!Drv`>c?vN1K7VGXWN$X#!I5;)+ITqP3RNo4qeJN_WS`@eGW&BQnJ0%cTKQme{jg z>Rkl4t@5QWsaerTa}TL=metP}>!$woSa( zCY4LeMAYR{^N_QvsvK%b1D{$bdf1i{>bu%ei#Xn0 z&Jx$Gk)~B>XessdN!3+O$rn}UscC3xZ6qT$1X>#=qUg0!H>q6oTPqEWLsRW~P#G^; z)=Dd+C&jU~QjYYKh`CxCBh`!Zua=5>@oW#C=(Ewab2@|9=AcOua@0DyJ52)zTXr*8 zjThUlmd0AQQk4Yn$5%@~sL~(AyVpyjQ#PX2CIUWn{PdcI+Gf%F2FX6#R1jM{M;#8C z+;0(ZvfoGYDh`i4zvPMFP3YlffLj3S#r7Mdu7kHw+Na{2CM539Llt%n9sxL1#Vl}> znd26@cAG5rOrXKuzFtbV-br=)c`MdSeZbAUjndfm4QRfZfHO7Xuy#h8Ibtx);p9Ra z+kqCM)j&2$*0huWnM9jK6m62qtanqL+1|;Ur2CcBdr&{xnCKdDQ^ejZ^|A46n)Ud+cUuymb$yuXyYh=_`5ey~svc zTg(b~jpi6Pe%e@Pg0>6UJ;06z@Bkhk1OO#IYeUU^cZ23Y2B`9>E$-zl#ysF8>N8Iy z12Jg}P4K-`Z=3fITcm%=?Mwy}9Er>%qvM`|j+tj}b4JD^;L#w55FG#`1z@*0v{mY5 zeUb>;^XI5-l0x&w=9S63 zt41#%v~aw1rWh<#1X$pu*0T^5hbkJo{HrEYcl%0F{0zVuHK>v_U)WBw-oI0|SG=|z zQla8{3Qe9SaC)~aIYxO#$?ZE+hfFP^KV&FwxQlf7FRJ^6_m#V(=cQg)<$PwbOtBct zpD!S6S-X?|@ulD{kf)_t!vb*riWZvs-7TKIhfMxuLJ%Xqx<{(=PCg+`l?!?4gto@g zX*g0tflBo+M#R38Qt@oI#BKp;vJBbAZa{umTUZE=|q;Q>_7OAg@G-ay0Wg!EtX`+rlq zPqcUNf2D=0H0$Q=a{5qMCtc@agW>gtxwXn?3OvqT1{u8qz~lDSNL_#PF}a|92a;@e zY!qA^$tWVB=824XD#7MypeCdgR=8^(K$ty53tt?aTC}u!s@*i6`K+yCy(D+D9VND= zMSGu=uS;Ww(@3&NiFv?7EA7L!hA*w~Y0D#U;Ei~~{qU4GK=8tnD z@+YQ5$yt3^WZa&)G1?Q7PHs#+aqN3n7F(j^LAL)81IwemZ$` zK7jV1qf?2UEaBoFG4uk^X87cZ<9ycfIQ%>nTZwnT991+_or*ahZ&)GevrburH?5vZ zmGf$SQKMb-%BhFmiyB+kvchMbM&fk)qGr`CsikZ2rxM1|qoa9&V+>xDIu$?J-8i2% zJg3nnj=L>&3LO|V(WBWl$3QUx^!mG(#D>pE^Vg}X>xNM5BE2M zrOhHGT`m~&A<}Fj=|DmJgxz;h+Q zRRG4d3rhK-Y1*=&rH)2*+^foKXT#&^1y(UW)#G;8M_jPa1Rma;tp@RdaoHD5=SXzS z)as_oabaE)I5jbjZ~eNb)=eNK!Q-yU)@m13(+L!f0@{0o*Jo>PZCTRVqAB7JcKJM8 zJl(PQQ?&OTyF4no3RAyRi5FojnqE_KEI=eOHWbh3#;ZYf1UcYbhS(nrg2_k&j_$Ju zy$8n!fLh&rdO3{6^cs+{tM?cC^5v4jEdEF?ex`1Xv531K%CZWJ1xdv2X?+pb6u^0I zXha3!DUi!;$y7HZ#=E^hek{(|NJ~U(IE@k3culeZ$~p$ZjnxfG7b4G`&DY^E3k8dj zst0HQ0IOBnnWpo;^v0T|)|v)-&1Xd}uZ~D5G&n6m(?RI*GNd%DbI$J)Sg>d$tPw;oTUUXIE0DK_QvhoLu9n3GZ_*0(p+;IRY87&-ErTrMoEY!* z6>{GgV+Cc}$}h5bWsp41IH-yiKVKo2h+%`}*xzwiG*92{u}t@Yht`V50(eWQl+==F!>&~_xqqP|ufCFuoqnzc)rd@<>fr&V%-%}KfOvEHAmisgJULU&D>3&Z!kn00m$xWV#m_V4zA}lW&n&sE zQ!IOC$s_H#AdEfTj13vWI$Q46-V;UH;G+vb4gkNy!7aK8Q~Wwx&X*Qo ziku?`&yjQEh7%t{W65qUiuih{I?j8?9Qpomzxxbt^HjldT=V5#w*KI~R&1Fs&zR@} zp?s%nI69h##}|Q(^R-O=x??7z@IkDo`DtWy3K&^atjIp6_ zn6|jGQSLFc4&6X_K5LDuv98G%Wh^P&bl0bKiCg1L4Q|#JA3T}VD#a&_^0QF({Y`R- z)KBbhlC#nfRcK0JPAO>r>!&8U4~3SV&9ZY04+T77SPowL0FVa!uR!orsy992<-BTR z{T{APaci?&HuNkCC1hrt!WcrA{>_NFT>Q{XuL_(iMl7K@^fVIEzF6<&OXMr9r+Y^H zQn@U;nlP}!hAx%65*xKk<+0;9IGPIMFj-F(wTa+`H59P1&VU`}s!iharE-PsBBH<% z=gnV6^Rs)R>*m@k*zXdQaCVYp-a1|l%2Y`SK43Tg!yBY=TWr>Dbe8?CaCY@8rMn0>6}&&)InhmGOv?8g}FM`E1$i(~fvTps^a%>}B&jg_M}%GnnjCBo^va|`I_sA9SY zFMI;?cVh*z8uhLOxC&qmfboJ0QY5Vj8{`p&wAih-h)o;h9BGT#wL#8m=fznik!ReK z+=ez`{f$GZaVZT%^U$yw^&)i|$+kGmhi$U>Wld;}=_W^@351nm;|z2YBxkX3qdX*T z4w3N?%;lsc9vGnJ=?^{H-FsrA{8)5ckVM5JTjYT@Q>J}}BOrW+^~X*LW5x;(ZNsx{ zp9K#rOkSOYam@?-DPrzza*^#i5Olv-cN@J$pox2KlP^IW{q;7v{{UE_FQJYuI2xal zsIGGv*m+xEA<^tCH}*ga(H(E*Z4rz9DEAH$y&0Lph+wHi5@`;Qg^9E)^fVvf2GGk} zTkOItYjaY<>)I(2p)mG2<4+fOpRIC!cgTnKCj+c8ZeRQYy2;ssyQFkKLj!|!hs3yV zrEirpY>kxjS)BKwt@30=UZi+`xm_Nv(l+Ticaa~k!Os$N?vi_Sn~SE^0OSA`tp+JW z(4f8+k38pfDdOR~WG7whd7Dr`@ITxo4>x8jQ^c^lDHztFGS61dC@WS6dwC==u_>@o zmVUvH3H4vLK+c4n10Rv`nvD5TJ9+PNpPP3@b1NJ!p z=3*(J9J&#_d5fOW# z$r-G`Y?dMqv2{96|LG)xwfesaqgMd$Dgtd*-i>MN=MX)gkS|KOlJY288FMGG;R!jX zoyVjnK(=`Wzt8`*^t5GQl5M#VT``Blk!s%t7^Xs2E6=$(kaMj6z0n*I#uwfBx{`Jw zJu<(YUzegoGkV3p0ofy|>vN1)J`+@MltDl@7D05;8@2o@rPOjT(TKd+NIgNXkmalC z;`=AbORk`t9r51ezsW1oZ9l_^W{C|4Xc|99JbXazll(cdz5@6X;0tl$04<8+M9x9E zy!ax@q61MYUwzlO>QRvKMWFsy!gWv{CVwi69S3Ds>DMUe05}ft4Ztyg697K~d@GA? zhve~wpCFQcmP{z8^H@kb^Zy$g8%EgLl&W5%X{e@DtfQ`RNwY?$?hc+Ywk7d(RF8wg zZlgGMNY2Rrj8+MlwEKr1u3o*kc|L}os}xc6x?CFXv+?a%k8^?+C#JkE7f#|x#?yJ3 zr0a_vS6ACI)m=k3%QXuWfEVk1(RCi|1G-y$3IH8fxEoy9Tk>c;*Y`n?)#Az5=~N(e z^zp@Dx1QD?v?jY3wVr@#`M7fj9$|oV?$^AG`ZMPoY@57OT8K)<=!g_YlQ2VWL&0i* zD+xsTr#qtN4LNVJIpN_QEOS6*zrd8mmq^$D=_6krvRSQpnMb<;T(1ZC13=IURJ`{|D!g`@~l54qk z4Un?UgmUPM# z0q`UM7sY50hA$eg&Q#aCSDbE9;>VM67kP~$GQX4aih4uIHtl*KVB-tLM}3HkzmrR8 zKDY8axxmhnz7dt#@!49N8tR(r={6~0Lhuf}E`SV-NWO&0qi0lCPCS2n^|Xnz#%q57<9M413UMaDI-SFwMGdyPr;y?R zdl3;%dy8ygFCq73fKXc_t%|~5<^Hz)RJI~f%>GsWD$Wk8&)g=tDlu%c?2b)2X51*US=Y=1R0!r-;ER%E$@ktm`N= z`8&RWfbqbW=xJTJkiK`xftv0>CyA$1lx*o0@m7kGFJG^SA5)atuB$;Hc0}6j6WO2q zuVy%ES{y{5qsmxJTrbw9D)Z9bM{TxDYiAr3-=``SQ5=_klthm-WpcYI=9|F>Pq9pa z^H5@rEVrNvk2X9sa@Z&YpDYNTVezbs=TO!K4XyOPILBeO!69I%{-B7b(v=+fZbkUgm68;5Dz^^(exQgX zyV8?xO%1mzV{Knk<9ial>+H%WiFA6=RH9VGv&H+(PTX6fJOkaj`=mU^>iYBR1wdVDsyOclwYRg8`s&t5o5}fG3}X1G(;=BWzWR3b|!(1 zj{*Jz!10u;VlGRyBMCSN7fp9}!GQ-tq~;We)H&Rutw1KLpLNWd4XnFqh+=tnpxZD} zzpsc<%a=^49mBf7jO%y$L z#?LTya8QUrbp_x@!DllC{gq6cmALIl@}ARQN%`IP;oFkN<7FO)|Ep+Z#fU_#yPf`9 zIbVS+KDquHsSbeS0N()cbw>-d%m?xkrJQM65igj3M@wFznzqWzHI6^LMq_*Z72R+M z3v@$U3Nz59DVunrI{2%uV$u*L#THExDNOdxAEHD_bb7gXsFJPZuv;gCOZIXsRoZe` z3PP%-Nz2~hisRDXL3%dm3YwaOpwP1fn!4~M)Noow?-5F2Y%CF1NozHaT_pD*>UNA! zzE;An)mCZ4#f~wG^UU}6P3im&l1%~nDGL=K-`WqF`OPK{eMxvdSxg+O#K*-G#S204 zcYGs$yztR2e(zUfm2Z@D!lcSmVgKO`Z|&O2T79wn4ukQj1Qqg%0hl0iCn(N^EGk}= z6+*JyipMb4y}2A=qu|y&>o8D2oxDF2ZHPRKv3V$u14t0}Pf&W;l8BXc$=-J+D1}ks zv{N-pB~25VAST14m5m%)uHvXh2W~^BD45;4rYQZ}CH73bH|HerhxsI#bfeW1ZGXm znRa>B(BbT23-ae!;9!M;CruK~jr852+)I>{*i0hxb#P!2OJ+!!-pQ9JW$4HyQB=c|?H<9HKg2PnEj zteUUnh&}U^Qn@rm9GRzFX{;nhis~Ar#I~I)(PtWS#GN&YQ|_K3o}RB1h_7mtTBBlD zaq)bm$X^lIM#_D-(mH9NsH{~g?7%@Ml6L~tT>v`?i1oF~G zn7;Cm2auBD{kuz9DM@)^^a5qaNM95!-e_N{$(SA8N+g_aAIu8J0;b*M=T?!oP|1oL z1ndAKQ^cr+N`GmPXkDnRm4=H`3zdo>b3K=`mM&8IWNrtS5$yG;MM@9(+!XQUBBk58 zdr;|NfV}`rg=w3QBJYd_Ius-Uj7t%h)+s}!q2ks$<<~)B_8ExkpYb?`(Ei!K2>7CC zuWvE#rtc@dDayXOSb4I*Cu?+_ls*_yyGVoO(KNs`h%u_Tw_cedUz8$F)hk27zwFG^ zb9birc-6JjKpBDp|Q>!FV6*^9C`UR+a!AQ|5}0Aaj}}7LazhSSHf_=Lpz_! z+$rAIHD!>bf)w%oU@5~}+M?Vp=VC%|Dsc)`55FXOo_KGm;+(;>pGG!y&I&ps!7K}= z+G^YjG)XrI7W0-V#c>x9ZEGm-dXCYX)ti)w-owk3i8fl_6|Pdc(PsVmtCUM7oUs#D z>hFyLbSn16Dkb~u3jf_P1p+tLP9~@MK?cEkXTo_Sns=s{(srIZMT%NyGQ#k1cJ_HV ztA)pVlQ936mskY@dAlmQ^UX@W3>iL|j5F35)%;S~bmDYJig)m8<^GtVFc+-~yc@es zc)SpgQvvw=fsPxDnT-R@X{mA^6*_xPRCHsY*Ys3vZT`J)3p5YB+LUlrBy3ZHz^Y?4Wo@Moe4ZUvtfL~0BL!{ z$bxtf-c3qR+f1VMoK)}Uo0LnVsE9fo9}U0+Nsz1QQ$PL**yI}c>k0L&-gOxKQ}>H`4W5sR=$?6^Zo zo6F`=i@JQWI~SGs(&k>It^`;Fz_BO{aqt`Mj6Fb-=Eo>f?^K4_+=O1FdZ*o~9FlAU zNY}Nh@okDxcPpcn5=4em8Po6)F#pGv!v_=!z<1xP6qT@IP5Tbh5|0_#V)BDD^{l>I zwu+>kinZZZc;VSiKa!ubshIt-S#W<44mk$c2;ZYvAXe{Gy4n^IyN{=OAKa-7RgLBT zLdciH9&z&H1GJv36~zxKRSQ{6Y`ABnkgdD~_QAT2rkA+*jXonpg?eo#U8Z62TM7RA z-d{YZ*li6&%F$G>?IGo=Xz2xUomVN5KbOTrUL{XHCW}|SN@M#;DEvDB4dx}4@K>n$ zyB}@VlHL`)d;4tD@KUZ@J4%H4?Ea~av9n)I_bccSxmx=P78-7+hI$>S27qbCzm||}? z6-D2+J#9}fG04Xwy_EARaI>SETYOg_&kSa9k|O$4O1a|ozbY9vjr8?%s<-2>%G+@? zxq0OuN*;f3kY3yp$Nxb)&o7`g$6ZX_>B~or_>lvx879Z;#4k|+3t=YB7wc+nqV0Ts zX@k`pXJqiEO_lL_N*@;H=$LQ1JvUvD;B|n!Pg^A$V3->k{TKkZuPR}c4!}&H2 zW88u4l^}Z+7>j-0%c4d>*hDfqf;!CcSEVHPeV7t`%lG@ z#yYtIhS^5IlR*85XZ}hrNc{XyCCPRb5m=e#P5qbhLagl&YF#AWKd4lsg-V!qbLXp~ z=#X-q&4{*Q&mkqxI2Yj2*ObYA@X!vvIok4y%Ll}!X_@zy0)nG0eABHt+9J4X*8yA) zupR*0mA>SbMf7HGBV88|lV4Y&qi#gW2Gw|1rR8&$VX!q`Tah5Wu!R(b_GE`;XKW@3}yS2EjIp=v-}+K&H;ok^jgf|bJ_$wtSK zgT0-H0ah3rBAXvu;{u5`#QF7kV{hMn7OR8EFqQT*8i*o1Hm?R3R|1%3X@&|IsKzeC z9u!AyJ$6Fna5%)U3^hx%eL&NNE!6xUY2M8rC|gwfSLo>&0A7DL-mSmSJL(g~t&D#i zIe0nP`1sR{NPP_Q0_vx^Y#4(30$92uWs$_^pDW#?v9_kmdb>p47fONjof!Uw(k1)@ zPRt-rB;G?Ok<9%E6x0Kl>g2e>OBWufp$$5L2qWu$5W}?b-5GWjvH1jf@fTkx@wpO; zPXX_*02Vwl{Xa<}^-E=>?GBPzar&Px`BJf6uodE`QP?VQV!AGJ_fmX9o`U(#5{-$&T+O9NW^pN4{6yPzJK7-vIvG zfj?lYw70?ESYXCXo>{JBoFD$AbZg&6i0A`@tb(49uwMm~nu^CW%Kv|4v61XF6~txw z#eSNs4=8Gu<2S|p@Bb>JY!4C(kEMGHe^zd=#_fV|4FeTkS#>yA&}WiR6`A1ovf9%y z!C?OXk4!L<{MqKxw1+^cSspF{Xi~hPsOhB-pyF?eB2iU`*dC^;U#EK~t7^1j|7*bF z_5g$5;uc4#J#B`%Mbr06UQw{wF-@k~u|OVB#oJM8ciSU`q=#K3M61jD@1%5FLNDG5 z>otExL(MYJ^tJ>eQDN+JX#3FpGXT#LsEy%EE#8--)%^075O6t5%v3f~F2^ zye2NwjDgh%RAkFv#;Xp)mUDtW)bantmeV0jGoYBJ-EPL<^l-9G?K0VBAm-7oBH(!J zbHAl)zwA8e)QpC=&i}+aCzK?Sov0?+yhP)8ySE}yy~Y;z7zp;e#J+Sjqqpf25mZnW zt^;xrn_Vq7T*RdG|13(gJWVZ|l4aWo)wk2G{RP~Z)i2>%|0D4-)4wHS(I-RgV|#)+ zyT2ozVy7 zMWDMkQf#Zdd%=^(<&;OCPN1);EWv$oUSKfVLfO*S-`C6NhG%))KX)DIm8L)@M-Pa{ z@>Qo)gzw3f*q$SvKDT>g3)J`My#P=E3bm&pxpA1+;leu|!Et&#()s^$;Cr)*b?KP^Kkm3DPH+5;u1u&jU+LMT+R11~uRJA(Dix)%z zc~LI&1jT9p0Pma{ zknM;~A=LJ0`85M*rRDg|k}PS07&<^*L_hp#u{30O?;W5Dyx}rxsG4pp&xzdGL)D(6 zOni}~U>X^HtOz8Tmc}x8OBMebs&=uxL*4Ao@P0m2ebIl<>1_yRj(B>c>X50g_eZM5 z?MyLtlYFTYrca+z-M@Fg>dFhIj+r=a-1sWPtN2it?K{%!o1M%Q2+Jo-D$E7Q#+ec* zW~ngQ@f_wm>E19(b=Zy)l`my@A04G`3Z?tP@ha}oJvd$+H!?uB_8yojWH|t^><%H1 zDG7|>%nI3+Au-x4QHRBSNY#GGpyg1uBsNsYDIZkD#tYT%@qB#AJXc7paItEr8r#ld zIlHwptLznQ_cI~f6#(Xe5MKgi>K~y3I-(9!uBqEMppYiCeiNyv!+=Lrk%aGNZ=R}V z*}kBh(V5=eQ`PI0c>#%NEH5^lIAiaRi8-Sv*{RLe+>kyb2B9-CTao%i6}Mfimf60d zE|+C`pTAgrCumfm^JB&ms%p|QlftnYSm*;}VM7X2#+BFx83_+59E(}qywSzPGxng9 z*kO>(F-S4^n~T`8q2E^GpNaJF`KvF^crpx)JK8EPovrq@eM8*bnCZQFw)&Zx!rL>S zA`tUn{IM!F%u@%qgK+xsdHNCz4KVC<%u1>e_&!8^K04%gu6P~8Oqr${K{AG1Oj~Cj z88;ZEfsoUzfkzUj-RC|@NsdHyMHg><-LZsBx1rxO@i8X1PdSq;``J ztKytRYIz(CRs%Se>1|r1eySur!rH>;o9Hu1^pdx`LB$(j*EXo-#@nAU;?V~6rn0rb z#vT)V#Qrnz{Q?jNJ*>m!rZSt=N?-EL^8T?=9UUW$74IxnTasC&9H02KZAi<9m#f9}DT?G3>dXP=@x~BzI1~W8h`#7XcS{YhkpqQS5?fZN zgB_1jUHVp9Yc1Yt!rQp?ySsb{#c$^3SjF)bsy8YZ80%H~psHFnhReIXI`!jN!bGTg2;Ue-U6ok^EH>N_I7B3K8$6k7kFTx zzGSjZdhg3cFE-(`ZuBnM8fe7iw*kH7p*M6HxFAKp_e`H#scF!PkxM^igVPRv1BE${ zB21V$8K1CErk=#f%gHnJQdVjXtnW zJ@-6pqSAQ565(xPmibE95AXHy0Z|&Ep01@6)G`3R0rpI!*oLx^iWVcTQAgTikxm8R z+c-%`QE0wKol+h^PyKa4KC=#i`& z?G;r-RojZjiwmf#*mA8}-X74QFL?od9j<0cU4_R@ADAR#)cOE%Ujlg1iaur256}Gp z20@?9`(UKZ-Enqe*0y;*Fa%ZVK(6WC4Uh!sshqwP0E8U&N|7o9fOfQUq+m3(FDn*Y zr&hSTB8M-?JCWl1@@zv1h7ECi<50&Y5}-eb@+HtJ3`FZez?WRR$X#1c@q#|pdG+-IeR zHmBcx%K~ENfGJ}RxSyad&O;Xf=Vglpo78^tdEA9gzd$VxZc@|7ZzCjCniYny82HSK zlvB_Y-~!^g^M|MtQ`Sy4>YxN4ZE9)Nn!LrE)en{2vpAY4yhZJkw;J8>!3X@5W)p0q z9HPi`nr~6_+82}W(oI{o4YtXEYXn&*PSkwSV_WGXp7io6y@2VaC__KJ;ftah*>q`8 zGnOi_QD2(>dpc9ym$lOCNyfVgJowB6zbgP(YHa9srpWXf_N02@5USm7+G}*JziS$x z&J$`s+e{M1v~2Ghp?;yZ8+IBnCDV$aQf&;tSOC@@xg?OyLYNR$}r(OmEHLPCI1)E9W*2_YIZs3 z1uKsCGsO2h)k>O1jJQwTHVW#f)W9RHQ(5fiQab1}gB%XRgvmObje286?)_@6yjT^3 z?^k<9R6SSbLmp>Dv*viqk(l*=5zsOX9AYoQBa4cSlq2*J=#o{z;^XsWo~Jt5c$hGr zE^r_S*pkLuY5d8>Y>}`_9Z+qK1Oe*_G9D1#xp)uNjS;owC5=3a`{I@~($|m;XVj)) z@MW9H!fR0#w&jH8FWKHVcByyTj463Nc!DmbYlp;jd)0h-j8*L1s}@3zFYi@LGkF#Z zE1?C3`PSDC5&ejoH-Q;4nPje5$n`KF)2tvcTI;SrZvp!0R&irB-npZ3z3~yXm#v+O zQ*yj_Jfga+rMzMYCJz`d2Lc$Ie-0tKKd#o<5U6PH^R~y;{A6~5a5{|-H=1;^PQyYP zI|=>7NJ|af+chaS2iFx)HETY=A#HkxvSe7*DiD|jdNHKpz1E^yt6i*kLd~>YO*sQ{ zyth1|o?~lgCrRpb61;X2a1EYLCsJ^oUbQpcSO7*c3?35C6wKImon3;}1|HaOVWi@x zuhpC&+5FC^7G#%Z_mP4c{oVqE5Ky@G73MsRpkWRKb14D;k8bdnp5*7$9NTr2xiH6D z^c-zM88&(i>TY4>{gZwv9HylMKe(|f^TRp6qEz7 zJWLna0)@^8&eOi|#olOY-Hq1l6ANf@wT^x?dj3J$f!;vbPvm&lA5^c68Nz=5dl;?* zvc&)#C1!}pZ>zmi8jvxT?F^|VtGMxPb?C*-c!1yf2N?ec56?yR*)_|~&Y@#0D)Tlj zUti5|ctqj-s=Y6VpCwe-xl>~z#Pjx#|+3<$prKm+gq zv{-45skI_`4)kY^KdjCG3-lc|cH%Gt4pS`abSaSWyUWXv3LbryysM6~ZKB*ybG(ne ztLo8<&Zy=|REu6+_Wvqz=ft6f-h7>ID6op6E|bK{Gh?5rqjBiouPCwS6SY+8D-M66 zj?d)EtdqRpfFWS`?JS1#(R4CE6~Mg^4Yy*svSQ3Jk0s2VnbPCgLzr)HaNr@(?;v5q zWCszi(--e-+*g*Muip)`-9!{j%=PyDOg#^;1YGg8nwiI8Fc1W$8v~+Y*W}%MzE%sn zusN;-^;Zy}OD171UOp|11Gez60h^70N%jjnY@B0~d7R@QmSw!)yQ+iE$OQGeHrIPk zhkC1R7_0df5EGH|prIUft^~M>KqX?;itp9DLSt-4^jt&vG?=mFg|ieC%GoL+dDU7V zH#KwCft^=FowLv|)=kql`H#y2h3p0V270!*@&`4~wuQQUFxPw659+R1N%LN)SmRaN zzM30l9ZLIfTcWHETVI5#{^IE<>qI(Ah>5mNF?S%eQJC16D_0QAKvybe=syw?iMzW7BgjSci(kFh2II$x~2G1mA_FI~UnPwHlhAET}5 zwjEUZL9RC~#+sHv##mlpEhQEv7g&2r?*G@>wTD+#UHP1wteZD^k;e^r+(39H5JDil zA4wnxh9m^SOCW|kARi&oolqfDy=q(d_{zrsj&5406%|KN{G!!4GYVBZyirKFYRPS} zaYjU~b}lK>4%7K&)^DAg+$7L`^UXiMv(Gtuuf5LN>#V)^+AsZg>EtYQu?ToWc3BX` z-WTS43V{1ZR+Ft?WyMKW3=m2XRHRYQ}G8cx?|{S0(VJwY3q zYqzTt&=j#NQeb5e&tZV5rj(@AuRaym6Gu==p6J}YlzKW+%DxT4q3|G`kR!@~eSJacJo9&plX5Z^da8Dv9Y18&Z<$Ng)VF9xqnDb6!#S*e;aZTkJ)y7gDCEI2xS0917wQ7 zl!3TY$D@q=i^s(6`Wi^B!*3!cI1|a*03!h%G05aHg3zk5!__MNN+zXcM&MJVsu~Xh zer@gN4b$IdQsSzY@%{({FHjJ{a2MEPw}O@&@HL)jYyNRP&X%x-Li}3PUn`lI7|b8& zwIgX*ynK>htzM;W8N}ZXa+v_Junqp*Hojn%Y^p6jkkZrFL;;zF4GvdK7zL>1Dz_=uGoJ}V)Fuw zctk?^d5E3zO|9(k5@&$L2441MqCk)rLRyrpEq4ADF?Q%rCs9(kC@ml?RU3KqY3v!q zk6<@pOI=e-1CI4Ezs#do16}VT)%$?64D5tBh45(r(#T*Gug&)@8|PUN7x-x@w_^u> zwUXTBGcO{U^v0z#F7%``?kD;oqBtP2=6h|DfB`Vc$KO6;1L2p2)a+8yeJonhCF_Ag zvKTwh8Tw~u$Az=tSQ1hU>Jq;jn`-WuMlS{hqiST6sdH!3Frz^)nN2nJ=fUkR{ZF&$ zuyIqbnnO*-8~W@V%9te19+5TtqrP3qS{`ZOEtmcU{{RfmfRVf(>*TrQbbJbmZ=j^Z zv3E=FuB89wYo0gEr8xVS9H%DLd~z;5AK2g1vqn!Wq6fGZ*j+@!XLo|q6~IRfeE45z z_4l|YsouaF5W&~YjbqqBYyGO^7`%Z`=;1X#>c^yiUqmCgW{jFoquFk8H2D^>kR$ol z6I|$vE7t0B#P}K@UUV2uPh7#))=gZOG}OpsZOgeRmK2J7mrqMwNAlkTUj+8`=%opj(r8ddfM$0}vH4stPnjfg`5l8bQh z7H$!LuyfaoXMl^TdfP&pVe^vt=0f^qV7Q3Xjn~}zyPwKg)R*q35 zLtpXh0P~%4T5Pyv5C~g6sse!UAk6_8(Ps0A-P_{v>Q9`Cx39EqY*l{-&;JRSiz?y{ zhf29a^H)ntsQODwNQ0b*a_SqKc^4!goV6Fp)B2uQVUPo~ir@+2g{6svB2P6Qj1Wd^j?Av(6-^?ChDuh}RlE84Q6n$nL zWrwBlgYThazSYh(P-~B(P__;%VJ^GOi zG*S-7<6$#lMshjHW#FOOty}oa(*NI&DPBPCNe}9ZA`3oDuz`ni!hkJRwGA~Ptx7~7 z{edidCdWCDW>(bD_ib#8F4xiMaKA-K(MCPR$BXFwlbuBVpXjW5%CwK-^k>t|%6jTH z?DE0o=FCmxwym*lxLEE4vn}6JH(}%*yA@E~(7HTMiK9AsK(@K5wuKjkib%v34MPMm zNNDqVBB~@|RSmmy>(oiTX)~KYFy#qqY}j09ZLBH_ZB9ebKGo1~Y^E_e_aX|`JVPv+ zzWTbw(ut)yw2?J+4oA3?W-e%?w~WZX4`HJ;T?aR@t((X1o$2PZCR$|}bM(Gu8e=cy zXQR^1cbn;EAnzgadJ9#K6{BFYG*m{U47HVi-2t|N@7u=d2e2&Yb#IJ542K- ztDIk0gL+e$vR`Iu7~l3v3%a9b;Jq}Bq>^>8VZGai6mM;8 zbaIoi&WTBFJYi@SvuV}G6TsX|{n$22vgaV&qYrJP(ZLfr+?HIVJ6)7)m(D<*{(2it zbWKBjAT{P>j`Fwa1RQLgT)Jil8B)&f)ephdc=d zB_(Am7tF6JtSBg*zo>L!)$-Nl3ziKwr1S7cTV^uUI%LFOJ>3DFh~~yx>?qkRQ)4Wlj*CuSMHD^@++s9E0T zC}HVhvVMH#QJcTjdNlIhB`lSGzSKPTqqXI)e((vbp@SF?=~s9-iZo6?x1VC|H5_@Z zQ-AmjIrKLNDZ;$ApJoOIuja?4cd~TnK1$ZppP|^HYx=R8Qfw8Sa63*t$gfm2!`-=t z4-l1p_8A&$-^4MWkSs45wdNsBxq+!mK|(TBRS4Dt@);l<{q;||{;*U-Cmp1e6j)id zWA{{j5zIUhjM(>qf|1H#zyblNx7vXGHUdPo{}O2JFK-^Cgz(M$_$4RXwBd$6xu2Zo zpAXVM21a{`m3T{w__(+kwMrlVIjy%p!jV67>X_%~M!d|udKu)q@sA!w=$ILKn6iwh zu|_d>67e^b78j4H)Fsc;ri=qH)KMIlceQ=EvAU6K^DWyaG^;Jvl&GsV{qghEG7J4* zPcWC~M)qe|9`9Pvwm}cDn%)zFsel`*d5o1CZfuJHPQa`BhcD0-V~k$;BBiE_r>PXr z%K+toXTgIA_G$gpi&T&ee-S&pWRR_jC+DfPh}PH9R_k#4FhqBbo;gBujMw$tBb4O& zXFPu!@Gjtez!|`Kzz2Y9fX@Nn0R9BH1Nd*i{{RBitZ7F+5kHNdjR_xJb^fH^J(1v6!0^^OMv$P zrvPUFoq&%3zXo&}SlwZR4HxyZBQyly0Hgp$0VV(@1EvFJ0_Fh<0Sf>%fE|ECfENHq z0N+7AuOnmvP5^LRvpNTGeE{qP{07hs_yPc@iHd-(L<154I8;#KY(9mv@~ldJEJ8T< zOcel@0V)A20jmKxWk+oRJOpR~YzI6V=wig)^XfPNdzH(^)~W-c{{UdiT4g}p2oM4Y z14IC#0P%oC0QLa1_Sj3s?Nos5Xf_+6`v4_?GQe^Mms)`!Hoj5o0d;^)fJOkeX;9cI zKw)XU!U}AKCB+KMW7W$5tl(5wyQtm-m;fxjQYQgeN~ExQMqx>bx(4V5U}1y$(gl=h z%9!b{FhN`qs^tlQnSj}VV!&bmX11x-fc1clfNcOw6;VF|>;mitU>I3pkWu{uKm#xW zqcC=%P7?G_8U@k{f>%rZ0U!<-xFVGFQlzIVeOYuq75aJ#T^@ygi`8(Rj?gTCv=O9& zL|a~=)hZ1>B@G_b`wEqvLY<<5Vdm9lyFPK0(x<>IdLnALRae!l8rW{NSUz!C-SQ^? zGM*vTwY61S8ft4B>mI3Ab=;SDRYxBqNBCh*$`Jxup3XNXA0w|3IoF8U+i)v@FSiP9 zX6~!B-x#&T;GJR*lz9Ur(4N?11^yTqU>o7J$wTjYoB7poIv*B4G{6?IGwq4Aj_6Tt zWAtYZXJN5$m)%HkfZ7cUZ>Cr*{?U>(7 zEOM*Sj~Z^e!(#X5?HcdZzdTKOt{#a1f}_1Qy!JM6*e9ehZe#3Cdq7ed}rqN1mDM*gO90R8-m{DHsRw*E-c|AWFQ~< z>>0{h-xHc|rQ*yuelOv%cCf{vL<#$Ob2a2BB?$c-e+Ih;)?g^b`lnV>f zyfy^A-v-9;{v=-!{MSN!_W2Q;4}ef9wzj$*Pgv0d?rK32_rA96p_@M`lC zrE#23F0meXb?9a8pS9~zm*sBtWm?7$k6)(MdWl@40Zm#|JM;!9~K zmCy0oB8@4xc?T>pR(!dgkY&Ui2)z}+m)mO%n=$4&zTwO5uwZ_{7L6~rhemSPaZCTU zlSSdxAy~al^afM|SDz8pnQb@d?I5E| RPx*$jgUfT13M*2A{ud~ZuO|Qi diff --git a/add_registration_permissions.py b/add_registration_permissions.py new file mode 100644 index 0000000..9fd39f0 --- /dev/null +++ b/add_registration_permissions.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +""" +Add Registration Permissions Script + +This script adds the new registration.view and registration.manage permissions +without clearing existing permissions. + +Usage: + python add_registration_permissions.py +""" + +import os +import sys +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from database import Base +from models import Permission, RolePermission, Role, UserRole +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Database connection +DATABASE_URL = os.getenv("DATABASE_URL") +if not DATABASE_URL: + print("Error: DATABASE_URL environment variable not set") + sys.exit(1) + +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# New registration permissions +NEW_PERMISSIONS = [ + {"code": "registration.view", "name": "View Registration Settings", "description": "View registration form schema and settings", "module": "registration"}, + {"code": "registration.manage", "name": "Manage Registration Form", "description": "Edit registration form schema, steps, and fields", "module": "registration"}, +] + +# Roles that should have these permissions +ROLE_PERMISSION_MAP = { + "registration.view": ["admin", "superadmin"], + "registration.manage": ["admin", "superadmin"], +} + + +def add_registration_permissions(): + """Add registration permissions and assign to appropriate roles""" + db = SessionLocal() + + try: + print("=" * 60) + print("Adding Registration Permissions") + print("=" * 60) + + # Step 1: Add permissions if they don't exist + print("\n1. Adding permissions...") + permission_map = {} + + for perm_data in NEW_PERMISSIONS: + existing = db.query(Permission).filter(Permission.code == perm_data["code"]).first() + if existing: + print(f" - {perm_data['code']}: Already exists") + permission_map[perm_data["code"]] = existing + else: + permission = Permission( + code=perm_data["code"], + name=perm_data["name"], + description=perm_data["description"], + module=perm_data["module"] + ) + db.add(permission) + db.flush() # Get the ID + permission_map[perm_data["code"]] = permission + print(f" - {perm_data['code']}: Created") + + db.commit() + + # Step 2: Get roles + print("\n2. Fetching roles...") + roles = db.query(Role).all() + role_map = {role.code: role for role in roles} + print(f" Found {len(roles)} roles: {', '.join(role_map.keys())}") + + # Enum mapping for backward compatibility + role_enum_map = { + 'guest': UserRole.guest, + 'member': UserRole.member, + 'admin': UserRole.admin, + 'superadmin': UserRole.superadmin, + 'finance': UserRole.finance + } + + # Step 3: Assign permissions to roles + print("\n3. Assigning permissions to roles...") + for perm_code, role_codes in ROLE_PERMISSION_MAP.items(): + permission = permission_map.get(perm_code) + if not permission: + print(f" Warning: Permission {perm_code} not found") + continue + + for role_code in role_codes: + role = role_map.get(role_code) + if not role: + print(f" Warning: Role {role_code} not found") + continue + + # Check if mapping already exists + existing_mapping = db.query(RolePermission).filter( + RolePermission.role_id == role.id, + RolePermission.permission_id == permission.id + ).first() + + if existing_mapping: + print(f" - {role_code} -> {perm_code}: Already assigned") + else: + role_enum = role_enum_map.get(role_code, UserRole.guest) + mapping = RolePermission( + role=role_enum, + role_id=role.id, + permission_id=permission.id + ) + db.add(mapping) + print(f" - {role_code} -> {perm_code}: Assigned") + + db.commit() + + print("\n" + "=" * 60) + print("Registration permissions added successfully!") + print("=" * 60) + + except Exception as e: + db.rollback() + print(f"\nError: {str(e)}") + import traceback + traceback.print_exc() + raise + finally: + db.close() + + +if __name__ == "__main__": + add_registration_permissions() diff --git a/alembic/versions/014_add_custom_registration_data.py b/alembic/versions/014_add_custom_registration_data.py new file mode 100644 index 0000000..962318e --- /dev/null +++ b/alembic/versions/014_add_custom_registration_data.py @@ -0,0 +1,39 @@ +"""add_custom_registration_data + +Revision ID: 014_custom_registration +Revises: a1b2c3d4e5f6 +Create Date: 2026-02-01 10:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '014_custom_registration' +down_revision: Union[str, None] = 'a1b2c3d4e5f6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add custom_registration_data column to users table + # This stores dynamic registration field responses as JSON + op.add_column('users', sa.Column( + 'custom_registration_data', + sa.JSON, + nullable=False, + server_default='{}' + )) + + # Add comment for documentation + op.execute(""" + COMMENT ON COLUMN users.custom_registration_data IS + 'Dynamic registration field responses stored as JSON for custom form fields'; + """) + + +def downgrade() -> None: + op.drop_column('users', 'custom_registration_data') diff --git a/models.py b/models.py index 4e3a4c6..f256cd7 100644 --- a/models.py +++ b/models.py @@ -151,6 +151,10 @@ class User(Base): # Stripe Customer ID - Centralized for payment method management stripe_customer_id = Column(String, nullable=True, index=True, comment="Stripe Customer ID for payment method management") + # Dynamic Registration Form - Custom field responses + custom_registration_data = Column(JSON, default=dict, nullable=False, + comment="Dynamic registration field responses stored as JSON for custom form fields") + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) diff --git a/seed_permissions_rbac.py b/seed_permissions_rbac.py index a4c407a..68806fc 100755 --- a/seed_permissions_rbac.py +++ b/seed_permissions_rbac.py @@ -123,6 +123,10 @@ PERMISSIONS = [ {"code": "payment_methods.create", "name": "Create Payment Methods", "description": "Add payment methods on behalf of users", "module": "payment_methods"}, {"code": "payment_methods.delete", "name": "Delete Payment Methods", "description": "Remove user payment methods", "module": "payment_methods"}, {"code": "payment_methods.set_default", "name": "Set Default Payment Method", "description": "Set default payment method for users", "module": "payment_methods"}, + + # ========== REGISTRATION MODULE (2) ========== + {"code": "registration.view", "name": "View Registration Settings", "description": "View registration form schema and settings", "module": "registration"}, + {"code": "registration.manage", "name": "Manage Registration Form", "description": "Edit registration form schema, steps, and fields", "module": "registration"}, ] # Default system roles that must exist @@ -204,6 +208,8 @@ DEFAULT_ROLE_PERMISSIONS = { # Payment methods - admin can manage but not view sensitive details "payment_methods.view", "payment_methods.create", "payment_methods.delete", "payment_methods.set_default", + # Registration form management + "registration.view", "registration.manage", ], "superadmin": [ diff --git a/server.py b/server.py index ff70028..3bf2f27 100644 --- a/server.py +++ b/server.py @@ -134,16 +134,23 @@ def set_user_role(user: User, role_enum: UserRole, db: Session): # Pydantic Models # ============================================================ class RegisterRequest(BaseModel): - # Step 1: Personal & Partner Information + """Dynamic registration request - validates against registration schema""" + + # Fixed required fields (always present) first_name: str last_name: str - phone: str - address: str - city: str - state: str - zipcode: str - date_of_birth: datetime - lead_sources: List[str] + email: EmailStr + password: str = Field(min_length=6) + accepts_tos: bool = False + + # Step 1: Personal & Partner Information (optional for dynamic schema) + phone: Optional[str] = None + address: Optional[str] = None + city: Optional[str] = None + state: Optional[str] = None + zipcode: Optional[str] = None + date_of_birth: Optional[datetime] = None + lead_sources: Optional[List[str]] = None partner_first_name: Optional[str] = None partner_last_name: Optional[str] = None partner_is_member: Optional[bool] = False @@ -151,16 +158,16 @@ class RegisterRequest(BaseModel): # Step 2: Newsletter, Volunteer & Scholarship referred_by_member_name: Optional[str] = None - newsletter_publish_name: bool - newsletter_publish_photo: bool - newsletter_publish_birthday: bool - newsletter_publish_none: bool - volunteer_interests: List[str] = [] - scholarship_requested: bool = False + newsletter_publish_name: Optional[bool] = False + newsletter_publish_photo: Optional[bool] = False + newsletter_publish_birthday: Optional[bool] = False + newsletter_publish_none: Optional[bool] = False + volunteer_interests: Optional[List[str]] = [] + scholarship_requested: Optional[bool] = False scholarship_reason: Optional[str] = None # Step 3: Directory Settings - show_in_directory: bool = False + show_in_directory: Optional[bool] = False directory_email: Optional[str] = None directory_bio: Optional[str] = None directory_address: Optional[str] = None @@ -168,10 +175,9 @@ class RegisterRequest(BaseModel): directory_dob: Optional[datetime] = None directory_partner_name: Optional[str] = None - # Step 4: Account Credentials - email: EmailStr - password: str = Field(min_length=6) - accepts_tos: bool = False + # Allow extra fields for custom registration data + class Config: + extra = 'allow' @validator('accepts_tos') def tos_must_be_accepted(cls, v): @@ -179,25 +185,6 @@ class RegisterRequest(BaseModel): raise ValueError('You must accept the Terms of Service to register') return v - @validator('newsletter_publish_none') - def validate_newsletter_preferences(cls, v, values): - """At least one newsletter preference must be selected""" - name = values.get('newsletter_publish_name', False) - photo = values.get('newsletter_publish_photo', False) - birthday = values.get('newsletter_publish_birthday', False) - - if not (name or photo or birthday or v): - raise ValueError('At least one newsletter publication preference must be selected') - return v - - @validator('scholarship_reason') - def validate_scholarship_reason(cls, v, values): - """If scholarship requested, reason must be provided""" - requested = values.get('scholarship_requested', False) - if requested and not v: - raise ValueError('Scholarship reason is required when requesting scholarship') - return v - class LoginRequest(BaseModel): email: EmailStr password: str @@ -560,11 +547,28 @@ async def register(request: RegisterRequest, db: Session = Depends(get_db)): existing_user = db.query(User).filter(User.email == request.email).first() if existing_user: raise HTTPException(status_code=400, detail="Email already registered") - + + # Get registration schema for dynamic validation + schema = get_registration_schema(db) + + # Convert request to dict for dynamic validation + request_data = request.dict(exclude_unset=False) + + # Perform dynamic schema validation + is_valid, validation_errors = validate_dynamic_registration(request_data, schema) + if not is_valid: + raise HTTPException( + status_code=400, + detail={"message": "Validation failed", "errors": validation_errors} + ) + + # Split data into User model fields and custom fields + user_data, custom_data = split_registration_data(request_data, schema) + # Generate verification token verification_token = secrets.token_urlsafe(32) - - # Create user + + # Create user with known fields user = User( # Account credentials (Step 4) email=request.email, @@ -573,65 +577,68 @@ async def register(request: RegisterRequest, db: Session = Depends(get_db)): # Personal information (Step 1) first_name=request.first_name, last_name=request.last_name, - phone=request.phone, - address=request.address, - city=request.city, - state=request.state, - zipcode=request.zipcode, - date_of_birth=request.date_of_birth, - lead_sources=request.lead_sources, + phone=user_data.get('phone') or request.phone, + address=user_data.get('address') or request.address, + city=user_data.get('city') or request.city, + state=user_data.get('state') or request.state, + zipcode=user_data.get('zipcode') or request.zipcode, + date_of_birth=user_data.get('date_of_birth') or request.date_of_birth, + lead_sources=user_data.get('lead_sources') or request.lead_sources or [], # Partner information (Step 1) - partner_first_name=request.partner_first_name, - partner_last_name=request.partner_last_name, - partner_is_member=request.partner_is_member, - partner_plan_to_become_member=request.partner_plan_to_become_member, + partner_first_name=user_data.get('partner_first_name') or request.partner_first_name, + partner_last_name=user_data.get('partner_last_name') or request.partner_last_name, + partner_is_member=user_data.get('partner_is_member', request.partner_is_member) or False, + partner_plan_to_become_member=user_data.get('partner_plan_to_become_member', request.partner_plan_to_become_member) or False, # Referral (Step 2) - referred_by_member_name=request.referred_by_member_name, + referred_by_member_name=user_data.get('referred_by_member_name') or request.referred_by_member_name, # Newsletter publication preferences (Step 2) - newsletter_publish_name=request.newsletter_publish_name, - newsletter_publish_photo=request.newsletter_publish_photo, - newsletter_publish_birthday=request.newsletter_publish_birthday, - newsletter_publish_none=request.newsletter_publish_none, + newsletter_publish_name=user_data.get('newsletter_publish_name', request.newsletter_publish_name) or False, + newsletter_publish_photo=user_data.get('newsletter_publish_photo', request.newsletter_publish_photo) or False, + newsletter_publish_birthday=user_data.get('newsletter_publish_birthday', request.newsletter_publish_birthday) or False, + newsletter_publish_none=user_data.get('newsletter_publish_none', request.newsletter_publish_none) or False, # Volunteer interests (Step 2) - volunteer_interests=request.volunteer_interests, + volunteer_interests=user_data.get('volunteer_interests') or request.volunteer_interests or [], # Scholarship (Step 2) - scholarship_requested=request.scholarship_requested, - scholarship_reason=request.scholarship_reason, + scholarship_requested=user_data.get('scholarship_requested', request.scholarship_requested) or False, + scholarship_reason=user_data.get('scholarship_reason') or request.scholarship_reason, # Directory settings (Step 3) - show_in_directory=request.show_in_directory, - directory_email=request.directory_email, - directory_bio=request.directory_bio, - directory_address=request.directory_address, - directory_phone=request.directory_phone, - directory_dob=request.directory_dob, - directory_partner_name=request.directory_partner_name, + show_in_directory=user_data.get('show_in_directory', request.show_in_directory) or False, + directory_email=user_data.get('directory_email') or request.directory_email, + directory_bio=user_data.get('directory_bio') or request.directory_bio, + directory_address=user_data.get('directory_address') or request.directory_address, + directory_phone=user_data.get('directory_phone') or request.directory_phone, + directory_dob=user_data.get('directory_dob') or request.directory_dob, + directory_partner_name=user_data.get('directory_partner_name') or request.directory_partner_name, # Terms of Service acceptance (Step 4) accepts_tos=request.accepts_tos, tos_accepted_at=datetime.now(timezone.utc) if request.accepts_tos else None, + # Custom registration data for dynamic fields + custom_registration_data=custom_data if custom_data else {}, + # Status fields status=UserStatus.pending_email, role=UserRole.guest, email_verified=False, email_verification_token=verification_token ) - + db.add(user) db.commit() db.refresh(user) - + # Send verification email await send_verification_email(user.email, verification_token) - + logger.info(f"User registered: {user.email}") - + return {"message": "Registration successful. Please check your email to verify your account."} @api_router.get("/auth/verify-email") @@ -2062,6 +2069,664 @@ async def get_config_limits(): "max_storage_bytes": int(os.getenv('MAX_STORAGE_BYTES', 1073741824)) } +# ============================================================================ +# Registration Form Schema Routes +# ============================================================================ + +# Default registration schema matching current 4-step form +DEFAULT_REGISTRATION_SCHEMA = { + "version": "1.0", + "steps": [ + { + "id": "step_personal", + "title": "Personal Information", + "description": "Please provide your personal details and tell us how you heard about us.", + "order": 1, + "sections": [ + { + "id": "section_personal_info", + "title": "Personal Information", + "order": 1, + "fields": [ + {"id": "first_name", "type": "text", "label": "First Name", "required": True, "is_fixed": True, "mapping": "first_name", "validation": {"minLength": 1, "maxLength": 100}, "width": "half", "order": 1}, + {"id": "last_name", "type": "text", "label": "Last Name", "required": True, "is_fixed": True, "mapping": "last_name", "validation": {"minLength": 1, "maxLength": 100}, "width": "half", "order": 2}, + {"id": "phone", "type": "phone", "label": "Phone", "required": True, "is_fixed": False, "mapping": "phone", "width": "half", "order": 3}, + {"id": "date_of_birth", "type": "date", "label": "Date of Birth", "required": True, "is_fixed": False, "mapping": "date_of_birth", "width": "half", "order": 4}, + {"id": "address", "type": "text", "label": "Address", "required": True, "is_fixed": False, "mapping": "address", "width": "full", "order": 5}, + {"id": "city", "type": "text", "label": "City", "required": True, "is_fixed": False, "mapping": "city", "width": "third", "order": 6}, + {"id": "state", "type": "text", "label": "State", "required": True, "is_fixed": False, "mapping": "state", "width": "third", "order": 7}, + {"id": "zipcode", "type": "text", "label": "Zipcode", "required": True, "is_fixed": False, "mapping": "zipcode", "width": "third", "order": 8} + ] + }, + { + "id": "section_lead_sources", + "title": "How Did You Hear About Us?", + "order": 2, + "fields": [ + {"id": "lead_sources", "type": "multiselect", "label": "How did you hear about us?", "required": True, "is_fixed": False, "mapping": "lead_sources", "width": "full", "order": 1, "options": [ + {"value": "Current member", "label": "Current member"}, + {"value": "Friend", "label": "Friend"}, + {"value": "OutSmart Magazine", "label": "OutSmart Magazine"}, + {"value": "Search engine (Google etc.)", "label": "Search engine (Google etc.)"}, + {"value": "I've known about LOAF for a long time", "label": "I've known about LOAF for a long time"}, + {"value": "Other", "label": "Other"} + ]} + ] + }, + { + "id": "section_partner", + "title": "Partner Information (Optional)", + "order": 3, + "fields": [ + {"id": "partner_first_name", "type": "text", "label": "Partner First Name", "required": False, "is_fixed": False, "mapping": "partner_first_name", "width": "half", "order": 1}, + {"id": "partner_last_name", "type": "text", "label": "Partner Last Name", "required": False, "is_fixed": False, "mapping": "partner_last_name", "width": "half", "order": 2}, + {"id": "partner_is_member", "type": "checkbox", "label": "Is your partner already a member?", "required": False, "is_fixed": False, "mapping": "partner_is_member", "width": "full", "order": 3}, + {"id": "partner_plan_to_become_member", "type": "checkbox", "label": "Does your partner plan to become a member?", "required": False, "is_fixed": False, "mapping": "partner_plan_to_become_member", "width": "full", "order": 4} + ] + } + ] + }, + { + "id": "step_newsletter", + "title": "Newsletter & Volunteer", + "description": "Tell us about your newsletter preferences and volunteer interests.", + "order": 2, + "sections": [ + { + "id": "section_referral", + "title": "Referral", + "order": 1, + "fields": [ + {"id": "referred_by_member_name", "type": "text", "label": "If referred by a current member, please provide their name", "required": False, "is_fixed": False, "mapping": "referred_by_member_name", "width": "full", "order": 1, "placeholder": "Enter member name or email"} + ] + }, + { + "id": "section_newsletter_prefs", + "title": "Newsletter Publication Preferences", + "description": "Select what you would like published in our newsletter.", + "order": 2, + "fields": [ + {"id": "newsletter_publish_name", "type": "checkbox", "label": "Publish my name", "required": False, "is_fixed": False, "mapping": "newsletter_publish_name", "width": "full", "order": 1}, + {"id": "newsletter_publish_photo", "type": "checkbox", "label": "Publish my photo", "required": False, "is_fixed": False, "mapping": "newsletter_publish_photo", "width": "full", "order": 2}, + {"id": "newsletter_publish_birthday", "type": "checkbox", "label": "Publish my birthday", "required": False, "is_fixed": False, "mapping": "newsletter_publish_birthday", "width": "full", "order": 3}, + {"id": "newsletter_publish_none", "type": "checkbox", "label": "Don't publish anything about me", "required": False, "is_fixed": False, "mapping": "newsletter_publish_none", "width": "full", "order": 4} + ], + "validation": {"atLeastOne": True, "message": "Please select at least one newsletter publication preference"} + }, + { + "id": "section_volunteer", + "title": "Volunteer Interests", + "order": 3, + "fields": [ + {"id": "volunteer_interests", "type": "multiselect", "label": "Select areas where you would like to volunteer", "required": False, "is_fixed": False, "mapping": "volunteer_interests", "width": "full", "order": 1, "options": [ + {"value": "Events", "label": "Events"}, + {"value": "Hospitality", "label": "Hospitality"}, + {"value": "Newsletter", "label": "Newsletter"}, + {"value": "Board", "label": "Board"}, + {"value": "Community Outreach", "label": "Community Outreach"}, + {"value": "Other", "label": "Other"} + ]} + ] + }, + { + "id": "section_scholarship", + "title": "Scholarship Request", + "order": 4, + "fields": [ + {"id": "scholarship_requested", "type": "checkbox", "label": "I would like to request a scholarship", "required": False, "is_fixed": False, "mapping": "scholarship_requested", "width": "full", "order": 1}, + {"id": "scholarship_reason", "type": "textarea", "label": "Please explain why you are requesting a scholarship", "required": False, "is_fixed": False, "mapping": "scholarship_reason", "width": "full", "order": 2, "rows": 4} + ] + } + ] + }, + { + "id": "step_directory", + "title": "Member Directory", + "description": "Choose what information to display in the member directory.", + "order": 3, + "sections": [ + { + "id": "section_directory_settings", + "title": "Directory Settings", + "order": 1, + "fields": [ + {"id": "show_in_directory", "type": "checkbox", "label": "Show my profile in the member directory", "required": False, "is_fixed": False, "mapping": "show_in_directory", "width": "full", "order": 1}, + {"id": "directory_email", "type": "email", "label": "Directory Email (if different from account email)", "required": False, "is_fixed": False, "mapping": "directory_email", "width": "full", "order": 2}, + {"id": "directory_bio", "type": "textarea", "label": "Bio for directory", "required": False, "is_fixed": False, "mapping": "directory_bio", "width": "full", "order": 3, "rows": 4}, + {"id": "directory_address", "type": "text", "label": "Address to display in directory", "required": False, "is_fixed": False, "mapping": "directory_address", "width": "full", "order": 4}, + {"id": "directory_phone", "type": "phone", "label": "Phone to display in directory", "required": False, "is_fixed": False, "mapping": "directory_phone", "width": "half", "order": 5}, + {"id": "directory_dob", "type": "date", "label": "Birthday to display in directory", "required": False, "is_fixed": False, "mapping": "directory_dob", "width": "half", "order": 6}, + {"id": "directory_partner_name", "type": "text", "label": "Partner name to display in directory", "required": False, "is_fixed": False, "mapping": "directory_partner_name", "width": "full", "order": 7} + ] + } + ] + }, + { + "id": "step_account", + "title": "Account Setup", + "description": "Create your account credentials and accept the terms of service.", + "order": 4, + "sections": [ + { + "id": "section_credentials", + "title": "Account Credentials", + "order": 1, + "fields": [ + {"id": "email", "type": "email", "label": "Email Address", "required": True, "is_fixed": True, "mapping": "email", "width": "full", "order": 1}, + {"id": "password", "type": "password", "label": "Password", "required": True, "is_fixed": True, "mapping": "password", "validation": {"minLength": 6}, "width": "half", "order": 2}, + {"id": "confirmPassword", "type": "password", "label": "Confirm Password", "required": True, "is_fixed": True, "client_only": True, "width": "half", "order": 3, "validation": {"matchField": "password"}} + ] + }, + { + "id": "section_tos", + "title": "Terms of Service", + "order": 2, + "fields": [ + {"id": "accepts_tos", "type": "checkbox", "label": "I accept the Terms of Service and Privacy Policy", "required": True, "is_fixed": True, "mapping": "accepts_tos", "width": "full", "order": 1} + ] + } + ] + } + ], + "conditional_rules": [ + { + "id": "rule_scholarship_reason", + "trigger_field": "scholarship_requested", + "trigger_operator": "equals", + "trigger_value": True, + "action": "show", + "target_fields": ["scholarship_reason"] + } + ], + "fixed_fields": ["email", "password", "first_name", "last_name", "accepts_tos"] +} + +# Supported field types with their validation options +FIELD_TYPES = { + "text": { + "name": "Text Input", + "validation_options": ["required", "minLength", "maxLength", "pattern"], + "properties": ["placeholder", "width"] + }, + "email": { + "name": "Email Input", + "validation_options": ["required"], + "properties": ["placeholder"] + }, + "phone": { + "name": "Phone Input", + "validation_options": ["required"], + "properties": ["placeholder"] + }, + "date": { + "name": "Date Input", + "validation_options": ["required", "min_date", "max_date"], + "properties": [] + }, + "dropdown": { + "name": "Dropdown Select", + "validation_options": ["required"], + "properties": ["options", "placeholder"] + }, + "checkbox": { + "name": "Checkbox", + "validation_options": ["required"], + "properties": [] + }, + "radio": { + "name": "Radio Group", + "validation_options": ["required"], + "properties": ["options"] + }, + "multiselect": { + "name": "Multi-Select", + "validation_options": ["required", "min_selections", "max_selections"], + "properties": ["options"] + }, + "address_group": { + "name": "Address Group", + "validation_options": ["required"], + "properties": [] + }, + "textarea": { + "name": "Text Area", + "validation_options": ["required", "minLength", "maxLength"], + "properties": ["rows", "placeholder"] + }, + "file_upload": { + "name": "File Upload", + "validation_options": ["required", "file_types", "max_size"], + "properties": ["allowed_types", "max_size_mb"] + }, + "password": { + "name": "Password Input", + "validation_options": ["required", "minLength"], + "properties": [] + } +} + + +class RegistrationSchemaRequest(BaseModel): + """Request model for updating registration schema""" + schema_data: dict + + +def get_registration_schema(db: Session) -> dict: + """Get the current registration form schema from database or return default""" + setting = db.query(SystemSettings).filter( + SystemSettings.setting_key == "registration.form_schema" + ).first() + + if setting and setting.setting_value: + import json + try: + return json.loads(setting.setting_value) + except json.JSONDecodeError: + logger.error("Failed to parse registration schema from database") + return DEFAULT_REGISTRATION_SCHEMA.copy() + + return DEFAULT_REGISTRATION_SCHEMA.copy() + + +def save_registration_schema(db: Session, schema: dict, user_id: Optional[uuid.UUID] = None) -> None: + """Save registration schema to database""" + import json + + setting = db.query(SystemSettings).filter( + SystemSettings.setting_key == "registration.form_schema" + ).first() + + schema_json = json.dumps(schema) + + if setting: + setting.setting_value = schema_json + setting.updated_by = user_id + setting.updated_at = datetime.now(timezone.utc) + else: + from models import SettingType + setting = SystemSettings( + setting_key="registration.form_schema", + setting_value=schema_json, + setting_type=SettingType.json, + description="Dynamic registration form schema defining steps, fields, and validation rules", + updated_by=user_id, + is_sensitive=False + ) + db.add(setting) + + db.commit() + + +def validate_schema(schema: dict) -> tuple[bool, list[str]]: + """Validate registration schema structure""" + errors = [] + + # Check version + if "version" not in schema: + errors.append("Schema must have a version field") + + # Check steps + if "steps" not in schema or not isinstance(schema.get("steps"), list): + errors.append("Schema must have a steps array") + return False, errors + + if len(schema["steps"]) == 0: + errors.append("Schema must have at least one step") + + if len(schema["steps"]) > 10: + errors.append("Schema cannot have more than 10 steps") + + # Check fixed fields exist + fixed_fields = schema.get("fixed_fields", ["email", "password", "first_name", "last_name", "accepts_tos"]) + all_field_ids = set() + + for step in schema.get("steps", []): + if "id" not in step: + errors.append(f"Step missing id") + continue + + if "sections" not in step or not isinstance(step.get("sections"), list): + errors.append(f"Step {step.get('id')} must have sections array") + continue + + for section in step.get("sections", []): + if "fields" not in section or not isinstance(section.get("fields"), list): + errors.append(f"Section {section.get('id')} must have fields array") + continue + + for field in section.get("fields", []): + if "id" not in field: + errors.append(f"Field missing id in section {section.get('id')}") + continue + + all_field_ids.add(field["id"]) + + if "type" not in field: + errors.append(f"Field {field['id']} missing type") + + if field.get("type") not in FIELD_TYPES: + errors.append(f"Field {field['id']} has invalid type: {field.get('type')}") + + # Verify fixed fields are present + for fixed_field in fixed_fields: + if fixed_field not in all_field_ids: + errors.append(f"Fixed field '{fixed_field}' must be present in schema") + + # Field limit check + if len(all_field_ids) > 100: + errors.append("Schema cannot have more than 100 fields") + + return len(errors) == 0, errors + + +def evaluate_conditional_rules(form_data: dict, rules: list) -> set: + """Evaluate conditional rules and return set of visible field IDs""" + visible_fields = set() + + # Start with all fields visible + for rule in rules: + target_fields = rule.get("target_fields", []) + if rule.get("action") == "hide": + visible_fields.update(target_fields) + + # Apply rules + for rule in rules: + trigger_field = rule.get("trigger_field") + trigger_value = rule.get("trigger_value") + trigger_operator = rule.get("trigger_operator", "equals") + action = rule.get("action", "show") + target_fields = rule.get("target_fields", []) + + field_value = form_data.get(trigger_field) + + # Evaluate condition + condition_met = False + if trigger_operator == "equals": + condition_met = field_value == trigger_value + elif trigger_operator == "not_equals": + condition_met = field_value != trigger_value + elif trigger_operator == "contains": + condition_met = trigger_value in (field_value or []) if isinstance(field_value, list) else trigger_value in str(field_value or "") + elif trigger_operator == "not_empty": + condition_met = bool(field_value) + elif trigger_operator == "empty": + condition_met = not bool(field_value) + + # Apply action + if condition_met: + if action == "show": + visible_fields.update(target_fields) + elif action == "hide": + visible_fields -= set(target_fields) + + return visible_fields + + +def validate_field_by_type(field: dict, value) -> list[str]: + """Validate a field value based on its type and validation rules""" + errors = [] + field_type = field.get("type") + validation = field.get("validation", {}) + label = field.get("label", field.get("id")) + + if field_type == "text" or field_type == "textarea": + if not isinstance(value, str): + errors.append(f"{label} must be text") + return errors + if "minLength" in validation and len(value) < validation["minLength"]: + errors.append(f"{label} must be at least {validation['minLength']} characters") + if "maxLength" in validation and len(value) > validation["maxLength"]: + errors.append(f"{label} must be at most {validation['maxLength']} characters") + if "pattern" in validation: + import re + if not re.match(validation["pattern"], value): + errors.append(f"{label} format is invalid") + + elif field_type == "email": + import re + email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$' + if not re.match(email_pattern, str(value)): + errors.append(f"{label} must be a valid email address") + + elif field_type == "phone": + if not isinstance(value, str) or len(value) < 7: + errors.append(f"{label} must be a valid phone number") + + elif field_type == "date": + # Date validation happens during parsing + pass + + elif field_type == "dropdown" or field_type == "radio": + options = [opt.get("value") for opt in field.get("options", [])] + if value not in options: + errors.append(f"{label} must be one of the available options") + + elif field_type == "multiselect": + if not isinstance(value, list): + errors.append(f"{label} must be a list of selections") + else: + options = [opt.get("value") for opt in field.get("options", [])] + for v in value: + if v not in options: + errors.append(f"{label} contains invalid option: {v}") + if "min_selections" in validation and len(value) < validation["min_selections"]: + errors.append(f"{label} requires at least {validation['min_selections']} selections") + if "max_selections" in validation and len(value) > validation["max_selections"]: + errors.append(f"{label} allows at most {validation['max_selections']} selections") + + elif field_type == "checkbox": + if not isinstance(value, bool): + errors.append(f"{label} must be true or false") + + elif field_type == "password": + if not isinstance(value, str): + errors.append(f"{label} must be text") + elif "minLength" in validation and len(value) < validation["minLength"]: + errors.append(f"{label} must be at least {validation['minLength']} characters") + + return errors + + +def validate_dynamic_registration(data: dict, schema: dict) -> tuple[bool, list[str]]: + """Validate registration data against dynamic schema""" + errors = [] + conditional_rules = schema.get("conditional_rules", []) + + # Get all fields and their visibility based on conditional rules + hidden_fields = set() + for rule in conditional_rules: + if rule.get("action") == "show": + # Fields are hidden by default if they have a "show" rule + hidden_fields.update(rule.get("target_fields", [])) + + # Evaluate which hidden fields should now be visible + visible_conditional_fields = evaluate_conditional_rules(data, conditional_rules) + hidden_fields -= visible_conditional_fields + + for step in schema.get("steps", []): + for section in step.get("sections", []): + # Check section-level validation + section_validation = section.get("validation", {}) + if section_validation.get("atLeastOne"): + field_ids = [f["id"] for f in section.get("fields", [])] + has_value = any(data.get(fid) for fid in field_ids) + if not has_value: + errors.append(section_validation.get("message", f"At least one field in {section.get('title', 'this section')} is required")) + + for field in section.get("fields", []): + field_id = field.get("id") + + # Skip hidden fields + if field_id in hidden_fields: + continue + + # Skip client-only fields (like confirmPassword) + if field.get("client_only"): + continue + + value = data.get(field_id) + + # Required check + if field.get("required"): + if value is None or value == "" or (isinstance(value, list) and len(value) == 0): + errors.append(f"{field.get('label', field_id)} is required") + continue + + # Type-specific validation + if value is not None and value != "": + field_errors = validate_field_by_type(field, value) + errors.extend(field_errors) + + return len(errors) == 0, errors + + +def split_registration_data(data: dict, schema: dict) -> tuple[dict, dict]: + """Split registration data into User model fields and custom fields""" + user_data = {} + custom_data = {} + + # Get field mappings from schema + field_mappings = {} + for step in schema.get("steps", []): + for section in step.get("sections", []): + for field in section.get("fields", []): + if field.get("mapping"): + field_mappings[field["id"]] = field["mapping"] + + # User model fields that have direct column mappings + user_model_fields = { + "email", "password", "first_name", "last_name", "phone", "address", + "city", "state", "zipcode", "date_of_birth", "lead_sources", + "partner_first_name", "partner_last_name", "partner_is_member", + "partner_plan_to_become_member", "referred_by_member_name", + "newsletter_publish_name", "newsletter_publish_photo", + "newsletter_publish_birthday", "newsletter_publish_none", + "volunteer_interests", "scholarship_requested", "scholarship_reason", + "show_in_directory", "directory_email", "directory_bio", + "directory_address", "directory_phone", "directory_dob", + "directory_partner_name", "accepts_tos" + } + + for field_id, value in data.items(): + mapping = field_mappings.get(field_id, field_id) + + # Skip client-only fields + if field_id == "confirmPassword": + continue + + if mapping in user_model_fields: + user_data[mapping] = value + else: + custom_data[field_id] = value + + return user_data, custom_data + + +# Public endpoint - returns schema for registration form +@api_router.get("/registration/schema") +async def get_public_registration_schema(db: Session = Depends(get_db)): + """Get registration form schema for public registration page""" + schema = get_registration_schema(db) + # Return a clean version without internal metadata + return { + "version": schema.get("version"), + "steps": schema.get("steps", []), + "conditional_rules": schema.get("conditional_rules", []), + "fixed_fields": schema.get("fixed_fields", []) + } + + +# Admin endpoint - returns schema with metadata +@api_router.get("/admin/registration/schema") +async def get_admin_registration_schema( + current_user: User = Depends(require_permission("registration.view")), + db: Session = Depends(get_db) +): + """Get registration form schema with admin metadata""" + schema = get_registration_schema(db) + + # Get version info + setting = db.query(SystemSettings).filter( + SystemSettings.setting_key == "registration.form_schema" + ).first() + + return { + "schema": schema, + "metadata": { + "last_updated": setting.updated_at.isoformat() if setting else None, + "updated_by": str(setting.updated_by) if setting and setting.updated_by else None, + "is_default": setting is None + } + } + + +# Admin endpoint - update schema +@api_router.put("/admin/registration/schema") +async def update_registration_schema( + request: RegistrationSchemaRequest, + current_user: User = Depends(require_permission("registration.manage")), + db: Session = Depends(get_db) +): + """Update registration form schema""" + schema = request.schema_data + + # Validate schema structure + is_valid, errors = validate_schema(schema) + if not is_valid: + raise HTTPException( + status_code=400, + detail={"message": "Invalid schema", "errors": errors} + ) + + # Save schema + save_registration_schema(db, schema, current_user.id) + + logger.info(f"Registration schema updated by user {current_user.email}") + + return {"message": "Registration schema updated successfully"} + + +# Admin endpoint - validate schema without saving +@api_router.post("/admin/registration/schema/validate") +async def validate_registration_schema_endpoint( + request: RegistrationSchemaRequest, + current_user: User = Depends(require_permission("registration.manage")), + db: Session = Depends(get_db) +): + """Validate registration form schema without saving""" + schema = request.schema_data + is_valid, errors = validate_schema(schema) + + return { + "valid": is_valid, + "errors": errors + } + + +# Admin endpoint - reset schema to default +@api_router.post("/admin/registration/schema/reset") +async def reset_registration_schema( + current_user: User = Depends(require_permission("registration.manage")), + db: Session = Depends(get_db) +): + """Reset registration form schema to default""" + save_registration_schema(db, DEFAULT_REGISTRATION_SCHEMA.copy(), current_user.id) + + logger.info(f"Registration schema reset to default by user {current_user.email}") + + return {"message": "Registration schema reset to default"} + + +# Admin endpoint - get available field types +@api_router.get("/admin/registration/field-types") +async def get_field_types( + current_user: User = Depends(require_permission("registration.view")), + db: Session = Depends(get_db) +): + """Get available field types for registration form builder""" + return FIELD_TYPES + + # ============================================================================ # Admin Routes # ============================================================================