From 2a7c45cceec1a1ab3a123d1d6f96b3a18508ade3 Mon Sep 17 00:00:00 2001 From: Kurzenberger <01kuni1bif@hft-stuttgart.de> Date: Fri, 15 Nov 2024 11:12:17 +0100 Subject: [PATCH] changed to match namespace conventions --- dta.zip | Bin 33005 -> 35018 bytes dta/classes/backend.php | 144 --------- dta/classes/{database.php => db_utils.php} | 73 ++--- dta/classes/dta_backend_utils.php | 151 ++++++++++ dta/classes/models/dta_recommendation.php | 90 ++++++ dta/classes/models/dta_result.php | 114 +++++++ dta/classes/models/dta_result_summary.php | 173 +++++++++++ .../{view.php => view_submission_utils.php} | 61 ++-- dta/locallib.php | 159 +++++----- dta/models/DtaResult.php | 277 ------------------ 10 files changed, 675 insertions(+), 567 deletions(-) delete mode 100644 dta/classes/backend.php rename dta/classes/{database.php => db_utils.php} (82%) create mode 100644 dta/classes/dta_backend_utils.php create mode 100644 dta/classes/models/dta_recommendation.php create mode 100644 dta/classes/models/dta_result.php create mode 100644 dta/classes/models/dta_result_summary.php rename dta/classes/{view.php => view_submission_utils.php} (75%) delete mode 100644 dta/models/DtaResult.php diff --git a/dta.zip b/dta.zip index b63cd5fd0c2660cc8626519f46958bd7ae94f5df..7dcd55a2535cd9a81852ca626777d90e7c5afd0e 100644 GIT binary patch delta 16638 zcma*O1#}!svMnlRW@ct)W@ct)W^OSvvt%(?mc?W-Gn2(ETg+t1U-Efp&dmJ#@B7s& zJG*OTMn!dXM(l{#c@_jRmIMN?Bnt|L2J~@+c&jAA<G__0pjh^uvs-@jDF)2g2p|~_ z+J6STzmakz>EHsIxH&BUZth;NK)_(fpg=%>-p+J&9oM)p0;X%XA%saW<5@$zT`A65 zt>=6=<?HKWgl8dw>1H{~>r2opxw7)!9{3|LqZS71EOqQKwFJr1#RvlU9$dwK|6<X# z@EsmDYd6cC(3XS#H65&%=%%n)Y7OQYKLSvRwDa=&mb@IoW{B61ms=*ey`HPmH{YfO z$&H^p+(=}cn3yXO8<RT;1KbY=oqWtSIZ6|^@(Y=|r;{YgN?{-wMfe7NIPGe8a?$rX zoPrI}#6@+&1S44%V=hz`1xVsse+C-~t2Csg9#MGIkPw3k5gt@Jg}(?k-}ET|BsV}W zrC<Ul+o+&~8|4ZlXv8(yf~ywNiBK9Z(&lGj-IWah**L1CU5<5S93`^%e2Ff8G&VVS z-W&PBnFWrbJHFhpc1v;e_$TO-yPGd>dv*X<P_Ic=pQ?`{I(74y2}G)~q<3PZBoIS} zU^l-ntg1xqP{!U5$UsGgq50~Y(NKV%-c~Yl8}^36u;m4#WXt~H>)GVpzMs%YC<$FO z7zdLl!vQE<Yz36o#ALc)pcHpu3~4>wIdvpQ$tl5Ca5A}u;qt+7v0yHF440hjFd$n# zN@<$sP|$A$O;t6^C%q)pC0_<@cmxBOSJq;VbIIAR5w`RVySw-vhBfDJ)~W%s2gV7% zt{|a8d^%G)J4I-qEn`A48_@Ttcmz>;%dHMEK{(v&jU%CcX(I)cN#E|T_)08yxnERX z6+W;Xx<ut(lVEhjhJ|TV69Lve6f(bPsO11%?ms@=@L%JFGLHvAL7Ns5g}wUlkf7ry z4-SH$WeGrKP6+APl%^9m0677qc!aR5e<uTP1&i}?aSM$nBh8UmsTF5V4PKGNK+4ud zr?G74CBGCX4VIFFWbN^T8`SJ}j9YEnE3%&gF3x(N#jZgGr$?*#;ENuS?bUFca~ZT& zBn7cPQ)xY1IN@@OgHH8ehK>_y&jjWB)BTRH?gQuU2=H|EcMo8{tCav;OpQ{nI_?d; zN#pmN@MXdt0D9fQOSQlbf>D!XAd?3JRe1Q$=t7Wkb#P;6%~%3l*}Z@N+`#J6H+&Kr z2l(^0XFg-@jo-|kJ&hFPE0#eiG3-b}72>ZAa*8j8S@M#lE6_-U^u=r}e>o=&j;`oc zhP)1kF$IOTifnt8RS*Xxg_H)4k-<WWm+|d<lM-Q*tS{^vt~7A0hzQX^d7U+LiuE}> zt6p>(65tv062NP3Us7Fvo{L7$wE#i@qksY4PhHi7Ygn}kKmKvBR6=i6@H!nFR=_yt zCyt=u`it^+pa7V%n3Y@-#ZSpR#s}({{QYR)?WS7rUm`3REksp-Sfh;!womGbfrw+s zNK;s;r08MAm_RprpKix2A6Hu}E*|nEqYu`;r|~YQ7au@PNW5}o&bG>aW~eNQG<}!~ z3Y<7Q2LBRt?4Q==q__`$mPYEU6G0z9Z=GvQK@xZ&<957H1JPK2Mla_>e$<)^noU() zOk(_-reYQt#06vya6%57A;G^89a1}_r}ad5J6qZkHGVgmu{{P0r#<%sA{7a2f2>#M z#(d8cL<J%}_gz_xgL4z)T09%9AyY#Bo<Un|_y+$m8nugtbXo=KaSkbn$Ls3yW<Hk) z)aCeuAD@Ft0W8$6q!|Y67^odz2>#j$no#78av2nd!}iJ$Fkc+E%cbv%ZO^8wY^P0@ zp))tPwgKrZT$jB=^H~!CT5`3^N&H)=3L8)%7-C@dJtdZXYDcJugiu-`wGjSZ<ce3P z!KAUt_ctj7HnK3QV%Si9uz>V=Iz>?!8aRc$EZ)vV(4ve8-^38jR&u+_B?xmUPw<Ax z3n2u{;4en^fJ(eGSwI*DSz+01RuIn4S(y?0UTcuPv9SelYFMC{xIJS0G)*juS{a2c z7gtXcnlEV$K|PG;n68Kc5s~_>k0j$y*6+7|Cr)2`2LhZJ1qnbhNYigbU7mw~IW7NG z(_00s%L925spK&0W3c!7d|`u9DiTb=bwbV?ZTtn~4rnEKv9!i<RP#f?s@lFl0Zkiz zqHtY)X43#K4|9iZ&Mt{JE`m1_ujL<f1SbnAG<s-s09_)phUsvAE-8VS+I+!fiiJ@$ z5%ifA>0WM_z!c1u#cHBeG0Z(v0K+(tg6N?CQDwo%vSAFZwqY$|J4EWOs~taq_Sl?M zc1w+*3qS^PA>7z4w20F0a0rcXHfx7SN2<HGLwOB|ZKb_0hHWkZMc^y;j^X9abXBTl zG2SE6FiKeFt&@;qDd$o<a}8ObAqW@GCk4e#n|T-(eYj3sRE_XPa{WGd%sPQKGX;Xa zv1D{t28Yk(A*Gq+b&JM}OYuMr=KBnm1)0;J1PCA8Nj&t<_Vwt?FA>lvQBgY~Qrx!C zta6Y>64cFPtAdk3s6^De3b}Qq$%_=Zs5cUEe<QATTKrrEb)1G9_jxCA&vP9S%6|%@ zOKK9T_7lE8cnL4OYYacHl99|J0hz`rFv+VhJI5p@H`JF{0)SDJ={(5q8i{R`Ozjhj zYCzKv$71DYFVfX%mO{=U#;nLJIGQ2rxwR(6u=Qj-p`E~_el22yW^iHfa<kme9r<+8 z&hd_i+i3~=ofL#G_;Sa~&C8r#)iXAg8KVn!NgS`4`!kg}`Od&u&WVy;2ahLFI1Crx zyP+N}16uWsY4t@NThK0ay_$Xoz{{$(7y_Et#VlpRH)g?VP8iu)5o%T^`zGS=lq4vQ z!OpU~QDVB2rpPrGORXt-?7uMy)Xd8CC`$60MKD0sOCD!1tdnorB^s%JQ7dQ1@i<;i z&r8wIA!}hvFO9zi&P-Xx!Gw?J#Kpkvz+YqIelzndMxM$>-eDia^r8z|7neB7egP<v z6XKZnpfGIJjILNbH_1!dEfXwi&gWX)eThWW%zzEI4uU49?4Dmo`+cU6?rca7Yaheu zUt*wqlBg$9Zn?ykrFeEM>$88G`fFxzn^jNBT_D+n9keBJ%6$r2nVBlj&G$4-xhfd) zZP=vBhPus<eK+y!2PoM@QPt)QD-u9OAEi|V-)L2t3bYkgW1QUbVP%*a-1MfoE~rb` z{Kx~q-2H~3T`#eUP4zQ@2W1z-HTAU8Wx%5;>7X-q={x71r=RzO8nGequji(K>e9t| zG|4XtT}dvVbOL@Wa!XAfxeb1~T4(lBC>cJf_JE4gQV>NJY)4PswD>__U;7B4^uf}) zREzJqf7{1JAKJ22{#HuYZS!fO%RWz!_;*27I--$9`tGMt)obexRO0r;=5T&xspS+9 zRqZt@z@UK_5v!I7(H*>DaHv#%hYM)Kz>Yy%7(-5~&D`K}16x~pTLr@HFMk*Ru+Qy? zCUjP%yy3C%zwC}zs(I3j6ia~s580CH;r)@})?%+)TxR{~a-K)E2UxrLTC?_Stb^K1 z_?8&W7d)`b`Bxr9$=w0D417`|L`Nf?&ocg%`JK<U@O;=q#5<pCi@1X)Secta9Ht0@ z?u}?$hG^7?S<7jmnMGzCD#dH@k`^X~I1<fU)VtC^F0?F56(*Z|&*gprbhhz}Iy4Al zEt&0Tdvw@_xr%R9JSFQ+)W;;ZY!5lF@k)3WBL*(raBB^ySlZ94JvF#=OChOjpA&@< zOsq0Nu;8`xK7q3Mn5&-Hsa_vfwR5OxkU%~H?F`U8$*Hl{j*zeQ*tmw*nvPfLeVTVV zY0I8Ehi`xjxx8H*LQ^OJ_@y-PsI*x!$WdmvUKZH2EtPKgv^#L=jm(E-K3R_oe{G6& zq(xuni2DB7(H;4M#8?vYpcFaKt{XVTjMaUHxaoOrEvlw-$X=sGvCgpO&2aY(H?HW~ z9GC#MkYYyABDLBTIP=miGq0KaW;Z3ox;lmCXDap33YNa_YisHYfIy9Skho{hSF-5& z+i9##etOMjyU+mxhh|?~{ebuuu8qb@8WY@daqox#58iOX3rPExS64R-ftg5C0%(98 z0vB9gqJ@{75R2a26@uQ|f&k;_@N1A4(u-$JLIgdNESHM`s`%Y6&vbXb?vtiM&54$; zralG))E<=Uc?)O#0E>&Vc~A3;n3F7{4VhXc+XH_#tea>Z2rYN4f|e?1Zj$1XIp2B7 zoq-oQLT|-<`6wzx*D~>oA0ec;{Gycx$?40xh<d%J8v8`9XD-03951;Cd@En>I?i5( zn(r1y=j=7wPf(8J^CiTk=%2C<F;H0!$5KQPu@4EaA%VOS0gx~$VpPb})Nx-GgdAt3 z1e&9;=9fN~GJ|fNjnrMk9;$nT*{G6v^|i@XR;l#Q(oevUJ{QJ6?!uppoLH#uD&Bh` z<)>g)2!&7moML$C?ug%Pp}hjvRerj`TH;b#wgL%m-f^}q8aJ3wS2x$L2B{m88O=_& zZj@F$vX1TU1dM`6SqqaN{J>H*884n>#OJ9xOm%PWge<+3LV~^<X|OcwcRh8g&RbB@ zfw@$6D7G1_ES|gOLN#cw31JKcX*!D{hN|UUv5x9I<U|FZK;&MSZy(OoC`apkvqYP3 zCP}JPip^R>URY+6ivCQ>>{XwEGn*;GqCu{SL0VN+gRiIZ8>(-)+D8#h+wlGkR7n;R zipMn$QXBpwKuf7WU;|a9<Y?GJcn16UkQ|8CaEW?_`RC+5?F-1C54#MqAfO=LW+_a} zByi%3RDWW=kNzX(12Cok6Z6ed0s&zG5doRG88e#N8N0fgyE2-Y7`nSz+qp70Svjez z!2?0kadBAw-To^i9Mmy<4+&8K59OJ#!Evfw%<cZN&26qZX103S5FWur$LQg7v&|gO zkjXMjz9XEg!>?EX{}ap(?BryssQSF~jr<@7I~xlwRy+iNpbg;Jw|UGb3X?U-TL4rs z(kUy_k09caUfv?;R>8NQc>RkAa14`4Ab>SNRtob{Kpqs2J2)+Mb<_!~dTW^45J5R& zAr|b1%aBkB?9;Kc2gnH<<d$q;0})zs#Eo82CKw}hICTVo3%+kKn{HbxA1pRRk2x`R zIM||$P|$Fzcbd+4J!YGtK=Ko!=BH^CVPhc)Qj<ZDOj_O&XK4YsI7FwcB4b69R8S*g zfuqYFE^+wUXztiv;F8?fTYl+$S1HyxUNO&QB=33^eg|-aK}?}cs_JUCek~0vxME+p zVyHtnmtG8jHye>TT6n_VnkV78gR$Eyv_L#X%Iw`)xhh(EHCjPEWSM-JGe(#dHtDVb z$?|b)_M6G|J$_Z}W^C<HOTmnPE8doYp<KDslQ9TsE4IADu;axS3DnlMoOQoU13XYU z?6$#?p74E!g=ZrWP>N0_6!DP)CuwjWOz^MQ1Xv7!n(02s+euKt$)&Z7xL7acY&!K! zim>A)FJNgJNTQj}0=Y_l0RaNW6um{AlEuNx)7E9&F0a`T%*0t2s8JNUpCh_^AO|e5 zB65j@#!1Cc(yyQ~Z=1U9CkD`1Ye3tXnSIeKA_y7UZr_rfOF}x@f+I9jUDUD1;H0me z+LEyVJc>IsmT&wq*iT{Th$4r;vSem_uVGIpC+jhz!^X84uq~dy`v48x?bFFd5_M7Y z4yzG~{8OLn?5|s2wa6CGa-;Z?r;`Swc31XJ^xc24#b>QXmGd|8Ax$(TTwWL;x+2=B z|F{7u`_5As%=zF1@hgNk&S#}A3A?Hr8<Efn0Q~h4g&4S{Z*kDh=i%+u>KfXhKYzTg z@2+$H@G~m<_Xl~Nw#-fa4UehHhHzB}rpx>Omj}${qvExzh?5@>vK|cL)+!e)#Lg=b z;`xuaSm|;c>=v;3Lk%P&0g>cwwU#2-*btyfqKl2jPF^4jK1;1CMgrqhd3%SOf&kVP zKzELToWeNeixx5;923yp_AwpYZNCbLZqI_@m6D8?H%Kv5iXXBiY;*LrSrq?t&W&Gh z@)(JD52!H3^cG$oy6}#Y;a4kXmtDCXAb|{uV^<|{ZrBh|FaIT|Zw&Saor%q^D$)S% z)A);FR6A#30{pmX<Et}T##}BdNbD3mfbH;#;Edt`Foc`XVnr@BoIiFC@Uy2Dy-;i5 z+g5CwsO@O1ZAPZ$(2-r3d=!d6U>N2O1Kg#MdY1^w^U6TFm)g#!T%+Pc;Ylb<mLusl zy2S~_Nh>$(<BJ^FokeKB7yl`*p>BVDrpK_^iKA9&l%3Hn8F;>o%%j>u!zC~uKqp{& z$;SUSll0Kj8qDkxry&D~7(N%y6q0ecNBcBWTD&*Fd>s+I__HdRPjGRAiJt@_jxc)A zfdD~h>VmSiDH16Lln8~B#DR=IM`zI&7$#eN+Q4krJDQ5DQWvJOoUF?F{t@~|%PDca zRNE~{q|p5AFusMm>TfNkHrVSA08>q<@82h?4&Jjqa&;$AF>R0zvq&A~5fR;;X_aF- zbGY@Y79$DztIXK-vME2U*f0e=Xo)MfOrTdQsZ*pfHKe<Zr5OOEsoRgIrmj;YW$kxM zjgvGxQ*c!a3(E?WBMH>p=Xx~c8?8-W-_BD@?KA?EAhaxP<lhA9#T+{<0Pm~XM`9tu zz0rtoc9j01=zHFvglF~=ttR<p1mUS4K_2^h=yL6j`7+Bqr6d;B_~vyM&Nyau!}o&n zB`g2^%31WYGR~*_#co3%*D{yjIF48(u`!l=M}SdVD$JCJZ7c^dTzWhZs1sY?&}KYW z2r1C$92r#Iku{wN_mNf|K%#*u&??K9xlsvNO%Mn$ik8$qT!(5Z>G9*cB)OVkySseq zk%|qLhLF${i(96j&~gT`bJ3U{e!6|I5QmjLQQ^;RJK;~yq`VY&V9v_8D7?bS!qyhA zj9CN6qrgO(G6UXvc?Cn62CkB(U!4<xzfFdJ)$Hf>)xDfDfWJ7k2lVoCa1?Kc4Mpez z;{@&hjw#5zYk|W53h8z5Mm3e-@trv_l%EHq#nC-P<ndT8oLN-WSpvGECiiZlv;r24 z@Z}+SenU>zFlK@VI|D!IMF7<#D1X;%;SOuoWPKAMChc+}-ECE#FtYE$Bvnl7>tbOg z2HWLRjH|nxD^0ot72xM7c$Mujjw~iF#>`NJhCYha{<qbqI62$8m$-Dt{rIT~d66gx zZ*DW--t=^nj^=orl@eoEM<iCu>~vK@9xzM;tb-M!Wo>GQa0|9(ZCF$qHjcWxTdNjR z4Po31odaD(6`TX-<b7t48Wu$xyD~`L+ae}ocY2)F&ncxObb!lr9nqUL8Y%Lv&)=sF z*&t^!$ir4tOVx814h|v%6n>v^<%Y_svz)-`mjSNbstA5OZ#hmV<r*MT=MSIXRAHIs zmiu9pjt2}m=VM=R#=#3+=i>8dU3vk-Ylo6*arfe^)WST1!|h;)u)%Ei#<;9sxJvkD zj#MtDr?G(q!UHgYAv@O78-;ERJtzwkZAfD^1dB4=B;L%lrV-D)cUFn#%Hf^mdCP}% zJezy+&hs~@FlImNLAG@F>^m)?mFRjiQY%97uuqk8R*p7WyUd5rN*s7B@s8Y5RT!JQ z-{|&kU9}aa#Ga4fx|X`OqdPSGXkVGtwpX_%U9tUMMgyRK{f=`I@=a$40YmdHu#=gE zj6pc#C_e<({tOfUtWV}sR(}#1TSbay<FLoK#t0GfLTBH4cUX4W8iF;{uTo#*VbR)U zRBP3H%47D1(Oosol1;=LVe9A`C$okTqGS(0=N!tgty)6rqunA%rqG0h!C004#L!h& z6Jk+sxoZS;GdZ3$2Avl%o+EpZWsQZFcqV<S$U+XZeBmdhd&C>7+?4)QA7A|G$F}2( zyq9!Rj;DMF($ub8t@70G-K4JG1N<rX4(TnTwz8HIm^TbZex290L2=J=32@$(I_sva zu_c2871!CiII+(uwu^9Vqu(jI^Wds2YI5rBp+3t4>TNi9k9EvvnjV+#Im8WI=&K<b z)xx?XyU-D<LB9>&EI^-c>rctJTz;`kQ<S$H{$BZG#Y{o?73bhYscI!xnfl2fBAC2g zxQfJ6e_dgGRctC!*Ud2|dGHL!uXc?#XVsp}gMyzUik8JJsS|-cW)QnH`iFZ>zWj-u z#&Tja0H)2z?Y4jP7zV|(>ZxNS#vRRnnSb+la`D2GG<gdw`)WYLgWp8~rZ8=^zTVl$ zwc&y7)$6GH%VJa)UbzeIF;DU50^*1-U-U1|@IB3Q%4wGh*m4>#E5UhRBfX_Au0M<5 zYXP)V8AZWwuEm6FsZ79K)}dNuuCh3oNngB|09(jVXF6-r1mYY+t4&k+mCMiFSg`>~ z<K>$Gpbxm~(UD`y3j+l7fldFaU_b#u0g1-^gIf9DiyFlLjauD|4NZ(qZOt9b{tLbq zka1Z4-QM9V;NMFcgPQh^-=&b=rfM@&ia?Nq^n5!JOQgq3OLkI$KqS={G)7izwH8mG zJ(iUg?z^`pzQ7;S%dhzg+d$uc<72&<m^hEg7-geQ!-2mn(-R#qg?P++91D!y))+%7 z$n9CkELpXhIK60BOM&<?(=8X8if&p6FgL^KGLhFBLjy4kG$CG=xN^2pnh0d<3s+-G zAx=m)11=~fF@u>%4KlVV8)bj>n(qwZlw&#=o>3fTM%)gEl(T{m2ri-Y856<MeMhF^ zSmD?Zt;tp^oO;nXOz{`!6IpYQxcLVHEFtWP(XncNq9f=dp|@2~p3rrUOI3G(jge*G z@SZSs-ZW?kR&iUb&ER3?SnQH)ace!&cVOCrf6?xFBwX!_=%^baTz#@p@ab!ojNka- zMAmKt*`XD-s7e=;teC?_3uMM}#oTkT>-^O@vY?*(HwRy?(CSSfof${?Z&L>w^0q*) z9m3t5cJG&qHDYGD6!IX|C7J*jjl@oyc1AxD#8e?0;PovOu+lm7JmU2}A&r<lo%JOl z3#OZx>`k~tw(=lDS=$sMZ^gvg#|#z;AXk>^o-VlW6~(*p3o!IL2h|6mMXFkz_?<!$ zX7%{h)mhlYN1yu}5y^-PnctbldzVm2?=&q3lPOT@6{@rRq7g=Y3k3!Q_|-g#9lP=E zWseEM#5tO?h><lwGMbZGBw5QPw+gT~tQ?d8KyNJ7Xk@7y=c&>(u6{XVdD*mv;SEF{ z{T8DBg*|_qc;@b|K@&!|8yuE)ZD0HFT*m*?t@FNct({tPXB_hsNu#nTbr7+Fd8~@9 zXoSs=Aq}cX9?U;}M`a3tLY>JZ^hLAD#l6Z>1^y*YdJclYx8>EKW#dj_W62(O>eDww z>nFS6`M@l-n%0oEcHH%SvCV$+T)tzb-4wN&wGBi|y&1Hx^AOs0aG0ohd!d?RJiytg zBd_1)RHJ%a3vijqqo<jlDgrS=(4CF@CTQGCGOJou><>Kzd2t^B++(4?nAG4@102}# zFQYA-7wjA;K0_SH9pDJ3Rcq%T_mft9)iy~_qpx1H=_U2wgydq|G$p->gycY<q)?Tq zg7Nkwq7@e;O~p5E9$R?oD6E1aZFzNvr;<Iyz@dD0FYgjXb6%W0IgB=2q1+pi%pRCS zJR=!RPb8eFXR4P11b@q{&fZPGwY=P%g^OQ*h^1Q7`E6|_g<UoobrbI!I7BFrvKHRh zdm2Ux`(xw{kO4}IwNkdAHHO8P#!{7%XmkV50;LkR#36kkam7*2mK4Ru1xZTk8m$f^ zyog6ZmUH)rw5Uh@pmKK#sg(w9!!*?C)|19!*2*NFDHTr*XlVfHAN{FgNGc8m<%sKq zKNj~?8>i+ZYID#4Kkq+?!Xvjr@*|k}0qCrAO-Pr5m*9I_pIzF{ufBttX6nW%TS_;P z6G}?VR31IcAxz+@$rM<EFN=b9ef@)#D~8lzC)c5N$kuqv*Nx%<u{YqA6XH_xQ4MIC zJP5VV64b2(fFvCbT$IyEn9}O>VQqdj4BKucOr!k^Wpug)=W@ekQ(jx$?~+|GWnSxr zR@b9)02W<``w;x>+VrMjdjH*p*=Kr0<5q}e@4O3`HK)F~RL7QkCrW{rR-_VsqBG#8 zI+-q|+3jiL+MElZY70C<Hym7fjvBP~`u?3+)=yt~0EkneCI(BSy9cBgbKR`uRCAJw zQ6QsTjv82o0K3$yWr<&c+*~8a046q<H$0nLQQyXdB8LP8t_}J3$4CAne|8p{(JKp) zGS0g{CAXvD30w9=7e0A*km5DERVIAKMc}(fI{8LedbQg9kC+{8^|g8fUjsqS4FYK2 zcFROVfXNVxo=V0s8&t&#BIi^P>i7KE^Akvtdqyd|!_YHo?%#Qe>lbE^NgWM37HN#k zA)Q^EIE|eh!pr^R2bWd+Uq63h)*~BG-fm@B0a0|)h4*Tt*=j@OYg>u?+I+3QaT+vE z4J+^+jTt9JTiyokb<BZC)HmjS=SR6>wYqSG4d4@|96C-!jMNunt}aOzUp=ilPxJQk z*b$E>P`%v#`FT~ZYq0xoe!$ZTiirC@=&!v=BuSpCyq?zgjA;31HiI&&9)*WJg7pp9 zn<Z?UYfC|4cDV`^ZXBDQP-R*3jT=WAV9nqg;+KByJ@I{@Zd1!taXDY3PuJti^PE}` z?*T6jcWa=lK3zFA!+9@xS(&Cq68C3TWt0hAPlShEyta4<drzRozur`hTVZ|`8~<WT zSMx)2A?|{=U?e^^p1PL&G`ZBjJh%1XD{~3!;A<g(fId9pKfW?B`yXE^Mgasw{_nmL z@SfuFur~KJbagkew{~^4c69hJhp8;cVflCauY6Av&bZ5(#Pw9|@z>l$wa3Ji9IMe! zW}_;D>1rhU)V0MTK|P6?EKr4kK~^-Sw&%LO8&#dBoTt`~Zz+DG9@B6Hpn-tYT`%uX z-18R?jlXuy_r+^^4C=`0@gY2tFU=E6-fQ=Ou%H7N-*ucgqr)pOjCISvtt5O{D9lR^ zIDJ0sKuR$;CY+l63u7!s$w*NvoM0!@AtGo@6Z_#)AP!@gczmG=z5K|`Y1JSjkkcOg zFy_x98H^laz|(U*=FCtDK}LWp8se$jy}JWmuSpP$H37YH*8<+la9%noDfq}^va3v3 zSLq0*DXH+vbl@owGCSCXdc@d0e>hJ%ikh9_*a(W12qp>=eci-;=1}CFJEe8_$7U*} z2=+nPSH10^lAmG7KX-|l7aS)22@m1sLg`hy6-S`py7eZeYBwj1T%G^~0a@$oatxLi z!%%eJkQ`U>RZAXeuDMkt;!JpvaMASY1v|O9B~wSQ26JLoebES~7%Vc%=c#k`H+sOn zP;xfJq<A_~a5}x7A3odo@PK8qpu`{t;ScFc{VoL7r?5Mx%o@9xErpgj>eycOgwsD^ z3W%s*-42~SbZ$XLUPA<gl&Y!bAPu%_2_Z!bkWp>I;@h0u`Lx2RVB2<yC`w57YuGln zEC=&Tfj@9(_g^uWl#bL|pi+V0k_hU%M0b<jfpb$|pTV1;Tq|b65}uj6@<;(qq5$FS z6WjYYBNdzPBl6c>r%f$8Zl6nDhr(fzvD%bMvnCjBw29-8?coE6`kD`^C}GznjhXW@ z3#8CWFAIX2lHHo>_a;@HbDf$@cp8Rk(yx2)Ep4kq$;rdFbm-ZLr3z*(5##q}JYM0= zL*y_8^<2so(IIWo4rloZQaH2noF;gN8$!r&ZBCPZdGrw>JGtE5KmWYGb+OP_*v#)E zZp<NbOt3|vGq(f4q#Lc9aG93QKZCZZh?@5ZO7|)$0Z*g(tLZyoMia^xh9N=ez{ns6 zqq3UCe>25Ec2(mjM`BGH0#@ShC8LfiMoUoRAP14Z=pli}a}oZcbSN2v)B%Gk$iDh= zsIINyJ<mHrHglc=f!B+T8V`Bw>6=L?IBw?&r7cB>N0$Zo>Y!#N-y~(}+*)ry{BvDF zYZ+}A?Gq<M<oLSCw1vS!aPCT1ic0b)Yy1e~+oWgzZ8gcSIeu_`?)gt<^tbHcXhZe` z{@BeuyIirTqKMK_P{_j>XCiQ9Ym>Klc-GgHD|%urkP#7I5x4vy!G)qGshz1s?`KMs zp4C={7V6po<QC63FA@^wYP%(D3!MCLg-0(h{*S;i6n;!_4?2?3a5ct*Q~jbVgI-W) z${jy(2Yv|1yUt6%OPpedduyxC(?El4a0Flv_0K!{+J`{+BgknUs;EX*5nVJh^NE?D z<Fr(wKE;H*m!71i7!R3a<HxS7B!)Rq@z@``9PaD^E25&CD5<5pkJ2cdp#xJR0ofzy zWU4Cu)}wg8REP_VlMEnB*ih-5xF~LjzbYmr@K>%8w7kR_%PP?CrH;#ZN%lwmW=qCt z4Kff``b?r<R!?eUykUt4xu)pVpa@piv?GXbUD2-4$!v9r3B&R7`ucEJBs}7@Vw8ml zbL3$MAVGra<)qq-4QkpIk$2@jg^*OT%Zisu>I;7+#L-dMLitj0%(fdcQ7N#(ARnQf z&@=AE%7#@5mHO-#g0b$XnKpT*E3NZRC<3N_TbikwwE;CY{xsmyb}6d6zS@-@7;Wn; z24`j$K+F7-xT>#_+9)SFHeRmU?uF6}%$lkR;7G&oqxYEhsC|;Tx8UuxeckaTwGh!B zFR;wpcCn_lhBXLmsiDyhmdk!NGgtmq;MKxO!1u+12^!_xE7(9;c-PD8?#YlV((AH? zt<D-lukc~sXf>js$>RZTORsj}CMe(2sXZEF;s!Z#2bHFvFUPW4`pwkostU$BRwK*< z0JR!1c=UTnCju*8nKBhe*PjH7f=PhKq(z;up(L6z^U1{oU^k?)L>;H4J5>E_rH)(8 zG)i_6)6<~LN8r?J?>Ap`pgluFQppBH41=Kj$kuq|+&)tHM#!o)bZ*9j|4R;4fQ4ML zDiG=CpseE@d{^KqNEl;@1cJa#=9!8oKxYj{3u6`0{z=cOe<ulU8apUob|%MZW64}f z*$c%1`^4F5P5^RwW3=ZdQ3=Z$ulpLdB}e*G2e?<21M4<;xRi`*0O@5``(&W(nV!cD zy%uHKYN6_*m1AvkBvz*@oOk-50PO8ega#g_0i5gi+yTERYmp>GD;$AxaX5}h!1V7j z_t@jK5ye9(<UBz4VdifvXx4W;o1@9sfL5nY#*zND?w7H8pN;0-XsU;b^GmblQYjUB z8x8^DxvJc`xptC^G&M_i1(Zd6=JZlYI&GS-t?=mXTb9`;jr%yYHr<0IT2J=MeYApr zOy=j+7>AiYi)l6NsV>4VS+?X*09$&l8-46$d}N9BAGe?S1?pCIpc|~ZB_SCM>9_>q z2Eqf#6(W^2ex6c@XADQ@=SkTx^k3-Sl&WL0%5Oj0@Zc_tC(59TWvY^koG|6O(t*6q zC=chY5y0Q4vF6S;RhXtAz^q6?O`oLh6DN__6DWhc@OsAhr12&|rhm552h>P(&q8)S z^QBYk&+i#yf}*vJ1^La%8DfjtY9=zCSd)nrr7~1z&MDKgy?7_|6G=KB^6N|rje=$S z%F{HOj8U*@d=Dw!V*AzK3VfUycnG0n549hKUP5cLsxRLrsW|lg%6XD684+EX_fVSZ z$b|ko(Vijp_rRx$T~oEM+5l5WC#Kgt^;GD^X{r0F5F`K3^s2UAFY}U$Brgf5<EBE8 zy$CiCN0<YT<Z$_>4t#(je2*DomJpge5&~a~L;trUJl@2;K|f-(D28Dxe<O}HNcQ;n z2hydoIiiiq0Y}SmIQ^vk+$ZE1NXwO*13u4!FV}UL4F?PptQHRQK>!?S84?Ux4zOb7 zJ{3>|2$>`yh2!#Sp>$I?v`*J_!Gc_M;2Px{zl+1B1EjHqMUGMo6h?P+#-D}4y7o4U zO3FCjXpFpD7gssT^Gl*PPoz+HQ@)aBu*$UOCB?;aD}*J!aPe1UqD(KI-S-ythftwo zff3pSyAM#9;N!6K)C2TM)$k=e6u?DsWIg1aSbmTjExRQ(^Jl#!Z2e}S|2=&1`}h{e zN=g)c0wOyH*lP`US<Y=rA5-)?m`z>{M_pIPT$U7>?hDde)fHM3C6;5un$v+$)sJtY zZ}_GM%j$s=ksCOve7`G}r~16oo;^NGs=95TD;W~`O=@xh@2LTZ)JuKm8bc5sL^PxD z!=e(4^H&~qQ1#srytZ$EhP$hr$q99Kur)Rz)V>&JF5;)H(YUm1tL37%l0Q{@*sL3k zK!#je!AeRUHVCKX)N3GGluqYU5cXTuRUf4iRHkj@Ig3lIXuNbhQ8|20`C+$aQ~n@s zR}~VFmFN5bZr1`>-CWrl(#_btlhm(lXmsqBGu*>h#VBcgY=_Wr?HHBKKQn5ePtAtI zQDw^L@z9a~!q)twZqV3E+jw>@8KdAg>K@sxmBoapr|On9>)KV}eR$#TcxCSDN%mEV za*_FOYbfz5>?v>CQf1oR-)<{4+8Ie~OXdwJ9P>?u-?Z2Po7+$+)xrpTXa4?Y(Z8Oa z8_m1aMmc8ElA6Mtk|;+yw;CbY-kMjj>gC;j&``<?(yY#Yb!}g!ZL(?Dywb9ky-wpx z#r+j~mi}#e{*`zuME-EK44-}i#cS{)ntVC=liibtfP6YtLmgAVZUNYBcvqgbXp1Q{ zufr#%M{lcFM8pr^e%>y*%>WAoBu0?}p}-INPRdOI_QyzZWQIyHd;g9=&e2e!pa;t} z?`8Hcg6;=V7xmwv{5z7jcQiA%b7lNfr@dJCtJ3??|Ect5rTh~Iz+nIZQUA}A-;2M7 zF6OT8c5d%6fxW%4i}$}uzpAVpmVdYZ3Iz_dwj6P|kphltGb5yk?QvWtb>~Wk4<vI` zVW1+bDl~~30Jc4oUpnhMi53n4-PmhiVP&Nht#4eU=uf>4a~y|QO&vEorzx~+W&_{E zu&|;+K?5x1bd^XSOVnbMIhpD>cpcbO9{dV#TZK7p87mU&4)J1fR<Y-QVI9&$L`u)_ zVGP+YPtaFs8?iW0!B<8PS}8zLjhj!fuRueNyW5Pg1Ni-{Hu@kL+AN^mu8m1ZT9J`6 z!a+ro$(T_I`e$|$2hp96!4aHFkutSE?F%>so#2$|;rfLaha~{DBFdmj<!PII5jHn# ziEukgWYWzfP$Ojoi4~TD<W<IoU=?PAbEHTH3&(Tmm-Mbz9IAjTsC^+^Yw@>gKHqk2 zShH__26$3d*ipi0KBf9nC0RrXnZZf3S|5$9w{@T91H@7e4VO8WUwQWtHJ4wP%xfVv z&luP~&7a=aPCGhg>mRRs@SbfR01G9c(klSz*7LgHhGNIInUgS&XI?m|!q3T?WPYU{ zo{VeFB;kfg8aq^U#0&Z=Hd+_Mx{u_opCClj0YHR!>V#e7$eBbKV(~mk_i~-*;;mT3 zsWn*KM%R;}Wx`kPi*-(Ae3oZ=VnQD}b57qk6$ZDu9@rJhX5p6<t$$@To#n@M?Lo|r zsiT8~tTR`L5ScUKs{?XG+#)cR(&?3-D)!C4=1vD&V!_H8*3%hjd>$f=L7!0Bn*g<R z0n`zbfG9qWa13d?Fx6S_HY~LT{Xn)aqvaW}Ua(0nT_-Z`O8P0dBE!c_*`zZeDRSM~ z^-{Uf)!K%q*YcdPp>bp3i5T#!hvwj}Gv&Aby<eb|&9*3ZsM5CYJ710g|5ms_wX80P z{!}XnyM=DD@MMn>EVat2C`~O2CkBdE4`7{Z03w?W(pR(1#$AEVGFdKiKtYDQHo|Mu zdNIK%v2$X^N}Np()y1l?kVsyEy`d{Vc-afxBi$1-0HqbaM`F?sW{IQ7y1olkXazJC zx|3u5xnHn%XkU3CuGiZl$-iVSR+oXa7ydR~b`;MF{ir`0B!*-HyI1P~Emn3T7NFXc z`CH<}nLrV&u#7Fg>U7QEZ7oW0_c!tq+sn~cR2b;;N@vK%g6XvrS~`s&Mz7G6{?@2h z&)N;06L-JHw)As9cOFqXcm@9PJG2~BMNc`@<`|c1V*@t<51?s13GES5Fs<<JSV%`q zI{ov0&SJC2GpW$6q-3n@k^;>1Cji4KeYg_PH5l!QSH@0So4Pz)di!9lG6U)I*E-qZ zz{iE^{rh;%X(T8@8P_r^qdX98cs{*OIOOJ@N=76F;p<fhE1boXF~_NBW;Y!#x+u<7 zwZoz4AEfr(Fm<J?nmsU`0zdN@Zx8Wn))%i|eOER%2CnKX^n?**bU$$#F#>!J(O@~T z@d-2Ty^~y*_3x6ajZ`=5l<Z*j2nYL@%C6;O?0>@eHfaJ}J;S_(g`W%ZHBT$)qQF^B z@DG-AU~9}nYMPTURQb$X?q+QQmbr~(y=P@Gwbv^c4P!`pZ_*pAK<-qSyk53?bwlzm z((LkHQ^~n&+0@&zV465BcK|q|541GwkZq~WQtcXb=BA!?TCDE%<#5Y(X43w}0_3fB z+Xb_`m&hT=OW5h*d6wo4L}w{(upNcr(!*|=lN|<K;70CqXt|hu_MaqZMpdbA+#(<P zRdn)N4+eGhD7{{5uu?9q=jE_>##+=MVP_?VGPf8FrPqMb3K7$yf&g0y@-bgjujWaR zYIA?~&`~68y7tpSp;pX2^%CkEO<zqe>m#Y59q8;8?pri@zhbxAeV?g7UR|tXhTebi zAq#LHRtTj|7_kgJGyst{r8{<C*1Vq{I-$(#sP*m7f7#lYximP<d$aRc@7$EvsVNJl zDD7X*W-DiQA__&W*90I{?K?wq8;{~SWINDmyQ3=9Dsq0?*duPjgDQvRw6%d%tTn7n zf9_Am5x+ZfB*WhzaRxX6e`%UyRP7*dnL4>AL5SM?<|)}-lzSBbVyx382Qq@4P=I{D zW{BkB{Gj^{yHjXCY~E7r_b_8*zVygJr&_~-p_~E9ukCB>Izj-0DWKvN_QTx~M2H>X zz2~VuywE>+D&K#r={&>zziT@G$GQFEpZ?{SV8}Tv|8D>7mo(mYsc@hLEY||K5W!Eu zvNk9j6&lpt^Oh<|aqDA=dVoUGsiI)TVaUw;ZJ)P1rKCODy_%TK6j2Ld8IO3k+M+K2 zv^R4n55!|x;i=9rj|@>PM5?VREWKHVM4V-Kw=ZG14<%#I8bh0t7_^_M!0A;Rh@%mV zXlWy(fW%SD;!LW?wmCq>$ov77g^AFzEf~N@G)YR}6y-)0G>z)^kOd*&er3Tlea0F! zTrtLdCNyl|!DMH#Q`R7~5vJr0^by|uzz*afsQo3aKx0n&z1cKzrmZ&aY=M|WY2ZIl zOtWYcOVfBo+jX}GPQ`s0>9=q*flv`RGRSdKNO7`TjC1>=MM|x8u3!PQu{k>dc>Y-o zB$pwOtq=V(h(!^$>vk?t)o${(R2r$d#4On*kxaT~=L-paJD6x!g7K7l$G}hhXIwB6 zH{9zAqL;>p5wl*cnUBO17&~@6gm*xP3v)#n+mxH~`hhbc(CnekoefXGAyr8PibRyQ z(Y<lhx}@A7b#yJmpq2n(s0<QU4-Nq_gFUoRHq64Ltrv<ljvRSL)HGR6gP4=ITK?&i zf&yPB1X5$HcG5#JTgfNY{g5vj;PVVa<h3jos7jjag@I{LO39B&W@wip%;`~`JuJ)v zXLV&%=?4(o*JlUl*+pb>G&q8D!4AymOcBC4G@tr}D)$N5I5GikB{K@&c5Kw~z77Pt zW@iUzTpKvR82H+91l^^<7s+xA{P4{u0$-HMBq(|2O-DL-{(gg!%_5xQaOA-A%7#rp zEX)Q23Yg;#DW{xJ_|D()w&hjB0Vhpwi@Ke2q^)Gx)j73OoJ_W(tDEq}sBJ>a&$xqt z2aoniF}FGYlNc*tAOA?qtZ`ZDVS_ha1Q$!;!eZVCO>DfU8@CtU4Z*wI^)<LRf?e4t z2LhunXQ++iJCiNm8H7ALcGYCU_wn|!d|86ooo~BBm>|4!)|b4WNVgH_xuCBpt>ET& z<2bOSGCRu8KD%3(&cT20bTmfxgD-m!=Dk8VjM+hYG2nFuK)1Yk)k8_14Wz*?c<oA> z#%f?G^oOkHo|Bf9tsu~fPhzb3nh`FUG-70f8&M~s;Cjeu;Qgj)q^euNv9<;X<Cgs} z-s0;Qhzsij8;piOSw3N{yh?Bqt{qH}SY=>uDNq7*ljL-U1EzAz^^0T;&_&b+VQ4*% zA!cF|Rp?>>bNS8&L5r^9l$X1i!|(~v0dJhS_c97bYBH0j*pL=y!zansSB5Ikm6fS8 zU(_4(B8Bvy&$Wy&Su7BR;&SXbhg3C2N1N4~8^)W?jy6bF+Lm@(+cqo?)LJka`K!}O zI|5us2s$7Qt;tP}KF8uvTEHKlHx<C^TspGD7#t4(9Qw}=AvW{)b?Y_zp@B1+-YFWg zlJBwHu$nC&tH<eRWO+z$0s!w<O$*T$@HEJ+V-<s<(xRBoZ&`Fr%8H7VG5moHHR?(Q zO%JpVlmronfjMLz;HBz<t<LU0m1?io5L^Q=AfON1^^aA%Ncumh8}N?TzrPo<|5w;; z>S%BOUb8lKd(XrF%e=8NaajJ{{>!`#YOFb|aUuCHYfgXw+dxJ&S#2g$XglsT8niYW z^HK#jLs?my)s;#Gm7D_DzyDh8I!kbu8eOh%Ccw}FPNH_6%3{Tgzdqzqydi=!PIdu> z#)P`fVYL?~zZ9;_+{_iRu@4{!&jL75^TXl~y{c0nd`9lA7+}a8a@DE=A)yd9orqiX z)6rsS=rO~ifzOKeu6KjG{gTLR^OZVSJP-TmJO8(ntzO>LimX6@F*7<}FhPP64wwYV z25yQ8oa*!*Qj0==>l{!^bY#|nOg@my_hlwY`(x}W4FQ@yRHg4pY?<z3>=o-f*<_y3 z!K?6%cWl@4fy_=ntX0!gnW*cOy<i!@<d9)Pq=^;qGGlj4J+A|tID<TX69_a*7JC3( z0K!8Y>Wwk3bd_F!bXbK6v}B7p2I)4>E$mKz#l-u94FgXX#pj)7-XESrrmgs~h4B6N zR&)No%eDzdUYs}xLwOiISb21AVAE!tSKvi(Lsr<B7Wg;(=!=Pb$f9G1QiE}c)6#e$ zQU~MxVXkCyRNObt8r)?18(<~%ltxlpIaO;IG2*rquv-p*qR^bnDs-@&D`%kLnWS`8 z9+KYJt7JzCd!|9Qy`g0;%)!_Wh*-6B<3(sNw-rGb41ZHruy&{^OBYbTNctNN2C_-Z zN^q~&mlIYPY1(6SEdPGEjEYX>(h20nV-GYCRNN_-Ss8nZv2{3KCP7#7n;LOGV<f5c zPsG|<w8yLfd@Qs>5-1b7)`8ZmpZ!Ko^Cx_6FDX+MTj!lNeF=mMF)LOEzZ|)r6;)NL zC=?Mpp0|CGxLgQ}J4TLjt)0pfc}_;Ea(NtfFVrQ^Q5tEqjsZ9IqOvRR6qQHLpOLtJ zPK&`^9qI%cym#xot)K!h^xF;#zSzvzzk}mtVI|oBW~c<{!9Lp-4QOn71LKdJVAE~P zNjVs%fb#6hNLEg`S8RawGykwEB_~+itXFpTP{<oW#wSZ%s~>`(n^rRVvK?$Px4Ae; z0wQew(xpY(2oB-T%<iVERxOnax3m&Vp9g9}N)m|2mj<6~Ml9RzZK~W$$mC^yr29TE z@s&;iWQ1P|<J<ADWhs!!!eYN!pSs>`vz_51&wJ6ddK+;6HgK7DFR|XI^3-7LZAguc zvsY739`oaT$hiD5z9azu@agTLV8CCYd}j0-OjUUV+kf;>ru_CtdD;$(0jU>A+!oXL zS$*A-i%-*0+3I@oDFcq#ND*3oZ|iTQ`~C9<z-N*oDl!I}!&uxB_rUpTmp;97UoCq* z!w|aAM|2<0$c3-1&`yE<Sqns(3jr+ng)7<c>~gvtkX~c`hN8+2pJ$sc&$9iWRq9TC zeBd~$@9KHvU(m>}nZ4iwKt51aW;V?-(7XNlFfxDHpN|y5s@0p;M-P;244HzYg9iSw z<5xDt$Or9DdO=)=@efJdQ^yDN&xc$6T@Z+mF*p$^#y+Si7e>tgHAUE1_|M2N<13hy z3wNeJUr0P2K|tU46{p~PQ~l@0Vr6gKKO2kxjkoUOO%3<=uHuhA<$X-bm^T6b(>t&I zgM4mhZSpVcXO$Rl{?2W;lz-QmAcfS20RI-{uTd)E!ea8`4EAP!@*V9sDj(yL1Bp@s zd<a0EXj1ZhaKS%jD*l<d{&A8%<(CikzfR($6b68Sy;I}0Q+@~F5W6t_CHHUY`ya9Y zL70EnQ}%-}|L>%v55oN4M`O^p|B~^)(&s<!TkV6&^zVh+4=U5&M`Msa?|<s_fqwt* z#HJ6yS|4@ae-cZ7)qVdF>?`u0g8%;)f!~E=e*ADL-F^hXcqu~um~el7seABOi@!2g z0>BRMr|CZORw<7DL=yi~+UjpjUWH+F{!^3x&m`9SP3e6kvHm@L`0%EGAB{n}|Ffun zLWch}rS<1=d_*Dt?#ci2ahx0dkGlSIfq(sN|BgpK`VYZ+A3)&Wm*{_L-eLZ~2>!pK zf`2`d1b+f-7Ms6BnVI~R2$UVI_;@aV7Ck}AzCQt|rwvewmJS}&$Kt@JL-=d`i3Jdl z-P-?as=Je=i?NyczwT%KTKUg1{4gCK>xKkx*XggpcE%2te_aVD;;$?HF&)@{{v!$g zNGY%Y1OjU3e;aJ>@IPs$4j=$=bxv^yF#b{O+fTT^6bla|Ao=X~*K`kamk)}<zqI<e d;>TV5WvtM^--AjZAk6pI9@IN!@B?xI{Xgk=StI}e delta 14529 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 QV1kT8gMc9U{2A>30}dhNegFUf diff --git a/dta/classes/backend.php b/dta/classes/backend.php deleted file mode 100644 index d0bc5bc..0000000 --- a/dta/classes/backend.php +++ /dev/null @@ -1,144 +0,0 @@ -<?php -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see <http://www.gnu.org/licenses/>. - -/** - * This file contains the backend webservice contact functionality for the DTA plugin - * - * @package assignsubmission_dta - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright Gero Lueckemeyer and student project teams - */ - -/** - * backend webservice contact utility class - * - * @package assignsubmission_dta - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright Gero Lueckemeyer and student project teams - */ -class DtaBackendUtils { - - /** - * Returns the base url of the backend webservice as configured in the administration settings. - * @return string backend host base url - */ - private static function getbackendbaseurl(): string { - $backendaddress = get_config(assign_submission_dta::COMPONENT_NAME, "backendHost"); - - if (empty($backendaddress)) { - \core\notification::error(get_string("backendHost_not_set", assign_submission_dta::COMPONENT_NAME)); - } - - return $backendaddress; - } - - /** - * Sends the configuration textfile uploaded by prof to the backend. - * - * @param stdClass $assignment assignment this test-config belongs to - * @param stdClass $file uploaded test-config - * @return bool true if no error occurred - */ - public static function sendtestconfigtobackend($assignment, $file): bool { - $backendaddress = self::getbackendbaseurl(); - if (empty($backendaddress)) { - return true; - } - - // Set endpoint for test upload. - $url = $backendaddress . "/v1/unittest"; - - // Prepare params. - $params = [ - "unitTestFile" => $file, - "assignmentId" => $assignment->get_instance()->id, - ]; - - // If request returned null, return false to indicate failure. - if (is_null(self::post($url, $params))) { - return false; - } else { - return true; - } - } - - /** - * Sends submission config or archive to backend to be tested. - * - * @param stdClass $assignment assignment for the submission - * @param int $submissionid submissionid of the current file - * @param stdClass $file submission config file or archive with submission - * @return string json string with testresults or null on error - */ - public static function send_submission_to_backend($assignment, $submissionid, $file): ?string { - $backendaddress = self::getbackendbaseurl(); - if (empty($backendaddress)) { - return true; - } - - // Set endpoint for test upload. - $url = $backendaddress . "/v1/task/" . $submissionid; - - // Prepare params. - $params = [ - "taskFile" => $file, - "assignmentId" => $assignment->get_instance()->id, - ]; - - return self::post($url, $params); - } - - /** - * Posts the given params to the given url and returns the response as a string. - * @param string $url full url to request to - * @param array $params parameters for http-request - * - * @return string received body on success or null on error - */ - private static function post($url, $params): ?string { - if (!isset($url) || !isset($params)) { - return false; - } - - $options = ["CURLOPT_RETURNTRANSFER" => true]; - - $curl = new curl(); - $response = $curl->post($url, $params, $options); - - // Check state of request, if response code is a 2xx return the answer. - $info = $curl->get_info(); - if ($info["http_code"] >= 200 && $info["http_code"] < 300) { - return $response; - } - - // Something went wrong, return null and give an error message. - debugging(assign_submission_dta::COMPONENT_NAME . ": Post file to server was not successful: http_code=" . - $info["http_code"]); - - if ($info['http_code'] >= 400 && $info['http_code'] < 500) { - \core\notification::error(get_string("http_client_error_msg", assign_submission_dta::COMPONENT_NAME)); - return null; - } else if ($info['http_code'] >= 500 && $info['http_code'] < 600) { - \core\notification::error(get_string("http_server_error_msg", assign_submission_dta::COMPONENT_NAME)); - return null; - } else { - \core\notification::error(get_string("http_unknown_error_msg", assign_submission_dta::COMPONENT_NAME) . - $info["http_code"] . $response); - return null; - } - } - -} diff --git a/dta/classes/database.php b/dta/classes/db_utils.php similarity index 82% rename from dta/classes/database.php rename to dta/classes/db_utils.php index f93fd06..0dfb452 100644 --- a/dta/classes/database.php +++ b/dta/classes/db_utils.php @@ -1,4 +1,5 @@ <?php +namespace assignsubmission_dta; // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -21,7 +22,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright Gero Lueckemeyer and student project teams */ -class DbUtils { + +use assignsubmission_dta\dta_backend_utils; +use assignsubmission_dta\view_submission_utils; +use assignsubmission_dta\models\dta_result; +use assignsubmission_dta\models\dta_result_summary; +use assignsubmission_dta\models\dta_recommendation; +class db_utils { /** * Summary database table name. @@ -77,6 +84,7 @@ class DbUtils { // Wenn der Abschlussstatus 1 ist, entferne den Datensatz aus $records if ($completion && $completion->completionstate == 1) { unset($records[$key]); + } } } @@ -94,10 +102,10 @@ class DbUtils { * @param int $submissionid submission id to search for * @return DttResultSummary representing given submission */ - public static function getresultsummaryfromdatabase( + public static function get_result_summary_from_database( int $assignmentid, int $submissionid - ): DtaResultSummary { + ): dta_result_summary { global $DB; // Fetch data from database. @@ -112,7 +120,7 @@ class DbUtils { ]); // Create a summary instance. - $summary = new DtaResultSummary(); + $summary = new dta_result_summary(); $summary->timestamp = $summaryrecord->timestamp; $summary->globalstacktrace = $summaryrecord->global_stacktrace; $summary->successfultestcompetencies = $summaryrecord->successful_competencies; @@ -121,7 +129,7 @@ class DbUtils { // Create result instances and add to array of summary instance. foreach ($resultsarray as $rr) { - $result = new DtaResult(); + $result = new dta_result(); $result->packagename = $rr->package_name; $result->classname = $rr->class_name; $result->name = $rr->name; @@ -138,33 +146,16 @@ class DbUtils { return $summary; } - public static function storeRecommendationstoDatabase( + public static function store_recommendations_to_database( int $assignmentid, int $submissionid, array $recommendations ): void { global $DB; - error_log(print_r($recommendations,true)); - - // Prepare recommendations to persist to array. - $recommendationrecords = []; - foreach ($recommendations as $recommendation) { - $record = new stdClass(); - $record->assignment_id = $assignmentid; - $record->submission_id = $submissionid; - $record->topic = $recommendation['topic']; - $record->url = $recommendation['url']; - $record->exercise_name = $recommendation['exercise_name']; - $record->difficulty = $recommendation['difficulty']; - $record->score = $recommendation['score']; - $recommendationrecords[] = $record; - } - - error_log("Das sind die Recommendationrecords."); - error_log(print_r($recommendationrecords,true)); + error_log(print_r($recommendations, true)); // If recommendations already exist, delete old values beforehand. - $existingrecords = $DB->get_record('assignsubmission_dta_recommendations', [ + $existingrecords = $DB->get_records('assignsubmission_dta_recommendations', [ 'assignment_id' => $assignmentid, 'submission_id' => $submissionid, ]); @@ -177,10 +168,22 @@ class DbUtils { } // Create new recommendation entries. - foreach ($recommendationrecords as $rec) { - error_log("Insert record"); - error_log(print_r($rec,true)); - $DB->insert_record('assignsubmission_dta_recommendations', $rec); + foreach ($recommendations as $recommendation) { + // Ensure $recommendation is an instance of DtaRecommendation + if ($recommendation instanceof dta_recommendation) { + // Add assignment and submission IDs to the recommendation object + $recommendation->assignment_id = $assignmentid; + $recommendation->submission_id = $submissionid; + + error_log("Insert record"); + error_log(print_r($recommendation, true)); + + // Insert the recommendation into the database + $DB->insert_record('assignsubmission_dta_recommendations', $recommendation); + } else { + // Handle the case where $recommendation is not a DtaRecommendation instance + error_log("Invalid recommendation object"); + } } } @@ -191,17 +194,17 @@ class DbUtils { * * @param int $assignmentid assigment this is submission is linked to * @param int $submissionid submission of this result - * @param DtaResultSummary $summary instance to persist + * @param dta_result_summary $summary instance to persist */ - public static function storeresultsummarytodatabase( + public static function store_result_summary_to_database( int $assignmentid, int $submissionid, - DtaResultSummary $summary + dta_result_summary $summary ): void { global $DB; // Prepare new database entries. - $summaryrecord = new stdClass(); + $summaryrecord = new dta_result_summary(); $summaryrecord->assignment_id = $assignmentid; $summaryrecord->submission_id = $submissionid; $summaryrecord->timestamp = $summary->timestamp; @@ -212,7 +215,7 @@ class DbUtils { // Prepare results to persist to array. $resultrecords = []; foreach ($summary->results as $r) { - $record = new stdClass(); + $record = new dta_result(); $record->assignment_id = $assignmentid; $record->submission_id = $submissionid; $record->package_name = $r->packagename; @@ -256,7 +259,7 @@ class DbUtils { /** * cleans up database if plugin is uninstalled */ - public static function uninstallplugincleaup(): void { + public static function uninstall_plugin_cleaup(): void { global $DB; $DB->delete_records(self::TABLE_RESULT, null); diff --git a/dta/classes/dta_backend_utils.php b/dta/classes/dta_backend_utils.php new file mode 100644 index 0000000..cfe0ac9 --- /dev/null +++ b/dta/classes/dta_backend_utils.php @@ -0,0 +1,151 @@ +<?php + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * This file contains the backend webservice contact functionality for the DTA plugin + * + * @package assignsubmission_dta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + + +/** + * Backend webservice contact utility class + * + * @package assignsubmission_dta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace assignsubmission_dta; + +defined('MOODLE_INTERNAL') || die(); + +class dta_backend_utils { + + /** + * Component name for the plugin. + */ + const COMPONENT_NAME = 'assignsubmission_dta'; + + /** + * Returns the base URL of the backend webservice as configured in the administration settings. + * @return string Backend host base URL + */ + private static function get_backend_baseurl(): string { + $backendaddress = get_config(self::COMPONENT_NAME, 'backendHost'); + + if (empty($backendaddress)) { + \core\notification::error(get_string('backendHost_not_set', self::COMPONENT_NAME)); + } + + return $backendaddress; + } + + /** + * Sends the configuration text file uploaded by the teacher to the backend. + * + * @param \assign $assignment Assignment this test-config belongs to + * @param \stored_file $file Uploaded test-config + * @return bool True if no error occurred + */ + public static function send_testconfig_to_backend($assignment, $file): bool { + $backendaddress = self::get_backend_baseurl(); + if (empty($backendaddress)) { + return true; + } + + // Set endpoint for test upload. + $url = $backendaddress . '/v1/unittest'; + + // Prepare params. + $params = [ + 'unitTestFile' => $file, + 'assignmentId' => $assignment->get_instance()->id, + ]; + + // If request returned null, return false to indicate failure. + if (is_null(self::post($url, $params))) { + return false; + } else { + return true; + } + } + + /** + * Sends submission config or archive to backend to be tested. + * + * @param \assign $assignment Assignment for the submission + * @param int $submissionid Submission ID of the current file + * @param \stored_file $file Submission config file or archive with submission + * @return string|null JSON string with test results or null on error + */ + public static function send_submission_to_backend($assignment, $submissionid, $file): ?string { + $backendaddress = self::get_backend_baseurl(); + if (empty($backendaddress)) { + return null; + } + + // Set endpoint for submission upload. + $url = $backendaddress . '/v1/task/' . $submissionid; + + // Prepare params. + $params = [ + 'taskFile' => $file, + 'assignmentId' => $assignment->get_instance()->id, + ]; + + return self::post($url, $params); + } + + /** + * Posts the given params to the given URL and returns the response as a string. + * @param string $url Full URL to request to + * @param array $params Parameters for HTTP request + * + * @return string|null Received body on success or null on error + */ + private static function post($url, $params): ?string { + if (!isset($url) || !isset($params)) { + return null; + } + + $options = ['CURLOPT_RETURNTRANSFER' => true]; + + $curl = new \curl(); + $response = $curl->post($url, $params, $options); + + // Check state of request, if response code is a 2xx return the answer. + $info = $curl->get_info(); + if ($info['http_code'] >= 200 && $info['http_code'] < 300) { + return $response; + } + + // Something went wrong, return null and give an error message. + debugging(self::COMPONENT_NAME . ': Post file to server was not successful: http_code=' . $info['http_code']); + + if ($info['http_code'] >= 400 && $info['http_code'] < 500) { + \core\notification::error(get_string('http_client_error_msg', self::COMPONENT_NAME)); + return null; + } else if ($info['http_code'] >= 500 && $info['http_code'] < 600) { + \core\notification::error(get_string('http_server_error_msg', self::COMPONENT_NAME)); + return null; + } else { + \core\notification::error(get_string('http_unknown_error_msg', self::COMPONENT_NAME) . $info['http_code'] . $response); + return null; + } + } +} diff --git a/dta/classes/models/dta_recommendation.php b/dta/classes/models/dta_recommendation.php new file mode 100644 index 0000000..19a1cae --- /dev/null +++ b/dta/classes/models/dta_recommendation.php @@ -0,0 +1,90 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Entity class for DTA submission plugin recommendation + * + * @package assignsubmission_dta + * @copyright 2023 Gero Lueckemeyer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace assignsubmission_dta\models; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Entity class for DTA submission plugin recommendation + * + * @package assignsubmission_dta + * @copyright 2023 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dta_recommendation { + + /** + * @var string $topic Topic of the recommendation. + */ + public $topic; + + /** + * @var string $exercise_name Name of the exercise. + */ + public $exercise_name; + + /** + * @var string $url URL of the exercise. + */ + public $url; + + /** + * @var int $difficulty Difficulty level of the exercise. + */ + public $difficulty; + + /** + * @var int $score Score associated with the recommendation. + */ + public $score; + + /** + * Decodes the JSON recommendations returned by the backend service call into an array of DtaRecommendation objects. + * + * @param string $jsonstring JSON string containing recommendations + * @return array Array of DtaRecommendation objects + */ + public static function decode_json_recommendations(string $jsonstring): array { + $response = json_decode($jsonstring); + + $recommendations = []; + + // Prüfe, ob Empfehlungen vorhanden sind + if (!empty($response->recommendations)) { + foreach ($response->recommendations as $recommendation) { + $rec = new dta_recommendation(); + $rec->topic = $recommendation->topic ?? null; + $rec->exercise_name = $recommendation->url ?? null; + $rec->url = $recommendation->exerciseName ?? null; + $rec->difficulty = $recommendation->difficulty ?? null; + $rec->score = $recommendation->score ?? null; + + $recommendations[] = $rec; + } + } + + return $recommendations; + } +} diff --git a/dta/classes/models/dta_result.php b/dta/classes/models/dta_result.php new file mode 100644 index 0000000..d1650ee --- /dev/null +++ b/dta/classes/models/dta_result.php @@ -0,0 +1,114 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Entity class for DTA submission plugin result + * + * @package assignsubmission_dta + * @copyright 2023 Gero Lueckemeyer and student project teams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace assignsubmission_dta\models; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Entity class for DTA submission plugin result + * + * @package assignsubmission_dta + * @copyright 2023 Gero Lueckemeyer and student project teams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dta_result { + + /** + * Broadly used in logic, parametrized for easier change. + */ + const COMPONENT_NAME = 'assignsubmission_dta'; + + /** + * @var string $packagename Package name of the test. + */ + public $packagename; + + /** + * @var string $classname Unit name of the test. + */ + public $classname; + + /** + * @var string $name Name of the test. + */ + public $name; + + /** + * @var int $state State is defined as: + * 0 UNKNOWN + * 1 SUCCESS + * 2 FAILURE + * 3 COMPILATIONERROR + */ + public $state; + + /** + * @var string $failuretype Type of test failure if applicable, empty string otherwise. + */ + public $failuretype; + + /** + * @var string $failurereason Reason of test failure if applicable, empty string otherwise. + */ + public $failurereason; + + /** + * @var string $stacktrace Stack trace of test failure if applicable, empty string otherwise. + */ + public $stacktrace; + + /** + * @var int|string $columnnumber Column number of compile failure if applicable, empty string otherwise. + */ + public $columnnumber; + + /** + * @var int|string $linenumber Line number of compile failure if applicable, empty string otherwise. + */ + public $linenumber; + + /** + * @var int|string $position Position of compile failure if applicable, empty string otherwise. + */ + public $position; + + /** + * Returns the name of a state with the given number for display. + * + * @param int $state Number of the state + * @return string Name of state as defined + */ + public static function getstatename(int $state): string { + if ($state == 1) { + return get_string('tests_successful', self::COMPONENT_NAME); + } else if ($state == 2) { + return get_string('failures', self::COMPONENT_NAME); + } else if ($state == 3) { + return get_string('compilation_errors', self::COMPONENT_NAME); + } else { + return get_string('unknown_state', self::COMPONENT_NAME); + } + } +} diff --git a/dta/classes/models/dta_result_summary.php b/dta/classes/models/dta_result_summary.php new file mode 100644 index 0000000..56ed33e --- /dev/null +++ b/dta/classes/models/dta_result_summary.php @@ -0,0 +1,173 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Entity class for DTA submission plugin result summary + * + * @package assignsubmission_dta + * @copyright 2023 Gero Lueckemeyer and student project teams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace assignsubmission_dta\models; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Entity class for DTA submission plugin result summary + * + * @package assignsubmission_dta + * @copyright 2023 Gero Lueckemeyer and student project teams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dta_result_summary { + + /** + * @var int $timestamp Result timestamp for chronological ordering and deletion of previous results. + */ + public $timestamp; + + /** + * @var string $globalstacktrace Global stack trace if applicable, empty string otherwise. + */ + public $globalstacktrace; + + /** + * @var string $successfultestcompetencies Successfully tested competencies according to tests and weights, empty string otherwise. + */ + public $successfultestcompetencies; + + /** + * @var string $overalltestcompetencies Overall tested competencies according to tests and weights, empty string otherwise. + */ + public $overalltestcompetencies; + + /** + * @var array $results List of detail results. + */ + public $results; + + /** + * Decodes the JSON result summary returned by the backend service call into the plugin PHP data structure. + * + * @param string $jsonstring JSON string containing DtaResultSummary + * @return DtaResultSummary The result summary + */ + public static function decode_json(string $jsonstring): dta_result_summary { + $response = json_decode($jsonstring); + + $summary = new dta_result_summary(); + $summary->timestamp = $response->timestamp; + $summary->globalstacktrace = $response->globalstacktrace; + + $summary->successfultestcompetencies = $response->successfulTestCompetencyProfile ?? ''; + $summary->overalltestcompetencies = $response->overallTestCompetencyProfile ?? ''; + + $summary->results = self::decode_json_result_array($response->results); + + return $summary; + } + + /** + * Decodes the array of JSON detail results returned by the backend service call into the plugin PHP data structure. + * + * @param array $jsonarray Decoded JSON array of results + * @return array Array of DtaResult + */ + private static function decode_json_result_array(array $jsonarray): array { + $ret = []; + foreach ($jsonarray as $entry) { + $value = new dta_result(); + $value->packagename = $entry->packageName ?? ''; + $value->classname = $entry->className ?? ''; + $value->name = $entry->name ?? ''; + + $value->state = $entry->state ?? 0; + + $value->failuretype = $entry->failureType ?? ''; + $value->failurereason = $entry->failureReason ?? ''; + $value->stacktrace = $entry->stacktrace ?? ''; + + $value->columnnumber = $entry->columnNumber ?? ''; + $value->linenumber = $entry->lineNumber ?? ''; + $value->position = $entry->position ?? ''; + + $ret[] = $value; + } + return $ret; + } + + /** + * Returns the number of detail results attached to the summary. + * + * @return int Count of occurrences + */ + public function result_count(): int { + return count($this->results); + } + + /** + * Returns the number of detail results with the given state attached to the summary. + * + * @param int $state State ordinal number + * @return int Count of occurrences for the provided state + */ + public function state_occurence_count(int $state): int { + $num = 0; + foreach ($this->results as $r) { + if ($r->state == $state) { + $num++; + } + } + return $num; + } + + /** + * Returns the number of detail results with compilation errors attached to the summary. + * + * @return int Count of occurrences + */ + public function compilationerrorcount(): int { + return $this->state_occurence_count(3); + } + + /** + * Returns the number of detail results with test failures attached to the summary. + * + * @return int Count of occurrences + */ + public function failedcount(): int { + return $this->state_occurence_count(2); + } + + /** + * Returns the number of detail results with successful tests attached to the summary. + * + * @return int Count of occurrences + */ + public function successfulcount(): int { + return $this->state_occurence_count(1); + } + + /** + * Returns the number of detail results with an unknown result attached to the summary. + * + * @return int Count of occurrences + */ + public function unknowncount(): int { + return $this->state_occurence_count(0); + } +} diff --git a/dta/classes/view.php b/dta/classes/view_submission_utils.php similarity index 75% rename from dta/classes/view.php rename to dta/classes/view_submission_utils.php index d6258cd..fed291a 100644 --- a/dta/classes/view.php +++ b/dta/classes/view_submission_utils.php @@ -1,4 +1,5 @@ <?php +namespace assignsubmission_dta; // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -20,6 +21,12 @@ * @package assignsubmission_dta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use assignsubmission_dta\db_utils; +use assignsubmission_dta\dta_backend_utils; +use assignsubmission_dta\models\dta_result; +use assignsubmission_dta\models\dta_result_summary; +use assignsubmission_dta\models\dta_recommendation; + class view_submission_utils { /** @@ -34,13 +41,13 @@ class view_submission_utils { * @param int $submissionid submission to create a report for * @return string html */ - public static function generatesummaryhtml( + public static function generate_summary_html( int $assignmentid, int $submissionid ): string { // Fetch data. - $summary = DbUtils::getResultSummaryFromDatabase($assignmentid, $submissionid); + $summary = db_utils::get_Result_Summary_From_Database($assignmentid, $submissionid); $html = ""; // Calculate success rate, if no unknown result states or compilation errors. @@ -52,7 +59,7 @@ class view_submission_utils { // Generate html. $html .= $summary->successfulCount() . "/"; $html .= ($summary->compilationErrorCount() == 0 && $summary->unknownCount() == 0) - ? $summary->resultCount() . " (" . $successrate . "%)" + ? $summary->result_Count() . " (" . $successrate . "%)" : "?"; $html .= get_string("tests_successful", self::COMPONENT_NAME) . "<br />"; @@ -80,7 +87,7 @@ class view_submission_utils { $html .= get_string("success_competencies", self::COMPONENT_NAME) . "<br />" . $tmp . "<br />"; - return html_writer::div($html, "dtaSubmissionSummary"); + return \html_writer::div($html, "dtaSubmissionSummary"); } /** @@ -90,7 +97,7 @@ class view_submission_utils { * @param int $submissionid Submission-ID, für die der Bericht erstellt wird * @return string HTML-Code */ -public static function generatedetailhtml( +public static function generate_detail_html( int $assignmentid, int $submissionid ): string { @@ -99,7 +106,7 @@ public static function generatedetailhtml( $html = ""; // Daten abrufen - $summary = DbUtils::getResultSummaryFromDatabase($assignmentid, $submissionid); + $summary = db_utils::get_Result_Summary_From_Database($assignmentid, $submissionid); // CSS-Klassen und HTML-Attributarrays definieren $tableheaderrowattributes = ["class" => "dtaTableHeaderRow"]; @@ -114,11 +121,11 @@ public static function generatedetailhtml( // (Ihr bisheriger Code bleibt unverändert) // **Abstand zwischen Tabellen** - $html .= html_writer::empty_tag("div", ["class" => "dtaSpacer"]); + $html .= \html_writer::empty_tag("div", ["class" => "dtaSpacer"]); // **Empfehlungstabelle hinzufügen** // Empfehlungen für die Submission abrufen - $recommendations = DbUtils::get_recommendations_from_database($assignmentid, $submissionid); + $recommendations = db_utils::get_recommendations_from_database($assignmentid, $submissionid); if (!empty($recommendations)) { // **Sortierparameter abrufen** @@ -160,7 +167,7 @@ public static function generatedetailhtml( }); // Überschrift für Empfehlungen - $html .= html_writer::tag('h3', get_string('recommendations', self::COMPONENT_NAME)); + $html .= \html_writer::tag('h3', get_string('recommendations', self::COMPONENT_NAME)); // Helper-Funktion zum Generieren von sortierbaren Headern $generate_sortable_header = function($column_name, $display_name) use ($sortby, $sortdir) { @@ -171,7 +178,7 @@ public static function generatedetailhtml( } // Button erstellen - $button = html_writer::empty_tag('input', [ + $button = \html_writer::empty_tag('input', [ 'type' => 'submit', 'name' => 'sortbutton', 'value' => ($new_sortdir == 'asc' ? '↑' : '↓'), @@ -179,57 +186,57 @@ public static function generatedetailhtml( ]); // Hidden Inputs für Sortierparameter - $hidden_inputs = html_writer::empty_tag('input', [ + $hidden_inputs = \html_writer::empty_tag('input', [ 'type' => 'hidden', 'name' => 'sortby', 'value' => $column_name ]); - $hidden_inputs .= html_writer::empty_tag('input', [ + $hidden_inputs .= \html_writer::empty_tag('input', [ 'type' => 'hidden', 'name' => 'sortdir', 'value' => $new_sortdir ]); // Formular für den Button erstellen - $form = html_writer::start_tag('form', ['method' => 'post', 'style' => 'display:inline']); + $form = \html_writer::start_tag('form', ['method' => 'post', 'style' => 'display:inline']); $form .= $hidden_inputs; $form .= $display_name . ' ' . $button; - $form .= html_writer::end_tag('form'); + $form .= \html_writer::end_tag('form'); - return html_writer::tag("th", $form, ["class" => $class]); + return \html_writer::tag("th", $form, ["class" => $class]); }; // Tabellenkopf für Empfehlungen $tableheader = ""; $tableheader .= $generate_sortable_header('topic', get_string("topic", self::COMPONENT_NAME)); $tableheader .= $generate_sortable_header('exercise_name', get_string("exercise_name", self::COMPONENT_NAME)); - $tableheader .= html_writer::tag("th", get_string("url", self::COMPONENT_NAME), ["class" => "dtaTableHeader"]); + $tableheader .= \html_writer::tag("th", get_string("url", self::COMPONENT_NAME), ["class" => "dtaTableHeader"]); $tableheader .= $generate_sortable_header('difficulty', get_string("difficulty", self::COMPONENT_NAME)); $tableheader .= $generate_sortable_header('score', get_string("score", self::COMPONENT_NAME)); - $tableheader = html_writer::tag("tr", $tableheader, ["class" => "dtaTableHeaderRow"]); - $tableheader = html_writer::tag("thead", $tableheader); + $tableheader = \html_writer::tag("tr", $tableheader, ["class" => "dtaTableHeaderRow"]); + $tableheader = \html_writer::tag("thead", $tableheader); // Tabellenkörper für Empfehlungen $tablebody = ""; foreach ($recommendations as $recommendation) { $tablerow = ""; - $tablerow .= html_writer::tag("td", $recommendation->topic, $attributes); - $tablerow .= html_writer::tag("td", $recommendation->exercise_name, $attributes); - $tablerow .= html_writer::tag("td", html_writer::link($recommendation->url, $recommendation->url), $attributes); - $tablerow .= html_writer::tag("td", $recommendation->difficulty, $attributes); - $tablerow .= html_writer::tag("td", $recommendation->score, $attributes); + $tablerow .= \html_writer::tag("td", $recommendation->topic, $attributes); + $tablerow .= \html_writer::tag("td", $recommendation->exercise_name, $attributes); + $tablerow .= \html_writer::tag("td", \html_writer::link($recommendation->url, $recommendation->url), $attributes); + $tablerow .= \html_writer::tag("td", $recommendation->difficulty, $attributes); + $tablerow .= \html_writer::tag("td", $recommendation->score, $attributes); - $tablebody .= html_writer::tag("tr", $tablerow, $tablerowattributes); + $tablebody .= \html_writer::tag("tr", $tablerow, $tablerowattributes); } - $tablebody = html_writer::tag("tbody", $tablebody); + $tablebody = \html_writer::tag("tbody", $tablebody); // Empfehlungstabelle zusammenstellen - $html .= html_writer::tag("table", $tableheader . $tablebody, ["class" => "dtaTable"]); + $html .= \html_writer::tag("table", $tableheader . $tablebody, ["class" => "dtaTable"]); } // Abschließendes Div für die gesamte HTML-Ausgabe - $html = html_writer::div($html, "dtaSubmissionDetails"); + $html = \html_writer::div($html, "dtaSubmissionDetails"); return $html; } diff --git a/dta/locallib.php b/dta/locallib.php index 28ea9be..cbea769 100644 --- a/dta/locallib.php +++ b/dta/locallib.php @@ -16,14 +16,15 @@ defined('MOODLE_INTERNAL') || die(); -// Import various entity and application logic files. -require_once($CFG->dirroot . '/mod/assign/submission/dta/models/DtaResult.php'); -require_once($CFG->dirroot . '/mod/assign/submission/dta/classes/database.php'); -require_once($CFG->dirroot . '/mod/assign/submission/dta/classes/backend.php'); -require_once($CFG->dirroot . '/mod/assign/submission/dta/classes/view.php'); +use assignsubmission_dta\db_utils; +use assignsubmission_dta\dta_backend_utils; +use assignsubmission_dta\view_submission_utils; +use assignsubmission_dta\models\dta_result; +use assignsubmission_dta\models\dta_result_summary; +use assignsubmission_dta\models\dta_recommendation; /** - * library class for DTA submission plugin extending assign submission plugin base class + * Library class for DTA submission plugin extending assign submission plugin base class * * @package assignsubmission_dta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -35,20 +36,20 @@ class assign_submission_dta extends assign_submission_plugin { */ const COMPONENT_NAME = "assignsubmission_dta"; /** - * Draft file area for dta tests to be uploaded by the teacher. + * Draft file area for DTA tests to be uploaded by the teacher. */ const ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST = "tests_draft_dta"; /** - * File area for dta tests to be uploaded by the teacher. + * File area for DTA tests to be uploaded by the teacher. */ const ASSIGNSUBMISSION_DTA_FILEAREA_TEST = "tests_dta"; /** - * File area for dta submission assignment. + * File area for DTA submission assignment. */ const ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION = "submissions_dta"; /** - * get plugin name + * Get plugin name * @return string */ public function get_name(): string { @@ -93,7 +94,7 @@ class assign_submission_dta extends assign_submission_plugin { } /** - * Allows the plugin to update the defaultvalues passed in to + * Allows the plugin to update the default values passed into * the settings form (needed to set up draft areas for editor * and filemanager elements) * @param array $defaultvalues @@ -162,7 +163,7 @@ class assign_submission_dta extends assign_submission_plugin { $file = reset($files); // Send file to backend. - return DtaBackendUtils::sendtestconfigtobackend($this->assignment, $file); + return dta_backend_utils::send_testconfig_to_backend($this->assignment, $file); } /** @@ -240,86 +241,78 @@ class assign_submission_dta extends assign_submission_plugin { return count($files); } - /** - * Save data to the database - * - * @param stdClass $submission - * @param stdClass $data - * @return bool - */ -public function save(stdClass $submission, stdClass $data) { - $data = file_postupdate_standard_filemanager( - $data, - 'tasks', - $this->get_file_options(false), - $this->assignment->get_context(), - self::COMPONENT_NAME, - self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION, - $submission->id - ); - - // If submission is empty leave directly. - if ($this->is_empty($submission)) { - return true; - } - - // Get submitted files. - $fs = get_file_storage(); - $files = $fs->get_area_files( - // Id of current assignment. - $this->assignment->get_context()->id, - self::COMPONENT_NAME, - self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION, - $submission->id, - 'id', - false - ); - - // Check if a file is uploaded. - if (empty($files)) { - \core\notification::error(get_string("no_submissionfile_warning", self::COMPONENT_NAME)); - return true; - } - - // Get the file. - $file = reset($files); + /** + * Save data to the database + * + * @param stdClass $submission + * @param stdClass $data + * @return bool + */ + public function save(stdClass $submission, stdClass $data) { + $data = file_postupdate_standard_filemanager( + $data, + 'tasks', + $this->get_file_options(false), + $this->assignment->get_context(), + self::COMPONENT_NAME, + self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION, + $submission->id + ); - // Send file to backend. - $response = DtaBackendUtils::send_submission_to_backend($this->assignment, $submission->id, $file); + // If submission is empty, leave directly. + if ($this->is_empty($submission)) { + return true; + } - // With a null response, return an error. - if (is_null($response)) { - return false; - } + // Get submitted files. + $fs = get_file_storage(); + $files = $fs->get_area_files( + // Id of current assignment. + $this->assignment->get_context()->id, + self::COMPONENT_NAME, + self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION, + $submission->id, + 'id', + false + ); - // Convert received json to valid class instances. - $resultsummary = DtaResultSummary::decodejson($response); - // Log an error message. - - $recommendations = DtaResultSummary::decodeJsonRecommendation($response); + // Check if a file is uploaded. + if (empty($files)) { + \core\notification::error(get_string("no_submissionfile_warning", self::COMPONENT_NAME)); + return true; + } - error_log(print_r($recommendations,true)); + // Get the file. + $file = reset($files); + // Send file to backend. + $response = \assignsubmission_dta\dta_backend_utils::send_submission_to_backend($this->assignment, $submission->id, $file); + // With a null response, return an error. + if (is_null($response)) { + return false; + } - // Persist new results to database. - DbUtils::storeresultsummarytodatabase($this->assignment->get_instance()->id, $submission->id, $resultsummary); + // Convert received JSON to valid class instances. + $resultsummary = dta_result_summary::decode_json($response); + // Decode recommendations from response. + $recommendations = dta_recommendation::decode_json_recommendations($response); + error_log(print_r($recommendations, true)); - - // Store the array of records in the database. - DbUtils::storeRecommendationstoDatabase($this->assignment->get_instance()->id, $submission->id, $recommendations); - - + // Persist new results to database. + db_utils::store_result_summary_to_database($this->assignment->get_instance()->id, $submission->id, $resultsummary); - return true; -} + // Store the array of recommendations in the database. + db_utils::store_recommendations_to_database($this->assignment->get_instance()->id, $submission->id, $recommendations); + return true; + } /** * Display a short summary of the test results of the submission - * This is diplayed as default view, with the option to expand + * This is displayed as default view, with the option to expand * to the full detailed results. * * @param stdClass $submission to show @@ -329,7 +322,7 @@ public function save(stdClass $submission, stdClass $data) { public function view_summary(stdClass $submission, & $showviewlink) { $showviewlink = true; - return view_submission_utils::generatesummaryhtml( + return view_submission_utils::generate_summary_html( $this->assignment->get_instance()->id, $submission->id ); @@ -342,16 +335,14 @@ public function save(stdClass $submission, stdClass $data) { * @return string detailed results html */ public function view(stdClass $submission) { - // Sicherstellen, dass $cmid verfügbar ist - - return view_submission_utils::generatedetailhtml( + return view_submission_utils::generate_detail_html( $this->assignment->get_instance()->id, $submission->id ); } /** - * generate array of allowed filetypes to upload. + * Generate array of allowed file types to upload. * * @param bool $settings switch to define if list for assignment settings * or active submission should be returned @@ -413,12 +404,12 @@ public function save(stdClass $submission, stdClass $data) { } /** - * The plugin is beeing uninstalled - cleanup + * The plugin is being uninstalled - cleanup * * @return bool */ public function delete_instance() { - DbUtils::uninstallplugincleanup(); + db_utils::uninstall_plugin_cleanup(); return true; } diff --git a/dta/models/DtaResult.php b/dta/models/DtaResult.php deleted file mode 100644 index 867ee81..0000000 --- a/dta/models/DtaResult.php +++ /dev/null @@ -1,277 +0,0 @@ -<?php -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see <http://www.gnu.org/licenses/>. - -/** - * entity classes for DTA submission plugin result summary and test results - * - * @package assignsubmission_dta - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright Gero Lueckemeyer and student project teams - */ -defined('MOODLE_INTERNAL') || die(); - -/** - * entity class for DTA submission plugin result - * - * @package assignsubmission_dta - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright Gero Lueckemeyer and student project teams - */ -class DtaResult { - - /** - * Broadly used in logic, parametrized for easier change. - */ - const COMPONENT_NAME = "assignsubmission_dta"; - - /** - * @var $packagename Package name of the test. - */ - public $packagename; - - /** - * @var $classname Unit name of the test. - */ - public $classname; - - /** - * @var $name Name of the test. - */ - public $name; - - /** - * @var $state State is defined like below - * - * 0 UNKNOWN - * 1 SUCCESS - * 2 FAILURE - * 3 COMPILATIONERROR - */ - public $state; - - /** - * @var $failuretype Type of test failure if applicable, "" otherwise. - */ - public $failuretype; - - /** - * @var $failurereason Reason of test failure if applicable, "" otherwise. - */ - public $failurereason; - - /** - * @var $stacktrace Stack trace of test failure if applicable, "" otherwise. - */ - public $stacktrace; - - /** - * @var $columnnumber Column number of compile failure if applicable, "" otherwise. - */ - public $columnnumber; - /** - * @var $linenumber Line number of compile failure if applicable, "" otherwise. - */ - public $linenumber; - /** - * @var $position Position of compile failure if applicable, "" otherwise. - */ - public $position; - - /** - * Returns the name of a state with the given number of display. - * @param int $state number of the state - * @return string name of state as defined - */ - public static function getstatename(int $state): string { - if ($state == 1) { - return get_string("tests_successful", self::COMPONENT_NAME); - } else if ($state == 2) { - return get_string("failures", self::COMPONENT_NAME); - } else if ($state == 3) { - return get_string("compilation_errors", self::COMPONENT_NAME); - } else { - return get_string("unknown_state", self::COMPONENT_NAME); - } - } -} - -/** - * entity class for DTA submission plugin result - * - * @package assignsubmission_dta - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright Gero Lueckemeyer and student project teams - */ -class DtaResultSummary { - - /** - * @var $timestamp Result timestamp for chronological ordering and deletion of previous results. - */ - public $timestamp; - - /** - * @var $globalstacktrace Global stack trace if applicable, "" otherwise. - */ - public $globalstacktrace; - - /** - * @var $successfultestcompetencies Successfully tested competencies according to tests and weights, "" otherwise. - */ - public $successfultestcompetencies; - /** - * @var overalltestcompetencies Overall tested competencies according to tests and weights, "" otherwise. - */ - public $overalltestcompetencies; - /** - * @var results List of detail results. - */ - public $results; - - /** - * Decodes the JSON result summary returned by the backend service call into the plugin PHP data structure. - * @param string $jsonstring jsonString containing DtaResultSummary - * @return DtaResultSummary the result summary - */ - public static function decodejson(string $jsonstring): DtaResultSummary { - $response = json_decode($jsonstring); - - - $summary = new DtaResultSummary(); - $summary->timestamp = $response->timestamp; - $summary->globalstacktrace = $response->globalstacktrace; - - $summary->successfultestcompetencies = $response->successfulTestCompetencyProfile; - $summary->overalltestcompetencies = $response->overallTestCompetencyProfile; - - $summary->results = self::decodejsonresultarray($response->results); - - return $summary; - } - - public static function decodeJsonRecommendation(string $jsonstring): array { - // Decode the JSON string into a PHP object - $response = json_decode($jsonstring); - error_log("decodeJsonRecommendation"); - error_log(print_r($response, true)); - - - // Initialize an empty array to store recommendations - $recommendations = []; - - // Loop through the recommendations in the response - if (!empty($response->recommendations)) { - foreach ($response->recommendations as $recommendation) { - // For each recommendation, create an associative array with the properties - $recommendations[] = [ - 'topic' => $recommendation->topic ?? null, - 'url' => $recommendation->exerciseName ?? null, - 'exercise_name' => $recommendation->url ?? null, - 'difficulty' => $recommendation->difficulty ?? null, - 'score' => $recommendation->score ?? null - ]; - } - } - error_log(print_r($recommendations,true)); - // Return the array of recommendations - return $recommendations; - } - - - /** - * Decodes the array of JSON detail results returned by the backend service call into the plugin PHP data structure. - * @param array $jsonarray decoded json array of results array - * @return array of DtaResult - */ - private static function decodejsonresultarray($jsonarray): array { - $ret = []; - foreach ($jsonarray as $entry) { - $value = new DtaResult(); - $value->packagename = $entry->packageName; - $value->classname = $entry->className; - $value->name = $entry->name; - - $value->state = $entry->state; - - $value->failuretype = $entry->failureType; - $value->failurereason = $entry->failureReason; - $value->stacktrace = $entry->stacktrace; - - $value->columnnumber = $entry->columnNumber; - $value->linenumber = $entry->lineNumber; - $value->position = $entry->position; - - $ret[] = $value; - } - return $ret; - } - - - /** - * Returns the number of detail results attached to the summary. - * @return int count of occurences - */ - public function resultcount(): int { - return count($this->results); - } - - /** - * Returns the number of detail results with the given state attached to the summary. - * @param int $state state ordinal number - * @return int count of occurences provided state has - */ - public function stateoccurencecount(int $state): int { - $num = 0; - foreach ($this->results as $r) { - if ($r->state == $state) { - $num++; - } - } - return $num; - } - - /** - * Returns the number of detail results with compilation errors attached to the summary. - * @return int count of occurences - */ - public function compilationerrorcount(): int { - return $this->stateoccurencecount(3); - } - - /** - * Returns the number of detail results with test failures attached to the summary. - * @return int count of occurences - */ - public function failedcount(): int { - return $this->stateoccurencecount(2); - } - - /** - * Returns the number of detail results with successful tests attached to the summary. - * @return int count of occurences - */ - public function successfulcount(): int { - return $this->stateoccurencecount(1); - } - - /** - * Returns the number of detail results with an unknown result - mostly due to compile errors - attached to the summary. - * @return int count of occurences - */ - public function unknowncount(): int { - return $this->stateoccurencecount(0); - } - -} -- GitLab