From 239c672fc3cb7cef6cef914b9c608d8fbded81d6 Mon Sep 17 00:00:00 2001 From: trbKnl Date: Tue, 2 May 2023 17:37:09 +0200 Subject: [PATCH 01/49] first version --- README.md | 4 + public/port-0.0.0-py3-none-any.whl | Bin 4748 -> 10936 bytes .../py/dist/port-0.0.0-py3-none-any.whl | Bin 4748 -> 10936 bytes src/framework/processing/py/port/api/props.py | 2 +- .../processing/py/port/my_exceptions.py | 8 + src/framework/processing/py/port/netflix.py | 101 ++++++ src/framework/processing/py/port/script.py | 327 ++++++++++++------ src/framework/processing/py/port/unzipddp.py | 157 +++++++++ src/framework/processing/py/port/validate.py | 98 ++++++ .../react/ui/pages/donation_page.tsx | 17 +- 10 files changed, 610 insertions(+), 104 deletions(-) create mode 100644 src/framework/processing/py/port/my_exceptions.py create mode 100644 src/framework/processing/py/port/netflix.py create mode 100644 src/framework/processing/py/port/unzipddp.py create mode 100644 src/framework/processing/py/port/validate.py diff --git a/README.md b/README.md index 5cf3b633..402e1635 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# Port pilot with data from Netflix + +This repository contains the code for running the Netflix D3I pilot at the Utrecht Data School with [port](https://github.com/eyra/port). + # Port Port is a research tool that enables individuals to donate their digital trace data for academic research in a secure, transparent, and privacy-preserving way. diff --git a/public/port-0.0.0-py3-none-any.whl b/public/port-0.0.0-py3-none-any.whl index 5def99695c091ced738ff17d1018ad4ad25a990a..55d08190e96a9994f9b41c1aea410bcc998fd4d9 100644 GIT binary patch delta 9338 zcmZX4Wl$c?*6oA4ySoG@xVyUqO>lR27#;`&hu}ejyGw9)cXtTx9`xq>?s@CnbM8!a zO;^|6UHeb3wR^3JqG|e6T?&E9_Q^rd0tN)?zyX0UKnWNaSU`i$y5lZAI`Ci!zMhkB zKlRN|aByEge$uM&Y57(fQQVEHPsH1mzAyatkPcxNMPj*XKqw_2NRS%4iOiuX-SAaS zW;9f9lo_K!YoMScR~*rEN_^pU=T*$rgIQi1+gVy;xR~a0zMLL$QtlijsWC^H-kKFQ zYg9>Wt4^=t1;93DlaZ$(!NHt#dg;gbl@|lAU^IUmN*WgT6|>BnMqVvGbfhJZi}gh- zJCkFl#C0vS;;Q#|sNZ*Ys?!)9oq_02ovWfW=_t+TpPMD2#BpC8MAdEuA3Ny-R)2@VY{MM;YtCXw5 zuM_p7-u~b06cKkCL;fYCRqIm?kh!C<|(JT_U$zY;^#t2DM{TEp% zG;sFt=DP(4TG<({)8Mbwd8+ctkKNi>>Og7HjmKrLyWp9N4a0s;t5q6N!P*>WQno&K zGAD`3vh^x?t)BW|_f8i})IJZD3a1U(M9tjrrfW&?BvDx%x@-^Bh3LH|Kk`x+l3)CV z&;`Hb+$$=ktQ?K^brdqQf%@n(mi#MZNrHikak$_^Tqht;(KnmMOGF$1MV%_26+Bd`L=SYk z0|gjGEAT@vhiWwbdLj@U-Q0oG$`K;nzm7~WQDgAtyD!d2B&}e?1ns;(s?cfsa)1k0 zN2YLx3$HE7#f_iJE_7_N&kL8I2t#>zpYTEE2C0;C)qC#U8}4$@EBP8GvaVeXuNFz4 zduGaV67z<#u%tIcaC4wP#oh2;azKfV0%DnbNi$9C_LP5;s&pTb?A?1`-=*idOfcQY zu$0%3JbieN8V5~iyKGZ2vnZjx&9W&K+?74UD!vUGqe`La;MM3w$e>||yGC=Ro8e`d z1P+ZYgCA3MsOC55R+qRfu`i)^oRY`dr}Eg+pKJ(iijQ9@XP4qHM%Q?pRyXCV0SSg9 zTAKc*h^`eQzGD1e1sKBO+&Q0?k$zFBnqT5ImC%LbFErQZG7D`Oh^h+t9T<(GS*K5_o1J58fA7MiGFjI%k>I$~T97&*-A1$0Q~*k1Me0yysg^TeNcc>QdJ zzc%jhYoS7I2}Ai2(gn<9PLG66FbZ~on(PcBGX^mGLdvR(5!@AsG33~{QO<}s%`?HR zd3?7*PIevWL^+k={UI`!)xKVtOVEFIG^9H3^Oqn9>131NE*_nC5XW$029;l0jYtx? zUv2R=T9KLs++MDlmfm84nzY2T(OrYC)0|w{9cH8OmpUC1<&@Ah->-1br6x>X3rE>J zgwkwjLKKp9QKYClV_(^H7{7;HVLna=h2zR7B#OOC5)FtPzgieyOVwgi1fqAL{!}0A zAPzRs6y9=$<|3XRofY!u)(>VX%X4{$Q&oV1#s+~v@P7vdNUd%j8UX_MAVFOR{<~20 zcl?EBkdvc}8>_vyp}Ci-xs#iLrWxOx=ri0d2;Xz=iMX#yApOh+cN|90bPGs#HI2>`w-GQ*#bvwVB(s z5b&B^k6jl04g(@~f#Do;y8DjtTd8X6{x}4+yZ-CGd;&XkX4z(cIS3@!Ha;c$_Y^k27%E2DcZr@&BD&w>u9h!@>BDmIClzcNv|Ky2ud{|rxfkk){GwVmM@~J|FP;W1byMQ)S`X4}yGeRG^&~Sn z5{uv}^S4NSQsnwBgNMsin>g-y(SjU5Ul4W#djo#q6R$Pr&>{k?+AV;CALEGNL$p2t zn4yk7_qMCnJlnDb>YaHgUU0E3NKl5^Eu(6J=VWs6m-Y#A!-YZN?7j@yhvcgbZH6o~ zN{ZvQQ%wCV>K~Q0*?GywHoEsx1t(;?N5R~&{9ewZt&h)F>2AWzYZO^^k40Z)#$MYl zugPC)@GQIRYu;H~UhRhonuuUQt~J=&@mzas z^YokXY~7BQ4qhPV*~=Z1`pr>HkkQ^2u(#WbM3&ujNY_`A zqQU;u{Q{j>PKLopN;B!!1fO6zumNOjyhy<-%^tha^X@FGPhMsBmrHn~Bcc%Tc(cLEoAg}`WFJ|ylSir?-iderSZKbK82`7fK1@`sU3#+OeUV&-^< zUD9f$M1Yv!2Q8c|#Nuz`36wwh{qWw&&LmReVK#My4yBl(TH8~3P|HWHy?^H_N;Y?y zCmU?Fx7BOnTz&U({l?=Y@O!7_E)Yre1MXUNH5A|MdSyR1+ekXh>$pL#bY5?D&m)&& zbi46GmrX`8uV68nHCc^zO7cp+0&i?QbdVw&#kJr56s&`a|KJGB2Ey!A*P2LMx zNvzsQEhMYWrayyERaAmAgB9O{y8b}ZdL^d{#KoaU0+qNR$ba87*!~AmSNzkKdtbzx zwG%+*jde-~QFH^{ka8>XsgPWj#-DZn)NV9+v}BvRAo=OD4TMT~b)kWS-Rye#iu_s4 z!tw{&+$d?q&lUNT-<#X=5dkm~Zt}P2U!n_--)oEXSeu@Aed;Vy7Jr?dcze`!e5t>3 zIJCEq_3y8kU!!t#7M*#9=-5~y$sJI`rIiF=aG0}_7Y)58*OO<-)srd8!PFGb)0lUk zLlOsJRz_yFb`bMO*#eWuQ@NjBg_>3b-6(!YIM#2KILgpcA8nn`>G;@oZ3BH$A$jUq@>H;r}EokHLw zyw?{MvJ7R!E7fy{1}BYvYEJupa4DO>cM$f^s@jBbZQZ$6o3U;r{s2+hBBb^y(d(AD z*B|6O_Z18m!58{l)Gxy$gp`Ud;FNB63aqxSEJG7AM2cd#LT))^&%C zV)6IYLokrcYTUNEGvS)*JZ#lyE)Oox)I7Z+@${n;*&ZUqPU7F?u`PKyUdEj)%sLn% z<>Sj!C%uKqoL?WMu$$Wtq+C-X{UWy#*3%eAUo$U{ef3OJSp@!I)x%A$CfKI8y?AxrO` z(qXIHx82plo$Hs!`Kmp5P9#<{7@O>X+wII6G2XnO!Kz5X->!h<7G|x9oBP}htPQUz z4-+6zt64;*EPQ-bU#z7`>TQ}Gp>vIs4)5aRBsvhFEPxiVMxe`1aY&pcG)@meGlS#u zbY|4Xk58Af>p6Q>HG9?nNTfJ@+$JOp_o*D)m3Fh2xy;Y;i&+SqD~sXl>#MVyx6hxq zM@Wl}4+~lBwwh`PZawM8_K2*EX^~O#{G&kX0%=w2zA{D~@xJdtT4H%*+Sm~a6=zH& z1DKr>=^8AzCBjw8neQE%--&(4{G$f4unrksN7*3)`3*}p(o77}E&9t_e!fl$j(eEa~Du7!s4l zAE2y5UwoHTa(pEFF5$UVlqbx}tbw2(;uVQ5E4oi`_LQy>6T!feRr=#ipJqw^amFn%F7hv6zR6yf1JoCKL6u#&z^lHL^H~LatslBFaev;$?lS z6~2_qms)yC8Md>i%DNz0{z@+mxECJI-SE)u@6on71ZEiYr=H6_k!kK@g%p4nWU-jz zf%8Sb0NaLMw{IyPrQ=2hjAS?&9v8m(0AYns{ehxZT5?+|a$T@l z+t~Q!Y8*U5r5@Wm;#^5w7F_u1>KOf5ofS0 zR!hps-AjmfSr!~H-4vZP_R~ebvFnSezGj0zM_KvHp08KILQiY@E+ay9GrbMFU{Kvgp8lRhorYpZ5m2 zt!f3v^LDrKrbpxHoWW`S^Nj~?72HK7CJh#pvJLB?uE_EidR>zTv5TlrtEHS$JG0;j+l?V9dSy`J+LiZCNws2JVi;dQP+SC? zRP~=wUL3v!(SGq=-($%} z+2&&HOFtOh^r%|>f)(Tl+)10#^k^3ZVKw)l(mw%AD<=R#jYq;ix0H5=hbvjP zD%JsejfN8|+d%W|7OpyB)NhI?p!xA}yCd!zcDzCHq~^&R z7ynV=3RtTP!8aFsX?gxzdTO0qN) zoxyQ!Eu_m9M7pqhunF85L@vWxJ=yiJD(a~LuD`eSt17~LV#~VdpOD#_nz*)>VjF49il}yOd?x&Acl2kW?1rsGyWo=fci;KXRj6uv9ofa`nXD3qNik+IJL;~WlTgzM=(})Z3dMm!{#`4O ztjbr;)@2MObjfKPdY`j$wq9xQQO7qercb1lGQfQ7aTpxYt1i;b$Pnhh&VY_WclmaR z0A{zsxMgFqqGWAY{9N|YQ6rg&uThK=CyrtxsX21FK}<&KRTyoc?PpiWU*53SUGWC7 zM%kPOhjuuvtMw)*&8SlFe`vjUg>D2^Nf+fwJE7!2kE82)~s=TL=jh0|%t8_vp@><0R3+gC!(Y_tHem>`9wLL2Hhi_>_ zZkbUW!%!QtuTYsQ-c-<(UoY^~4yoaxJp!)MVSSCt3FzgZ>LQ4Cd^fZG_TGAxXLk$H zW<}JVnaKtt*DWSvOnc9)(;Ou&s^;78j|XAVZ~6}rz`eR2CcOgtD{_RTd4--^t5c9W zU5)hS{(==Aw=JgxmLlJTZWB)c=~hxsBaw)*4;E~-!73a_4}*Kge6)FFj(gM%%RoAf zW>FtRrUX$aLv`U(?_oTktLG3k`c^dU-^M)=rcaF@ zRkX1XLlo+(+G*cOvb>fjz7!s<4>aNM2!au<8fgbl5ak83==b`O(o72~5;hHtKduwi z#Zp{k2~rb#dZFR{;vRUY4W}b5V%BayxhII79VFIyc5|F4^QhV0J?`FLUWTljCy*@j z^|LwDhAw$PVAzshNENaqUt;E9mR<9|p^>H&QbK7|`;v>^xs`ev6-M|r1i*ayqlN`& zUcwlbvpLI;rx@%#tZ>eYyDabdg*9`UgP=Xn&0$D|iUiT%fYCUg3mxeXdugNmle#g8 zfoPszGubKF?I-!53@zrF%aWPUhDGoN7x##QB~eCMnhd*!QV1c5Y>RQD3NSymm|sQz z2B;jx6w|1PmU7XtmI-M60V#wvW8E~dPonr?V$NtpEWglmC@R^uFd7Xj#?Tn&HcgtT zP`LPvOhdU*PMObtjByuwkJY}eox;|S2{T4B#kO;OG?|iVgCTb}T1amy06&RAD1w{9 zAqvm3@S8T$12%*85)zA~LN}Z4R1_Ut@1}nosM^faR_9#pN~ytH0P^+aks7+1f$HdD z5Q4@$g0PAm6JBl9z{{JX9n$O7Gf>T1dd((mD0^x60qN4RaylD(L<9F2j(1;%NC&+U zv=*{D=~Uj81{EYh2nnyZ^ByLPL>WDuo54|y`DP(wT1Ss@S)@6vBo90-^sSg}6KTSUZ`SIsNUY+qLa&*Lcz2&JEcg z>1&MZvPZ>HE4{(QG8!vg-62LmePdf$nKpO)n%V3La1ljawfDtx`6P)0>{T#5#^E?Q zWSvHvQo*Sxi{j3zK&9u$!K|6VbxA!&XdtN_8G1r3wVkhZ2Fq`;5UmrmZ~N( z+IsSC2#orqm*9Fv8*;bx@C(0%?8pm3-QC+*0j}Mi#n(Blr*1?3o<=Psa;*00B zIr%w_Mdurnle@$xV;NVcYwVhZI$rbl;pO_ zZ;Y|yR9p9$F<$#V{}A_5x(NU9Wht3lVZOJ&UyL7hVhXrXDEi8|ylm#t&DUqt z+hD=y5P3belC9|>-Zt&uuN%UP=Fc0p+kX5fe-6{#4;6pPdpYg!bVwcA+X!1;p zfI~%#XwinmqHL>)*C2x-nkJNB>Xr4bvN^hb;3)p2sr#&IOet|*z0Efhd^rEjg*#Cg znR&PpI&4PWPCXzUzV(7O92xg6{G|P;oGykFu{1U4<3!Tx&}oGKm%!I=xMs^*vch2Y zIJV48@-JJYmg&xXA@&DuOs=tk4i|5l*2<$ zK4aR~4WV2n<;FH~{# zl*n4$aFub~JcOL@JvhqSF5{zVZ32*6&Q z@NC`&Lw*K)Wqma|`&oXzA2?1n=oG|Ol2n_*$(cm2m&9dFYw``Hcyf(M4Ly=}`vhST zI?{y>$6G4TB7eNLvREIY8wk(qc(xxDTKy6}Qp%to&dyZ{b(Gdyjw)J|F+fB6Em_Sg zTDR{R8Ll=abymm?U)OCoMX!yW)FS(-cCY39y96w7vo;?jV@yQB89|k;W{WyW`tDm= zr*!#T)cT;EQQGVCxAEVTyNH)Vlrk?MiJK+&@@=g?S3Aim;E{DPU**|df?MqUitpQG zdCA4{?hR{t-c|3jvvd{ONXoE<33K2i_(U+#U;6lKn(T=OrhgzK*br+sutuF&zRYzy zlJdNl_wD`m-JI;tHz!n!W$~MB@Y1QP+WZdWM$UhXtt4}R4Q=sYmqIz*eL;lqcS~UE zpq-YWQg2X6lmr@^VB{b*oT%p|^;SK(<1tDkodj@0dvfDIkwu1Wp}NP&L~ z;uxCrPxDY9&>G_ZgQOnDcGhObZsz~1DyunWNTUZFXh#)Oin%whL|==$t5FMR={Gyj z$avQ%TS2+5jZ{Z26;vkdcUpRE8Kv}&v=cK4Y>owN@<2Hg%V_KAQY3+MaR+`vl;udO z^(NHI0CA;f%rA^Kx`>Pokp6)uCx9Zh#$nDZ%2W$)$VlOQW@ZYvQ=Cppue7GW>&g|l zGS8~5#Ql@pC#mnPU8o*KN%kT4$&2xV+PU3=DtYuKR!umi$5J8Tz4CSNhcZ%1C!q;V zG=DXS7$??0*XPMBKxW_}FHf!X^IDd1=7K~qAae1Z)gAEuxClCMs5}>$z9R|>5OHR+ zY{tZ8To+P#lo(TU%dK9LGc^~xB?}2L(R&FZ|LlZLHHsW{_SnL-`ogyG033mL{7x6U;H>;-=Ld&@V?;@iP{D^TR{*~~r zf2yCuS4~uvl+y)i9u$6#?IOK>6R3zo-hNB=n*8Ay-oKHw=Us7Ux@n<05;VW^c;y?h zr=<`E-`a}N(0zNFNC8_?do}Xq8@{{iXMnVN^c9L4_9Xb^lF(H~al*~lLw@^C;IZ6Y zsCb`o@c5_@Cm;9EKnQR5Qrv{{4W@v*S9PYik4UL`=O>ti#*w==+xA74xM2-dn}srP>)&?~G%=H?r|T6MoB%g_ zZK-wn$Y{BlA9N2y&m1C$21$wg6kf*xDdgcC-~31F?g)wEu)iWzhVw8dB5vJxe{t&h z_D6Jrs?ah1XFYZRh4QCbEeS-C-rxOFoH-iZ(T}VMn06%+zWFyrmkw}_61Xw7{FFxx z0#K1zc~k79ZjN`g%!Ee@Ybds#49{+h1#J%zoWntH@3uDU(Em+)H**t zDNpq=+woTM#9g<|+nHP6WFUPhneg6}Sck@1y6eMRrnPLI455-?YMJ%Foca~Xd3$6Q z!VtzNGAXD%i_-UUp4(nd9G(8VjOz`e-q4-x!4UR=yI*bSrF3?k7^(D9SR+&{ql2#QV)J7JJWQ8v{uOcXSWrVZ1&<8A^CLlN-FPkt&>&=Q^A&KK2^wv zpw3kI-c9;|5*uz7?fob0>8|xPKfUV2@;Z*9769R!P~%HoH%^iA1O2{?{L~rlytP2W zQ;9}jhLi%>GFQCMbO<-y^Ec{5hj(k`A@Xg5-i#DcL*kIE2(ANqmIRsNc#H2_d-*L! ze=w$xlEbaj?eqa>uSoqcI~Qq+gv+vO5P5{k5pK6n=-1xFY^pB2(oM|*fHR^ec~7Hu z9*{Em<5}CF_sW3&NoNFm65weO66K~Pphcg*kz(i+DvQoeie*4AXu%&?+e0~3ju&9) z`=LzFfX3x`0gB;ExYh<$iRD9H)0pYy^$G(54>zO!9I1A#F zJPl!gjl$bey&M$#4|c9#kyMHegM8(oAV7>f8a$e@KGk}B=tfL$x(JVj78hEi6N-VW zQdCA4Hj6z8=PZQa;gXh0h?;bqN_%(bXVZ@F2veb(XU;@#xhJsg@lFu_=M5nHVodnm z4%T??3$sk8LUEJ+c48O=@~M|&N@YF#7*n0#lBXyf?ziH8m%Fc4`*$bmOPGWUe8xUE zN=mD*jx1G@s+ad{iqiW(A~drPsxqE?I#o5-GPGWcB-}yW z^|UI%IMsMII%KsKoj@(&)k7B<%u`P-^Pr!&GtCc84?o delta 3152 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 delta 3152 zcmV-W46pOJRg5LDssRoe@0VZu0ssJ-4*&oW0Fyx|6MwZ<+in{n5PkPopnXcDl|LX- zpPE|DTU4b_QH03Cro?MlEktTm_1`-Kn9JT2yN(|W!<^xA&MZvRF=NR6pnLQ4?z>=I z3^yABKT1uft)Y~BA(fT36V=8!xM;yCm9&&z8PT;haqep$hd&(MxOHXY?}P^JFV^P z>H9&y48j=Ni=h$9vX>PuX24@+W+1T4%;-f~^T!*rc3ky79W7Egx1$g62+g6lU$tuG zaF|Ob@!M84sq`NI8>Sl3t>hMQBlMxp}Q0Zw^ZJvQSq z2Ba~FqOV4)Q}o=)PTVO-<{2TmFwG!)D}S{@A>mpT%7C$QM`a;3NdaRiGZ<=NEc08;+)x7`S}rvdU-dO)Bmq zb*auCpUMLRuMy_$C7~Vn5sTImGN===-af5#g!6q<{N~b}!JO6V`VXNvSa{cuwZr-g zkfSE|N#?_AB{R>>kYZDlGH3Cw{j)F!83BKTaonb-1^@sk6#xJZ0001RZ*p`mb7OL8 zaC9zkd8Jt0Zre5#zV}mb8ivZ4=d`^H5DY`xtQm$iXwnv25E!&{WV4n?fu!OlMSwoY z_HuoaokLQ8mYl__E@X>3{LbIU!$XOpRN6AS&3P&2rB@PhLo8^ zi_%8y{`v`8M4)BPZKiZ#*a^FeF1LTeq9npZr#ykBQXBezCXMAQ%?oO-0vw4D~*mYA=wY! z*m~rIJ@rzk3SX!2Y1Vi`;vlO6G{vd4#|M$F0xVyo+;Z~ydN_<}E-`Cy9HoD-tSG1$ z?HcI*7~SC7M$-^MZ|ue8t8?}Y-$4$EWp)kBrL>l4O#ks@Hw=wTFzF8^+p+B$QP`!f zptb)XV^MKYT4xNI+{N&(PD#N@cvz5J6Ew^=qUR#v7KwI*G@UUEpX?C|2pRR!Xy#im zRc;Z@b5%R;FC1kNU{RvC9q@lv|E!5mA5&x<*!f~%Z%bG#*vScda?Wpa#nTI`k@rvf z!8s^E=nt+QizuT(qN^5BvT-m~7YbFZaoPuA*OzNJ?MJR-_YKc07^0$c1K{t_Ga+$; zNJo77Uw$cxYc!#Xl2!SPtrWA0B}y7(AFZEa%;OFncWoS%U25Q)VcdUm1K;RHpfB2+ z30*iEk^LI1zI8DUD-Or^`Qc<2`XFlhIbE&I%jrI{cJkU)IZ9hv(0dYmPm&NskULo5n7!}x`vV-jM`FWuBbgv*m9@#c@{8&d1rpQe7%l(FPK&?~iQ2)<)+sMMWyD})5^J{r@|qz%TsPo-~bT;Di9 zr~A9Ja~ykxE_o|)WmH=3_J=V|+`-8eC@RLgUNFo0%oN>GEc6yr#6hb3#J3^WIo4neJ=qatc`98cj! zBt&gI25f)W*Drrx;O6@E%Rh|U4uzM>T2(Z(PnFyL*8B#L!-Hiq!s7QTU!krMgTdf& zj>uyiKWj>P#;S4!7?2f8c&4h7=#QHS*PPoq`^7PGMU)&_6MoJn7aK=woQw{>L3Sf1 zm*k~}WNi&bgZSPM;6E5ZM}ae0RyOV=v6tdNg-3s#A|x*$a3ZJ-E%^cuWK{7G)ta~w z4cHG0c|7`S+EXxA&~|Ii{JAiEo0$J4N@rWErWPEYq^Wx%89Iu)$@Gw8U5Tgtt>5Xd zxBNaTT>`5xB0Go`Sb7|h>a`8@dgdn5hnkZ1p@k85^%z09=T6TMf=V!M3_c}LHXmi? zptXO}$Gg%|=(eN&qOSWekq$esV^15mco$aK93EMhF!VkfbsC2S>P)Uxm6FXhxBFbp z)#@OXzpE7sACX41(c2|xflC9$dy2@@MW!kFs41Pl#OG6bjD0u~l~;u{Cs$ETB>JXQ zr;0vNVRC55{a#PokPVuMy4X^{PjR(eI#*Jn4&M={6YW{YIFIXcGiq#;wXVg~&5~*L z*R|^o%0VVlHrnX6$(eO5dc-bMqsCWG`}K@nwYFThHO4i;8^Av1qrrC4xKAeiiZb#I zb2wx2FS8mWumKHmqISBa0RRAY0ssITlfNSvlkX%4f6&WzqA&n};eDSav>+E@(FGC& z#2Xh&VUvTRCQ?X*Ajs3t>CE)hh3DUYi=WYkzad0w!V!99chKT|qH6EU&^327*jc@R zhJX?wn|1YIcqy6XreBn^t3&o|h#R*Ds{F#fnVgYllrn{Oz%IHwi*f}{+o$`ZHw(uE_s z)$k(VHgAC@8ow56D0_squxDPvMW$cCZ( zld7E54@laNrz}SbhDlRilC6VrV3^UtpKE-Pe_Xl0Xy;!lhw9V>it_(aduRyb4~j2a zyl;Y`FzSyikbT{j@7s+ot|wwNj^~y}Mt+#yw@nxTRgtv6j3B;#SCEzqq(0HN#I^0^ z4+%|OLs$v=esqo@9kmIS+FQ!i(y>1o-NjwZ`;o?bSp>TL99-G43C-C2F{R`ao|U*M zClSM|(2q}tO)*=e6&4IK-k@!uE~wCcUY99QrjzGrXj_{Q0`T$&lT#|Vxaonb-1^@sk6#xJZ00000000000001_fzSn$Iw>EMCnFII z0000000000qyeK0lfWYvlL;y>0xb=bz#|`%St<}5aiVs*r2zl{b^-tZ8vp ValidateInput: + """ + Validates the input of an Instagram zipfile + """ + + validate = ValidateInput(STATUS_CODES, DDP_CATEGORIES) + + try: + paths = [] + with zipfile.ZipFile(zfile, "r") as zf: + for f in zf.namelist(): + p = Path(f) + if p.suffix in (".txt", ".csv", ".pdf"): + logger.debug("Found: %s in zip", p.name) + paths.append(p.name) + + validate.set_status_code(0) + validate.infer_ddp_category(paths) + except zipfile.BadZipFile: + validate.set_status_code(1) + + return validate + + +def extract_users_from_df(df: pd.DataFrame) -> list[str]: + """ + Extracts all users from a netflix csv file + This function expects all users to be present in the first column + of a pd.DataFrame + """ + out = [] + try: + out = df[df.columns[0]].unique().tolist() + out.sort() + except Exception as e: + logger.error("Cannot extract users: %s", e) + + return out + +def filter_user(df: pd.DataFrame, selected_user: str) -> pd.DataFrame: + """ + Keep only the rows where the first column of df is equal to + selected_user + """ + + df = df.loc[df.iloc[:, 0] == selected_user].reset_index(drop=True) + print(df) + return df + + +def split_dataframe(df: pd.DataFrame, row_count: int) -> list[pd.DataFrame]: + """ + NOTE FOR KASPER: + + Port has trouble putting large tables in memory. + Has to be expected. Solution split tables into smaller tables. + I have tried non-bespoke table soluions they did not perform any better + + I hope you have an idea to make tables faster! Would be nice + """ + # Calculate the number of splits needed. + num_splits = int(len(df) / row_count) + (len(df) % row_count > 0) + + # Split the DataFrame into chunks of size row_count. + df_splits = [df[i*row_count:(i+1)*row_count].reset_index(drop=True) for i in range(num_splits)] + + return df_splits diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index 7abb7d71..16eaf5b8 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -1,142 +1,275 @@ +import logging +import json +import io +import inspect + +import pandas as pd + import port.api.props as props +import port.unzipddp as unzipddp +import port.netflix as netflix from port.api.commands import (CommandSystemDonate, CommandUIRender) -import pandas as pd -import zipfile +LOG_STREAM = io.StringIO() +logging.basicConfig( + stream=LOG_STREAM, + level=logging.INFO, + format="%(asctime)s --- %(name)s --- %(levelname)s --- %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S%z", +) -def process(sessionId): - yield donate(f"{sessionId}-tracking", '[{ "message": "user entered script" }]') +LOGGER = logging.getLogger("yolo") - platforms = ["Twitter", "Facebook", "Instagram", "Youtube"] +TABLE_TITLES = { + "netflix_ratings": props.Translatable( + { + "en": "Ratings you gave according to Netlix:", + "nl": "Jouw beoordelingen volgens Netflix:", + } + ), +} - subflows = len(platforms) - steps = 2 - step_percentage = (100/subflows)/steps + +def process(sessionId): + LOGGER.info("Starting the donation flow") + yield donate_logs(f"{sessionId}-tracking") # progress in % + subflows = 1 + steps = 2 + step_percentage = (100 / subflows) / steps progress = 0 + progress += step_percentage - for index, platform in enumerate(platforms): - meta_data = [] - meta_data.append(("debug", f"{platform}: start")) + platform_name = "Netflix" + data = None - # STEP 1: select the file - progress += step_percentage - data = None - while True: - meta_data.append(("debug", f"{platform}: prompt file")) - promptFile = prompt_file(platform, "application/zip, text/plain") - fileResult = yield render_donation_page(platform, promptFile, progress) - if fileResult.__type__ == 'PayloadString': - meta_data.append(("debug", f"{platform}: extracting file")) - extractionResult = doSomethingWithTheFile(platform, fileResult.value) - if extractionResult != 'invalid': - meta_data.append(("debug", f"{platform}: extraction successful, go to consent form")) - data = extractionResult + while True: + LOGGER.info("Prompt for file for %s", platform_name) + yield donate_logs(f"{sessionId}-tracking") + + promptFile = prompt_file("application/zip, text/plain", platform_name) + file_result = yield render_donation_page(platform_name, promptFile, progress) + selected_user = "" + + if file_result.__type__ == "PayloadString": + validation = netflix.validate_zip(file_result.value) + + # Flow logic + # Happy flow: valid DDP, user could be selected + # Retry flow 1: No user was selected, cause multiple reasons see code + # Retry flow 2: No valid Netflix DDP was found + + if validation.ddp_category is not None: + LOGGER.info("Payload for %s", platform_name) + yield donate_logs(f"{sessionId}-tracking") + + # Extract the user + users = extract_users(file_result.value) + if len(users) == 1: + selected_user = users[0] + extraction_result = extract_netflix(file_result.value, selected_user) + data = extraction_result + elif len(users) > 1: + selection = yield prompt_radio_menu_select_username(users, progress) + # If user skips during this process, selected_user remains equal to "" + if selection.__type__ == "PayloadString": + selected_user = selection.value + extraction_result = extract_netflix(file_result.value, selected_user) + data = extraction_result + else: + LOGGER.info("User skipped during user selection") + pass + else: + LOGGER.info("No users could be found in DDP") + pass + + # Enter retry flow, reason: if DDP was not a Netflix DDP + if validation.ddp_category is None: + LOGGER.info("Not a valid %s zip; No payload; prompt retry_confirmation", platform_name) + yield donate_logs(f"{sessionId}-tracking") + retry_result = yield render_donation_page(platform_name, retry_confirmation(platform_name), progress) + + if retry_result.__type__ == "PayloadTrue": + continue + else: + LOGGER.info("Skipped during retry ending flow") + yield donate_logs(f"{sessionId}-tracking") break + + # Enter retry flow, reason: valid DDP but no users could be extracted + if selected_user == "": + LOGGER.info("Selected user is empty after selection, enter retry flow") + yield donate_logs(f"{sessionId}-tracking") + retry_result = yield render_donation_page(platform_name, retry_confirmation(platform_name), progress) + + if retry_result.__type__ == "PayloadTrue": + continue else: - meta_data.append(("debug", f"{platform}: prompt confirmation to retry file selection")) - retry_result = yield render_donation_page(platform, retry_confirmation(platform), progress) - if retry_result.__type__ == 'PayloadTrue': - meta_data.append(("debug", f"{platform}: skip due to invalid file")) - continue - else: - meta_data.append(("debug", f"{platform}: retry prompt file")) - break - else: - meta_data.append(("debug", f"{platform}: skip to next step")) - break + LOGGER.info("Skipped during retry ending flow") + yield donate_logs(f"{sessionId}-tracking") + break + + else: + LOGGER.info("Skipped at file selection ending flow") + yield donate_logs(f"{sessionId}-tracking") + break # STEP 2: ask for consent progress += step_percentage + if data is not None: - meta_data.append(("debug", f"{platform}: prompt consent")) - prompt = prompt_consent(platform, data, meta_data) - consent_result = yield render_donation_page(platform, prompt, progress) + LOGGER.info("Prompt consent; %s", platform_name) + yield donate_logs(f"{sessionId}-tracking") + prompt = prompt_consent(platform_name, data) + consent_result = yield render_donation_page(platform_name, prompt, progress) + if consent_result.__type__ == "PayloadJSON": - meta_data.append(("debug", f"{platform}: donate consent data")) - yield donate(f"{sessionId}-{platform}", consent_result.value) + LOGGER.info("Data donated; %s", platform_name) + yield donate_logs(f"{sessionId}-tracking") + yield donate(platform_name, consent_result.value) + else: + LOGGER.info("Skipped ater reviewing consent: %s", platform_name) + yield donate_logs(f"{sessionId}-tracking") + + break yield render_end_page() -def render_end_page(): - page = props.PropsUIPageEnd() - return CommandUIRender(page) +################################################################## +# helper functions +def prompt_consent(platform_name, data): + table_list = [] -def render_donation_page(platform, body, progress): - header = props.PropsUIHeader(props.Translatable({ - "en": platform, - "nl": platform - })) + for k, v in data.items(): + df = v["data"] + table = props.PropsUIPromptConsentFormTable(f"{platform_name}_{k}", v["title"], df) + table_list.append(table) + return props.PropsUIPromptConsentForm(table_list, []) + + +def return_empty_result_set(): + result = {} + + df = pd.DataFrame(["No data found"], columns=["No data found"]) + result["empty"] = {"data": df, "title": TABLE_TITLES["empty_result_set"]} + + return result + + +def donate_logs(key): + log_string = LOG_STREAM.getvalue() # read the log stream + + if log_string: + log_data = log_string.split("\n") + else: + log_data = ["no logs"] + + return donate(key, json.dumps(log_data)) + + +def extract_users(netflix_zip): + """ + Reads viewing activity and extracts users from the first column + returns list[str] + """ + b = unzipddp.extract_file_from_zip(netflix_zip, "ViewingActivity.csv") + df = unzipddp.read_csv_from_bytes_to_df(b) + users = netflix.extract_users_from_df(df) + return users + + +def prompt_radio_menu_select_username(users, progress): + """ + Prompt selection menu to select which user you are + """ + + title = props.Translatable({ "en": "Select", "nl": "Select" }) + description = props.Translatable({ "en": "Please select your username", "nl": "Selecteer uw gebruikersnaam" }) + + header = props.PropsUIHeader(props.Translatable({"en": "Select", "nl": "Select"})) + radio_items = [{"id": i, "value": username} for i, username in enumerate(users)] + body = props.PropsUIPromptRadioInput(title, description, radio_items) footer = props.PropsUIFooter(progress) - page = props.PropsUIPageDonation(platform, header, body, footer) + + page = props.PropsUIPageDonation("Netflix", header, body, footer) + return CommandUIRender(page) -def retry_confirmation(platform): - text = props.Translatable({ - "en": f"Unfortunately, we cannot process your {platform} file. Continue, if you are sure that you selected the right file. Try again to select a different file.", - "nl": f"Helaas, kunnen we uw {platform} bestand niet verwerken. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen." - }) - ok = props.Translatable({ - "en": "Try again", - "nl": "Probeer opnieuw" - }) - cancel = props.Translatable({ - "en": "Continue", - "nl": "Verder" - }) - return props.PropsUIPromptConfirm(text, ok, cancel) +################################################################## +# Extraction functions +def extract_netflix(netflix_zip, selected_user): + result = {} -def prompt_file(platform, extensions): - description = props.Translatable({ - "en": f"Please follow the download instructions and choose the file that you stored on your device. Click “Skip” at the right bottom, if you do not have a {platform} file. ", - "nl": f"Volg de download instructies en kies het bestand dat u opgeslagen heeft op uw apparaat. Als u geen {platform} bestand heeft klik dan op “Overslaan” rechts onder." - }) + # Extract the ratings + ratings_bytes = unzipddp.extract_file_from_zip(netflix_zip, "Ratings.csv") + df = unzipddp.read_csv_from_bytes_to_df(ratings_bytes) + if not df.empty: + df = netflix.filter_user(df, selected_user) + result["ratings"] = {"data": df, "title": TABLE_TITLES["netflix_ratings"]} - return props.PropsUIPromptFileInput(description, extensions) + # Extract the viewing activity + viewing_activity_bytes = unzipddp.extract_file_from_zip(netflix_zip, "ViewingActivity.csv") + df = unzipddp.read_csv_from_bytes_to_df(viewing_activity_bytes) + + if not df.empty: + df = netflix.filter_user(df, selected_user) + df_list = netflix.split_dataframe(df, 5000) + for i, df in enumerate(df_list): + index = i + 1 + title_translatable = props.Translatable( + { + "en": f"Your viewing activity according to Netlix {index}:", + "nl": f"Jouw kijk activiteit volgens Netflix {index}:", + } + ) + result[f"viewing_activity_{index}"] = {"data": df, "title": title_translatable} + return result -def doSomethingWithTheFile(platform, filename): - return extract_zip_contents(filename) + +########################################## +# Functions provided by Eyra did not change + +def render_end_page(): + page = props.PropsUIPageEnd() + return CommandUIRender(page) -def extract_zip_contents(filename): - names = [] - try: - file = zipfile.ZipFile(filename) - data = [] - for name in file.namelist(): - names.append(name) - info = file.getinfo(name) - data.append((name, info.compress_size, info.file_size)) - return data - except zipfile.error: - return "invalid" +def render_donation_page(platform, body, progress): + header = props.PropsUIHeader(props.Translatable({"en": platform, "nl": platform})) + footer = props.PropsUIFooter(progress) + page = props.PropsUIPageDonation(platform, header, body, footer) + return CommandUIRender(page) -def prompt_consent(id, data, meta_data): - table_title = props.Translatable({ - "en": "Zip file contents", - "nl": "Inhoud zip bestand" - }) +def retry_confirmation(platform): + text = props.Translatable( + { + "en": f"Unfortunately, we could not process your {platform} file. If you are sure that you selected the correct file, press Continue. To select a different file, press Try again.", + "nl": f"Helaas, kunnen we uw {platform} bestand niet verwerken. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen." + } + ) + ok = props.Translatable({"en": "Try again", "nl": "Probeer opnieuw"}) + cancel = props.Translatable({"en": "Continue", "nl": "Verder"}) + return props.PropsUIPromptConfirm(text, ok, cancel) - log_title = props.Translatable({ - "en": "Log messages", - "nl": "Log berichten" - }) - data_frame = pd.DataFrame(data, columns=["filename", "compressed size", "size"]) - table = props.PropsUIPromptConsentFormTable("zip_content", table_title, data_frame) - meta_frame = pd.DataFrame(meta_data, columns=["type", "message"]) - meta_table = props.PropsUIPromptConsentFormTable("log_messages", log_title, meta_frame) - return props.PropsUIPromptConsentForm([table], [meta_table]) +def prompt_file(extensions, platform): + description = props.Translatable( + { + "en": f"Please follow the download instructions and choose the file that you stored on your device. Click “Skip” at the right bottom, if you do not have a file from {platform}.", + "nl": f"Volg de download instructies en kies het bestand dat u opgeslagen heeft op uw apparaat. Als u geen {platform} bestand heeft klik dan op “Overslaan” rechts onder." + } + ) + return props.PropsUIPromptFileInput(description, extensions) def donate(key, json_string): diff --git a/src/framework/processing/py/port/unzipddp.py b/src/framework/processing/py/port/unzipddp.py new file mode 100644 index 00000000..c5c0363e --- /dev/null +++ b/src/framework/processing/py/port/unzipddp.py @@ -0,0 +1,157 @@ +""" +Contains functions to deal with zipfiles +""" + +from pathlib import Path +from typing import Any, Callable +import logging +import zipfile +import json +import csv +import io + +import pandas as pd + +from port.my_exceptions import FileNotFoundInZipError + +logger = logging.getLogger(__name__) + +def extract_file_from_zip(zfile: str, file_to_extract: str) -> io.BytesIO: + """ + Extracts a specific file from a zipfile buffer + Function always returns a buffer + """ + file_to_extract_bytes = io.BytesIO() + + try: + with zipfile.ZipFile(zfile, "r") as zf: + file_found = False + + for f in zf.namelist(): + logger.debug("Contained in zip: %s", f) + if Path(f).name == file_to_extract: + #print('extract_file_from_zip found a message json', f) + + file_to_extract_bytes = io.BytesIO(zf.read(f)) + file_found = True + break + + if not file_found: + raise FileNotFoundInZipError("File not found in zip") + + except zipfile.BadZipFile as e: + logger.error("BadZipFile: %s", e) + except FileNotFoundInZipError as e: + logger.error("File not found: %s: %s", file_to_extract, e) + except Exception as e: + logger.error("Exception was caught: %s", e) + + finally: + return file_to_extract_bytes + + +def _json_reader_bytes(json_bytes: bytes, encoding: str) -> Any: + json_bytes_stream = io.BytesIO(json_bytes) + stream = io.TextIOWrapper(json_bytes_stream, encoding=encoding) + result = json.load(stream) + return result + + +def _json_reader_file(json_file: str, encoding: str) -> Any: + with open(json_file, 'r', encoding=encoding) as f: + result = json.load(f) + return result + + +def _read_json(json_input: Any, json_reader: Callable[[Any, str], Any]) -> dict[Any, Any] | list[Any]: + """ + Dunder function that read json_input and applies json_reader + Performs several checks (see code) and tries different encodings + """ + + out: dict[Any, Any] | list[Any] = {} + + encodings = ["utf8", "utf-8-sig"] + for encoding in encodings: + try: + result = json_reader(json_input, encoding) + + if not isinstance(result, (dict, list)): + raise TypeError("Did not convert bytes to a list or dict, but to another type instead") + + out = result + logger.debug("Succesfully converted json bytes with encoding: %s", encoding) + break + + except json.JSONDecodeError: + logger.error("Cannot decode json with encoding: %s", encoding) + except TypeError as e: + logger.error("%s, could not convert json bytes", e) + break + except Exception as e: + logger.error("%s, could not convert json bytes", e) + break + + return out + + +def read_json_from_bytes(json_bytes: io.BytesIO) -> dict[Any, Any] | list[Any]: + """ + Reads json from io.BytesIO buffer + this function is a wrapper around _read_json + + Function returns {} in case of failure + """ + + out: dict[Any, Any] | list[Any] = {} + try: + b = json_bytes.read() + out = _read_json(b, _json_reader_bytes) + except Exception as e: + logger.error("%s, could not convert json bytes", e) + + return out + + +def read_json_from_file(json_file: str) -> dict[Any, Any] | list[Any]: + """ + Reads json from file + this function is a wrapper around _read_json + + Function returns {} in case of failure + """ + out = _read_json(json_file, _json_reader_file) + return out + + +def read_csv_from_bytes(json_bytes: io.BytesIO) -> list[dict[Any, Any]]: + """ + Reads csv from io.Bytes() + Expects input from extract_file_from_zip + """ + out: list[dict[Any, Any]] = [] + + b = json_bytes.read() + + try: + stream = io.TextIOWrapper(io.BytesIO(b), encoding="utf8") + reader = csv.DictReader(stream) + for row in reader: + out.append(row) + logger.debug("succesfully converted csv bytes with encoding utf8") + + except Exception as e: + logger.error("%s, could not convert csv bytes", e) + + finally: + return out + + +def read_csv_from_bytes_to_df(json_bytes: io.BytesIO) -> pd.DataFrame: + """ + csv to pd.DataFrame + expects io.BytesIO as input (from extract_file_from_zip) + """ + return pd.DataFrame(read_csv_from_bytes(json_bytes)) + + diff --git a/src/framework/processing/py/port/validate.py b/src/framework/processing/py/port/validate.py new file mode 100644 index 00000000..1dda35b3 --- /dev/null +++ b/src/framework/processing/py/port/validate.py @@ -0,0 +1,98 @@ +""" +Contains classes to deal with input validation of DDPs +""" +from dataclasses import dataclass, field +from enum import Enum + +import logging + +logger = logging.getLogger(__name__) + + +class Language(Enum): + """ Languages Enum """ + EN = 1 + NL = 2 + + +class DDPFiletype(Enum): + """ Filetype Enum """ + JSON = 1 + HTML = 2 + CSV = 3 + + +@dataclass +class DDPCategory: + """ + Characteristics that characterize a DDP + """ + id: str + ddp_filetype: DDPFiletype + language: Language + known_files: list[str] + + +@dataclass +class StatusCode: + """ + Can be used to set a status + """ + id: int + description: str + message: str + + +@dataclass +class ValidateInput: + """ + Class containing the results of input validation + """ + + status_codes: list[StatusCode] + ddp_categories: list[DDPCategory] + status_code: StatusCode | None = None + ddp_category: DDPCategory | None = None + + ddp_categories_lookup: dict[str, DDPCategory] = field(init=False) + status_codes_lookup: dict[int, StatusCode] = field(init=False) + + def infer_ddp_category(self, file_list_input: list[str]) -> bool: + """ + Compares a list of files to a list of known files. + From that comparison infer the DDP Category + Note: at least 5% percent of known files should match + """ + prop_category = {} + for identifier, category in self.ddp_categories_lookup.items(): + n_files_found = [ + 1 if f in category.known_files else 0 for f in file_list_input + ] + prop_category[identifier] = sum(n_files_found) / len(category.known_files) * 100 + + if max(prop_category.values()) >= 5: + highest = max(prop_category, key=prop_category.get) # type: ignore + self.ddp_category = self.ddp_categories_lookup[highest] + logger.info("Detected DDP category: %s", self.ddp_category.id) + return True + + logger.info("Not a valid input; not enough files matched when performing input validation") + return False + + def set_status_code(self, code: int) -> None: + """ + Set the status code + """ + self.status_code = self.status_codes_lookup.get(code, None) + + def __post_init__(self) -> None: + for status_code, ddp_category in zip(self.status_codes, self.ddp_categories): + assert isinstance(status_code, StatusCode), "Input is not of class StatusCode" + assert isinstance(ddp_category, DDPCategory), "Input is not of class DDPCategory" + + self.ddp_categories_lookup = { + category.id: category for category in self.ddp_categories + } + self.status_codes_lookup = { + status_code.id: status_code for status_code in self.status_codes + } diff --git a/src/framework/visualisation/react/ui/pages/donation_page.tsx b/src/framework/visualisation/react/ui/pages/donation_page.tsx index f4f45cd0..57e6cf9b 100644 --- a/src/framework/visualisation/react/ui/pages/donation_page.tsx +++ b/src/framework/visualisation/react/ui/pages/donation_page.tsx @@ -59,13 +59,18 @@ export const DonationPage = (props: Props): JSX.Element => { /> ) + //COMMENT BY NIEK: I TURNED OFF THE SIDEBAR (UGLY) + //const sidebar: JSX.Element = ( + // + // } + // /> + //) const sidebar: JSX.Element = ( - - } - /> + <> + ) const body: JSX.Element = ( From 38e321b0024e587d6dbfc20062167a491eeb56b3 Mon Sep 17 00:00:00 2001 From: trbKnl Date: Tue, 2 May 2023 20:52:30 +0200 Subject: [PATCH 02/49] first version working and tested, sets up a baseline for the project, extracts something from netflix, allows participant to select a users, shows how to deal with very large tables --- public/port-0.0.0-py3-none-any.whl | Bin 10936 -> 11041 bytes .../py/dist/port-0.0.0-py3-none-any.whl | Bin 10936 -> 11041 bytes src/framework/processing/py/port/netflix.py | 7 +-- src/framework/processing/py/port/script.py | 45 ++++++++++-------- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/public/port-0.0.0-py3-none-any.whl b/public/port-0.0.0-py3-none-any.whl index 55d08190e96a9994f9b41c1aea410bcc998fd4d9..e5495b97af179b5c5b0d62d1a0f774fce7d4cbf6 100644 GIT binary patch delta 5232 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 V0lXxWK_wlN3o8%?Qzrlb003=Ho?HL` diff --git a/src/framework/processing/py/dist/port-0.0.0-py3-none-any.whl b/src/framework/processing/py/dist/port-0.0.0-py3-none-any.whl index 55d08190e96a9994f9b41c1aea410bcc998fd4d9..e5495b97af179b5c5b0d62d1a0f774fce7d4cbf6 100644 GIT binary patch delta 5232 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 V0lXxWK_wlN3o8%?Qzrlb003=Ho?HL` diff --git a/src/framework/processing/py/port/netflix.py b/src/framework/processing/py/port/netflix.py index 5f7aec8d..e78334e8 100644 --- a/src/framework/processing/py/port/netflix.py +++ b/src/framework/processing/py/port/netflix.py @@ -1,6 +1,5 @@ """ DDP extract Netflix module - """ from pathlib import Path import logging @@ -73,12 +72,10 @@ def extract_users_from_df(df: pd.DataFrame) -> list[str]: def filter_user(df: pd.DataFrame, selected_user: str) -> pd.DataFrame: """ - Keep only the rows where the first column of df is equal to - selected_user + Keep only the rows where the first column of df + is equal to selected_user """ - df = df.loc[df.iloc[:, 0] == selected_user].reset_index(drop=True) - print(df) return df diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index 16eaf5b8..d90223f9 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -1,7 +1,6 @@ import logging import json import io -import inspect import pandas as pd @@ -19,7 +18,7 @@ datefmt="%Y-%m-%dT%H:%M:%S%z", ) -LOGGER = logging.getLogger("yolo") +LOGGER = logging.getLogger("script") TABLE_TITLES = { "netflix_ratings": props.Translatable( @@ -31,9 +30,9 @@ } -def process(sessionId): +def process(session_id): LOGGER.info("Starting the donation flow") - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") # progress in % subflows = 1 @@ -47,7 +46,7 @@ def process(sessionId): while True: LOGGER.info("Prompt for file for %s", platform_name) - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") promptFile = prompt_file("application/zip, text/plain", platform_name) file_result = yield render_donation_page(platform_name, promptFile, progress) @@ -57,13 +56,14 @@ def process(sessionId): validation = netflix.validate_zip(file_result.value) # Flow logic - # Happy flow: valid DDP, user could be selected - # Retry flow 1: No user was selected, cause multiple reasons see code + # Happy flow: Valid DDP, user could be selected + # Retry flow 1: No user was selected, cause could be for multiple reasons see code # Retry flow 2: No valid Netflix DDP was found + # Retry flows are separated for clarity and you can provide different messages to the user if validation.ddp_category is not None: LOGGER.info("Payload for %s", platform_name) - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") # Extract the user users = extract_users(file_result.value) @@ -88,32 +88,32 @@ def process(sessionId): # Enter retry flow, reason: if DDP was not a Netflix DDP if validation.ddp_category is None: LOGGER.info("Not a valid %s zip; No payload; prompt retry_confirmation", platform_name) - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") retry_result = yield render_donation_page(platform_name, retry_confirmation(platform_name), progress) if retry_result.__type__ == "PayloadTrue": continue else: LOGGER.info("Skipped during retry ending flow") - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") break # Enter retry flow, reason: valid DDP but no users could be extracted if selected_user == "": LOGGER.info("Selected user is empty after selection, enter retry flow") - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") retry_result = yield render_donation_page(platform_name, retry_confirmation(platform_name), progress) if retry_result.__type__ == "PayloadTrue": continue else: LOGGER.info("Skipped during retry ending flow") - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") break else: LOGGER.info("Skipped at file selection ending flow") - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") break # STEP 2: ask for consent @@ -121,17 +121,17 @@ def process(sessionId): if data is not None: LOGGER.info("Prompt consent; %s", platform_name) - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") prompt = prompt_consent(platform_name, data) consent_result = yield render_donation_page(platform_name, prompt, progress) if consent_result.__type__ == "PayloadJSON": LOGGER.info("Data donated; %s", platform_name) - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") yield donate(platform_name, consent_result.value) else: LOGGER.info("Skipped ater reviewing consent: %s", platform_name) - yield donate_logs(f"{sessionId}-tracking") + yield donate_logs(f"{session_id}-tracking") break @@ -139,9 +139,13 @@ def process(sessionId): ################################################################## -# helper functions +# helpers def prompt_consent(platform_name, data): + """ + Assembles all donated data in consent form tables + data is the result from extract_netflix() + """ table_list = [] for k, v in data.items(): @@ -163,7 +167,6 @@ def return_empty_result_set(): def donate_logs(key): log_string = LOG_STREAM.getvalue() # read the log stream - if log_string: log_data = log_string.split("\n") else: @@ -190,8 +193,8 @@ def prompt_radio_menu_select_username(users, progress): title = props.Translatable({ "en": "Select", "nl": "Select" }) description = props.Translatable({ "en": "Please select your username", "nl": "Selecteer uw gebruikersnaam" }) - header = props.PropsUIHeader(props.Translatable({"en": "Select", "nl": "Select"})) + radio_items = [{"id": i, "value": username} for i, username in enumerate(users)] body = props.PropsUIPromptRadioInput(title, description, radio_items) footer = props.PropsUIFooter(progress) @@ -205,6 +208,10 @@ def prompt_radio_menu_select_username(users, progress): # Extraction functions def extract_netflix(netflix_zip, selected_user): + """ + Main data extraction function + Assemble all extraction logic here, results are stored in a dict + """ result = {} # Extract the ratings From 2ed0fa4fec49b571320faa0ee3277a6d3f596fa7 Mon Sep 17 00:00:00 2001 From: trbKnl Date: Wed, 3 May 2023 09:11:48 +0200 Subject: [PATCH 03/49] very minor points --- public/port-0.0.0-py3-none-any.whl | Bin 11041 -> 11140 bytes .../py/dist/port-0.0.0-py3-none-any.whl | Bin 11041 -> 11140 bytes src/framework/processing/py/port/netflix.py | 9 ++++++++- src/framework/processing/py/port/script.py | 6 +++--- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/public/port-0.0.0-py3-none-any.whl b/public/port-0.0.0-py3-none-any.whl index e5495b97af179b5c5b0d62d1a0f774fce7d4cbf6..c4f218fa97d4292a7c2df6bb3e69523d37a979b5 100644 GIT binary patch delta 5287 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 ValidateInput: """ Validates the input of an Instagram zipfile + + NOTE FOR KASPER: + This function sets a validation object generated with ValidateInput + This validation object can be read later on to infer possible problems with the zipfile + I dont like this design myself, but I also havent found any alternatives that are better """ validate = ValidateInput(STATUS_CODES, DDP_CATEGORIES) @@ -70,6 +76,7 @@ def extract_users_from_df(df: pd.DataFrame) -> list[str]: return out + def filter_user(df: pd.DataFrame, selected_user: str) -> pd.DataFrame: """ Keep only the rows where the first column of df diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index d90223f9..212a15d2 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -56,7 +56,7 @@ def process(session_id): validation = netflix.validate_zip(file_result.value) # Flow logic - # Happy flow: Valid DDP, user could be selected + # Happy flow: Valid DDP, user was set selected # Retry flow 1: No user was selected, cause could be for multiple reasons see code # Retry flow 2: No valid Netflix DDP was found # Retry flows are separated for clarity and you can provide different messages to the user @@ -73,7 +73,6 @@ def process(session_id): data = extraction_result elif len(users) > 1: selection = yield prompt_radio_menu_select_username(users, progress) - # If user skips during this process, selected_user remains equal to "" if selection.__type__ == "PayloadString": selected_user = selection.value extraction_result = extract_netflix(file_result.value, selected_user) @@ -119,6 +118,7 @@ def process(session_id): # STEP 2: ask for consent progress += step_percentage + # Something got extracted if data is not None: LOGGER.info("Prompt consent; %s", platform_name) yield donate_logs(f"{session_id}-tracking") @@ -127,8 +127,8 @@ def process(session_id): if consent_result.__type__ == "PayloadJSON": LOGGER.info("Data donated; %s", platform_name) - yield donate_logs(f"{session_id}-tracking") yield donate(platform_name, consent_result.value) + yield donate_logs(f"{session_id}-tracking") else: LOGGER.info("Skipped ater reviewing consent: %s", platform_name) yield donate_logs(f"{session_id}-tracking") From ce681270c0d01dd40186dd60f0b9169599244647 Mon Sep 17 00:00:00 2001 From: trbKnl Date: Thu, 25 May 2023 15:16:52 +0200 Subject: [PATCH 04/49] added all tables Dennis wants to have, decoupled the code so its easier to work with --- public/port-0.0.0-py3-none-any.whl | Bin 11140 -> 12683 bytes .../py/dist/port-0.0.0-py3-none-any.whl | Bin 11140 -> 12683 bytes src/framework/processing/py/port/helpers.py | 18 ++ src/framework/processing/py/port/netflix.py | 229 ++++++++++++++++-- src/framework/processing/py/port/script.py | 141 +++++++---- 5 files changed, 327 insertions(+), 61 deletions(-) create mode 100644 src/framework/processing/py/port/helpers.py diff --git a/public/port-0.0.0-py3-none-any.whl b/public/port-0.0.0-py3-none-any.whl index c4f218fa97d4292a7c2df6bb3e69523d37a979b5..ca0a0aa73d1fd97c89c9b56a7a596c1fd4063924 100644 GIT binary patch delta 7334 zcmZ8mWmFu@k{#Tg;0_bqU4lb!cLD?%2pU|13=Y9zu;2~>f(`BxEV%ms!QCPF=Iz3FP1ma&_Zy z_R&q!jGy7c99(~79>vufaX~U=T+#kR>QXAik-43rMqj=1Wils{QM z+Ps;uV3Pum%(aND#8djREC zrKk|pKkk=1<8AMn)#Bn+(SkX3v4WtVl;R;Rs{c!6uIIsHWSp2p)u?;2kKKiVD_=dL~CiOd^v5>jR#xqE^2vM6p4>>dHB_XDDj!=p6=8f4KbM+_z!Iye$8R z^LKHSl1XwE0ri(PUF;cjO^a$U)oiNeYpRWsGwbuu&A=XNHb3~-3h6VovQ5ef{WoUA z1nbYpdKO-wHSl@uylYNciiaxih$HB2SRid*51=+7BlaEBl+v=-EYX0=xFAoQIU!RV zW9bz%m$Q0e!kX*BdY!_C`h}Sr8y||LmO*4-lr`@eD=HulSsL`z(c=UiFA!#%)OR%b zxTHadcstQxXE0_<`iyWS>Xdeb8TB4rV~|&;&)XI?@O)RdWX*(N&Ym>zBeZ#%C&g&@ zD_`ds%T^mu-KiQBPCwqr(^T>XvCAz>dOT&!=)1j>`Q`47O*iJ7u_Es7tRa{&KBs!v z7#%K2#R|1|?@;05tdWnr zJ1P*0!BV9PgcC63{69rQR~dT&)61#f>py_lCCP%>Y6-B*K%`^*71w&F?Tc2(t!#PQ zz_<2Jdl`Fgr^+3AvIvc7vJI7OJ8HFCbWO~h&trE;4`<}8tBPxlfE%e~{-5_#jjQgm zFg@|LGY)uCR>|Ne1UvK$ktIEmh-Di$KXQ|df_F==aG>6t-vz@?7Ko7kEiC+Eq-Ega*qc0Oevg2L0le?xZKZ4;(!4dUU3WUs9bo5=qFzsm+hNJ9&sDkHsKf*%bl3nN79 zX&7aAtM_M#q?B@Di~arDo(jOgUUU&Pl7827eDZ`>t0VMCh7 z2(&=?!DTzn4Z~w{BIUwYsY`!(mvub}E;@7p`P-?Ni^Ke$G#05emA+-&#w!eg8qh-oQ4b=Y2NM(tsewx#LiPyo$2rB|QMr8- z0s43Ic(@z_geoqf}Nk#}APMlQ9VYfujfNE*r49maD26aiLwS953le*6^(!U(DY` zUdtCHYTG!OvJ+`+m94XqV$&N7VS57U5J0MnZvCXs8|Z;^l!CK=(kF;-^5 z^^!%z=SsJ0TC>dIS+1-gMJi$EpJ-N`l~+Z1FL(&x1h>|ZhZi4=Uo^B}kSM$su~$Oi zyoM^pMpSxKvQ@Xtk8akv8o3Bi)_=x(OXZxWYMl~1B)mC`qxaN421G{HCQk^GMq#x#Y``WiQYiH zFLn7kVb?w>%J&{*}wy9HULFsT%gvwTNtlbsyb?`U{FI zsXF{g)7tPSiViQd&t92*W9k%r0e*0eo^fgM+&3<7iBi-Ol!^T+^iG+s)ttBukr%wR z^EVf-q_&PeyBhYkOm@sgtdxSLb5JMa;WrewMa49!>0uH%;15GT#yAEJZ;`y`V6N{n z^`2F~iK(%dN^>nsbj5JJM|?(pVpzUDJ?Ru3_|v6Tu^Jw9gx@1Lsw>O1Q_Zwh*HQQ+ ze!y=`?4?j?28da2R-FeBn;MeIFBDH3ENmz>G2&LypR*Oba~*qJn>x)r2?h;#F)>{b z75@6cn4%Ej4-RMWMq9Hh}l)F20f_BSv(v z?Rk65{tjV9IGaQXTx9+dH0gV;+^7Hc!PYl(m8!Kh7c7uo;hn3)PEk(9ZAREs7&oDT z!UZ0MMJ3jomlfnPc$y=L8naYcuBwdG;;@4hI;IG!z0Hf{H(+umOF=E zO~>KSyFc)o+cy@J4k@!MK5czt332WXnp}`9{Td(l&4J3q9~$%=_4ya$oUG~Yy{yFT zx6Adm_j6$C4wV?exeHg~*R_-_{aFbde=7d~;n|ivyRI&Q+006xoVKIt0F%>kOUP;f z?TvK=v3Z<#w%_U@1wUu{T8=@D#3kCjnc{R8KtmY;k?=3Y{7Ys4w16|$rFT$F0Dy=G z0Kon?@pQ9vwRLv?7p*aVc3Kg_f0Y2MOO>J0Sia#m*?4i;e0RNUl_@6?%y6=qfPrXK z;7|opWYtn!qv;kOS{pfmtYS(qXr^Y*j_o>klSb>{NA!K8wh7OoP2(IXNMc~M=FOS7 zZ4P+WC2aBE?a%RNv(3?v+UVl3mFUe)NEKtND431?v%xoEs8^@NpYgu5WxNY~$)l*` z&@_Q9HExx0!MW%}b$5F^U2f1=IN0xs_8ZOO;lBDN;wmM<;l5<(yA>QawM2GSt(f`T zr_X0=gSmxQ(a=KuvtdMctO*d3WR~A`{#YKq+V_o%BRci_(RKQBf;tO#VcSRKL$`q{ zLxI{3HE{~??D|ZBk(Thqtv?IcVmQr#VW_7EZt2c#*++9Nn^XBMVs=kkDJd{dwpCW` zRcO(S>X?!E@$=y0+2G^eqgjw_#nsgmX_-ZFI`=&`mEn#rU6nWUyS!HKnhKYhrnqNt|B!fb%4D4Twj+6#eNbfu)}6JIuO`iR z`JjX5M_)%nosRfoS*S9@KT&hUDd#U!QVDQPl|o5{9MOe<5pR`!DbHqSm=@a*lobr3 z3I+mGIX7@PFC!TXYYV0#L;rG|bb zX<12zN4ng;MX_}@1kYGNg@_EOK{mzpd1yR92i~c3Mq)n_kI%X31JZ~An<4&Oux@1a z0!#PIX`VUZuEbg@8TwoD;x8r>>6PyKmll}2(;wH&2!fiDu zK#b%ya%rK%J<;WhvW?^Q86$A^%PGMemBc6bh$PR5iB^^cj%FFP5#^_4Q^8xL$$Z1b z7cp0s@GES!<%1XHyNkQlQLh%Y=+P-+hqNE^{`gOk7ooP&ph1hF?gvV^*qEKp0_uF# zE`tRMus@C_B?nG5^4D0xGUKgsJ)SMJqdKHhlB0s(6KEdxeU%(S(pO^-#*rVMp-63xsbg0S^Y-ccq8JGE1UaekF#$geSb(5I;sDlS%?4m0Tqv{b?R8?n0Mq)?gki;cH()n#!Dg z#+I-$vJ>r}_DYy3?_8|OUJezJA3AK-pJ!6?Bht6hH`bL+&P4jVa@FeB+vfu*5T22e z%wmUn)U8DG!(mtM9Xlzc)OE9LP@>Rcv8YE`gywQQ3nG#JYaPHj>(*^57UEj#uQO6` zOQDt|N40#35D@~5OKyIDHrbBvNo22U4w*b1$~`ZDK-V(5sPW86qlmr^!Cl*ImWE1- zQN`q7jP+AU8yM}0s0?0-m_-U48pd!5>h~tm0l~szVhnF~pIosMsb(ISd2b=O8+4L} zse*m`t+sYwW_UP$s9DSCAqMLX3&N0PaK zoMSH$NLKa)v*yc zeYcZs(Uqt@8OV`eMdwAbOsa$RwpboyiagWaipa2V`Xg9FrPpAFGtx*RFB@hSMrA7c zz!2CQ`NvArU5mPTzC*5#>)AZm8`rypU*rV!%IdZ#oNIL*`KOWlCTqeX-&Tz(=CGzQ z8hMtx?HP3B;07GjdCw;$e}ENUPz8awV)(ks@UCvxQ-9?8@*4P`O5C&DGaCBtVG@ok zr(~|B6U8V#h`$a@gb&j_2t#pZ?{(5=N3{Ml?tPl2LiDo)4#X;2x6PhXC8Ee%KO-G& zUwp8kHPA15<7OF~QHe!m6|d;dgWzdDEG%*gSO=pH1>)PqEJBtpI3`I3<^bNv?xZf=?yKA*tc;$Jtchcngb;7Vbx5QG%ovkBK z9z{E7_t}0s>8?_^G~%u1G^Zn5B^H@@BP(a)!g0eXPir7;!N4Un%S7lguZwm$@7x?I zzOPRjii}x*s&h1M@=Pap2WgGaA8z4yn@L!-aT%%o$5j=d6P`6;w}e2N(M3~SjWY6%f1 z+fRR1w`~s$bEq^?^QaFYD^7$LFOJV#=% z=ZUY-Ux^hi*1TN~uVCIxxWa9HEw8yF1(I1->B4Ry%(lxJ3VuHr(@eRsQn>ZY% zbPO8vj?t%oc|V}4XccjHzWH@b#g{*UvI4-ae*(Pi_zo||H3lp3g@Bquuo#zc!fm#E zD&Y{$h_NHS4oF0tc9+0PDVm4KMs!J0JuRyTHo?c4_T{ZQZYzLdH8vU7q~#2JJpLz5 zpwp8@Vb(Z)RC%wfx?L3`UDv{Hs2~HeYH^e&N2`Y|C(HQ}snzc0hU)69FGe))8#u#R zpK({6yv_r3QINF7$01?Jc80ECrBRI-1K8%=6jJQ~2f;Fw5S!Q>spkKQFn^uFo2tZP z36B2FI`XO%fXr>p^-9W%poWSvFaf`N9~ZuaAP+h zz93!+@k5XyU-J!F*`U&1+yt$(ETMh+=C4O)NM#)Vc@o*bz6nr-J?h;NT+)hE@mqqb z`s=Opc=}O?T%T{{d@}wFUEfIhL%_9a6}XE1k|Nm05uQAldLmky3yNxGB04umiYrCx z-CN~@DR55)6}=ohd+pU8ln8hg%$cg-!OR^qXVp+J43uzld{)dC165VUXI2a}Z`UI? z4?9T^FU2s(1ozcp64iHur|5p**^;v>8AR|#363{75&_A?*e}GHQ)qpP9h}4E-wSOZ zwNB7&VSt7vNeQ1(Z!yJe#0T5q17onoi zy+*UR4u!x?!-tt5zMCf0yp!sf-m@J_)S6Q*NWsj!k><3i5JkJ>`>6CZ&al!(;vl=X zXuMe1w8aleypKQmVw`+Rs z;6EiF*F1`u-rtKGK%VGn!C1dU(F3_~k-Ei8hmiE1K0+`evm-xI?&M1^rl@Ld->Dc*+xNyJ6svIFtFP*Tm>Gt*O65ZW z*EsofYV?3o$V8zi4m8fDX6vGC)T`EC&2tG>sX13xent`5KJ3JdA^hg+$2b^J>GNSM{n|1R^yOWVqIOs*EcznV*b5!sLJcbs zjM?~Tqd2<|Q&JXe%V&jwT3A~Lj{CxzU(gWzzBhMs>Fb@^fztKTD9=d9466G_?#Mi( z3#7!^$GzejYK6t^{fU5+Um3^0*6pfAMXOepW>^eY>ca@7QnEg5;^Gt}oY;%i=vGI1 zENLRX=j|%&Y%x9Sl%pktV~-$a9iGc!{B zXUFLOHb_B-3^UVG{AcOq{{SBdAjm8t@S-#jX%;c~FF;5j3oYe86V|`Mf7hZQe`W|A z1M%MkMobC#-{}5pA^Qg^o#iiz0Z^S EUs|2N-v9sr delta 5811 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 Vf(`BxEV%ms!QCPF=Iz3FP1ma&_Zy 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 EUs|2N-v9sr delta 5811 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 V list[pd.DataFrame]: + """ + Port has trouble putting large tables in memory. + Has to be expected. Solution split tables into smaller tables. + I have tried non-bespoke table soluions they did not perform any better + + I hope you have an idea to make tables faster! Would be nice + """ + # Calculate the number of splits needed. + num_splits = int(len(df) / row_count) + (len(df) % row_count > 0) + + # Split the DataFrame into chunks of size row_count. + df_splits = [df[i*row_count:(i+1)*row_count].reset_index(drop=True) for i in range(num_splits)] + + return df_splits + diff --git a/src/framework/processing/py/port/netflix.py b/src/framework/processing/py/port/netflix.py index c00201ef..c00817fd 100644 --- a/src/framework/processing/py/port/netflix.py +++ b/src/framework/processing/py/port/netflix.py @@ -4,9 +4,14 @@ from pathlib import Path import logging import zipfile +import json +from collections import Counter import pandas as pd +import port.api.props as props +import port.unzipddp as unzipddp + from port.validate import ( DDPCategory, Language, @@ -76,30 +81,226 @@ def extract_users_from_df(df: pd.DataFrame) -> list[str]: return out - -def filter_user(df: pd.DataFrame, selected_user: str) -> pd.DataFrame: +def keep_user(df: pd.DataFrame, selected_user: str) -> pd.DataFrame: """ Keep only the rows where the first column of df is equal to selected_user """ - df = df.loc[df.iloc[:, 0] == selected_user].reset_index(drop=True) + try: + df = df.loc[df.iloc[:, 0] == selected_user].reset_index(drop=True) + except Exception as e: + logger.info(e) + return df -def split_dataframe(df: pd.DataFrame, row_count: int) -> list[pd.DataFrame]: +def netflix_to_df(netflix_zip: str, file_name: str, selected_user: str) -> pd.DataFrame: """ - NOTE FOR KASPER: + netflix csv to df + returns empty df in case of error + """ + ratings_bytes = unzipddp.extract_file_from_zip(netflix_zip, file_name) + df = unzipddp.read_csv_from_bytes_to_df(ratings_bytes) + df = keep_user(df, selected_user) + + return df + + +def ratings_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract ratings from netflix zip to df + Only keep the selected user + """ + + columns_to_keep = ["Title Name", "Thumbs Value", "Device Model", "Event Utc Ts"] + columns_to_rename = { + "Event Utc Ts": "Date", + "Device Model": "Device" + } + + df = netflix_to_df(netflix_zip, "Ratings.csv", selected_user) + + # Extraction logic here + try: + if not df.empty: + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df + + +def viewing_activity_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract ViewingActivity from netflix zip to df + Only keep the selected user + """ + + columns_to_keep = ["Start Time","Duration","Attributes","Title","Supplemental Video Type","Device Type"] + columns_to_rename = { + "Device Type": "Device" + } - Port has trouble putting large tables in memory. - Has to be expected. Solution split tables into smaller tables. - I have tried non-bespoke table soluions they did not perform any better + df = netflix_to_df(netflix_zip, "ViewingActivity.csv", selected_user) - I hope you have an idea to make tables faster! Would be nice + # Extraction logic here + try: + if not df.empty: + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df + + +def clickstream_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract Clickstream from netflix zip to df """ - # Calculate the number of splits needed. - num_splits = int(len(df) / row_count) + (len(df) % row_count > 0) - # Split the DataFrame into chunks of size row_count. - df_splits = [df[i*row_count:(i+1)*row_count].reset_index(drop=True) for i in range(num_splits)] + columns_to_keep = ["Source","Navigation Level","Referrer Url","Webpage Url", "Click Utc Ts"] + columns_to_rename = { + "Click Utc Ts": "Time" + } + + df = netflix_to_df(netflix_zip, "Clickstream.csv", selected_user) + + try: + if not df.empty: + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df + +def my_list_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract MyList.csv from netflix zip to df + """ + + columns_to_keep = ["Title Name", "Utc Title Add Date"] + columns_to_rename = { + "Utc Title Add Date": "Date" + } + + df = netflix_to_df(netflix_zip, "MyList.csv", selected_user) + + try: + if not df.empty: + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + print("renamed") + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df + + + +def indicated_preferences_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract MyList.csv from netflix zip to df + """ + + columns_to_keep = ["Show", "Has Watched", "Is Interested", "Event Date"] + columns_to_rename = { + "Event Date": "Date" + } + + df = netflix_to_df(netflix_zip, "IndicatedPreferences.csv", selected_user) + + try: + if not df.empty: + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df + + +def playtraces_counts_to_df(df): + """ + creates a df with counts for playback + """ + out = [] + for item in df["Playtraces"]: + events = json.loads(item) + out.append(Counter([event.get("eventType") for event in events])) + + return pd.DataFrame(out).fillna(0) + + +def playback_related_events_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract PlaybackRelatedEvents.csv from netflix zip to df + """ + + columns_to_keep = ["Title Description", "Device", "Playback Start Utc Ts"] + columns_to_rename = { + "Title Description": "Title", + "Playback Start Utc Ts": "Date time" + } + + df = netflix_to_df(netflix_zip, "PlaybackRelatedEvents.csv", selected_user) + + try: + if not df.empty: + playtraces_df = playtraces_counts_to_df(df) + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + df = df.join(playtraces_df) + + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df + + +def search_history_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract SearchHistory.csv from netflix zip to df + """ + + columns_to_keep = ["Device", "Is Kids", "Query Typed", "Displayed Name", "Action", "Section", "Utc Timestamp"] + columns_to_rename = { + "Utc Timestamp": "Date time" + } + + df = netflix_to_df(netflix_zip, "SearchHistory.csv", selected_user) + + try: + if not df.empty: + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df + + +def messages_sent_by_netflix_to_df(netflix_zip: str, selected_user: str) -> pd.DataFrame: + """ + Extract MessagesSentByNetflix.csv from netflix zip to df + """ + + columns_to_keep = ["Sent Utc Ts", "Message Name", "Channel", "Title Name", "Click Cnt"] + columns_to_rename = { + "Sent Utc Ts": "Date time", + "Click Cnt": "Click Count" + } + + df = netflix_to_df(netflix_zip, "MessagesSentByNetflix.csv", selected_user) + + try: + if not df.empty: + df = df[columns_to_keep] + df = df.rename(columns=columns_to_rename) + except Exception as e: + logger.error("Data extraction error: %s", e) + + return df - return df_splits diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index 212a15d2..902ed3d9 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -5,8 +5,11 @@ import pandas as pd import port.api.props as props +import port.helpers as helpers import port.unzipddp as unzipddp import port.netflix as netflix + + from port.api.commands import (CommandSystemDonate, CommandUIRender) LOG_STREAM = io.StringIO() @@ -42,7 +45,7 @@ def process(session_id): progress += step_percentage platform_name = "Netflix" - data = None + table_list = None while True: LOGGER.info("Prompt for file for %s", platform_name) @@ -70,13 +73,13 @@ def process(session_id): if len(users) == 1: selected_user = users[0] extraction_result = extract_netflix(file_result.value, selected_user) - data = extraction_result + table_list = extraction_result elif len(users) > 1: selection = yield prompt_radio_menu_select_username(users, progress) if selection.__type__ == "PayloadString": selected_user = selection.value extraction_result = extract_netflix(file_result.value, selected_user) - data = extraction_result + table_list = extraction_result else: LOGGER.info("User skipped during user selection") pass @@ -119,10 +122,10 @@ def process(session_id): progress += step_percentage # Something got extracted - if data is not None: + if table_list is not None: LOGGER.info("Prompt consent; %s", platform_name) yield donate_logs(f"{session_id}-tracking") - prompt = prompt_consent(platform_name, data) + prompt = assemble_tables_into_form(table_list) consent_result = yield render_donation_page(platform_name, prompt, progress) if consent_result.__type__ == "PayloadJSON": @@ -139,21 +142,33 @@ def process(session_id): ################################################################## -# helpers -def prompt_consent(platform_name, data): +def assemble_tables_into_form(table_list: list[props.PropsUIPromptConsentFormTable]) -> props.PropsUIPromptConsentForm: """ - Assembles all donated data in consent form tables - data is the result from extract_netflix() + Assembles all donated data in consent form to be displayed """ - table_list = [] + return props.PropsUIPromptConsentForm(table_list, []) - for k, v in data.items(): - df = v["data"] - table = props.PropsUIPromptConsentFormTable(f"{platform_name}_{k}", v["title"], df) - table_list.append(table) - return props.PropsUIPromptConsentForm(table_list, []) +def create_consent_form_tables(unique_table_id: str, title: props.Translatable, df: pd.DataFrame) -> list[props.PropsUIPromptConsentFormTable]: + """ + This function chunks extracted data into tables of 5000 rows that can be renderd on screen + """ + + df_list = helpers.split_dataframe(df, 5000) + out = [] + + if len(df_list) == 1: + table = props.PropsUIPromptConsentFormTable(unique_table_id, title, df_list[0]) + out.append(table) + else: + for i, df in enumerate(df_list): + index = i + 1 + title_with_index = props.Translatable({lang: f"{val} {index}" for lang, val in title.translations.items()}) + table = props.PropsUIPromptConsentFormTable(f"{unique_table_id}_{index}", title_with_index, df) + out.append(table) + + return out def return_empty_result_set(): @@ -175,17 +190,6 @@ def donate_logs(key): return donate(key, json.dumps(log_data)) -def extract_users(netflix_zip): - """ - Reads viewing activity and extracts users from the first column - returns list[str] - """ - b = unzipddp.extract_file_from_zip(netflix_zip, "ViewingActivity.csv") - df = unzipddp.read_csv_from_bytes_to_df(b) - users = netflix.extract_users_from_df(df) - return users - - def prompt_radio_menu_select_username(users, progress): """ Prompt selection menu to select which user you are @@ -205,40 +209,83 @@ def prompt_radio_menu_select_username(users, progress): ################################################################## -# Extraction functions +# Extraction function -def extract_netflix(netflix_zip, selected_user): +def extract_netflix(netflix_zip: str, selected_user: str) -> list[props.PropsUIPromptConsentFormTable]: """ Main data extraction function Assemble all extraction logic here, results are stored in a dict """ - result = {} + tables_to_render = [] # Extract the ratings - ratings_bytes = unzipddp.extract_file_from_zip(netflix_zip, "Ratings.csv") - df = unzipddp.read_csv_from_bytes_to_df(ratings_bytes) + df = netflix.ratings_to_df(netflix_zip, selected_user) if not df.empty: - df = netflix.filter_user(df, selected_user) - result["ratings"] = {"data": df, "title": TABLE_TITLES["netflix_ratings"]} + table_title = props.Translatable({"en": "Netflix ratings", "nl": "Netflix ratings"}) + tables = create_consent_form_tables("netflix_ratings", table_title, df) + tables_to_render.extend(tables) # Extract the viewing activity - viewing_activity_bytes = unzipddp.extract_file_from_zip(netflix_zip, "ViewingActivity.csv") - df = unzipddp.read_csv_from_bytes_to_df(viewing_activity_bytes) + df = netflix.viewing_activity_to_df(netflix_zip, selected_user) + if not df.empty: + table_title = props.Translatable({"en": "Netflix viewings", "nl": "Netflix viewings"}) + tables = create_consent_form_tables("netflix_viewings", table_title, df) + tables_to_render.extend(tables) + # Extract the clickstream + df = netflix.clickstream_to_df(netflix_zip, selected_user) if not df.empty: - df = netflix.filter_user(df, selected_user) - df_list = netflix.split_dataframe(df, 5000) - for i, df in enumerate(df_list): - index = i + 1 - title_translatable = props.Translatable( - { - "en": f"Your viewing activity according to Netlix {index}:", - "nl": f"Jouw kijk activiteit volgens Netflix {index}:", - } - ) - result[f"viewing_activity_{index}"] = {"data": df, "title": title_translatable} + table_title = props.Translatable({"en": "Netflix clickstream", "nl": "Netflix clickstream"}) + tables = create_consent_form_tables("netflix_clickstream", table_title, df) + tables_to_render.extend(tables) - return result + # Extract my list + df = netflix.my_list_to_df(netflix_zip, selected_user) + if not df.empty: + table_title = props.Translatable({"en": "Netflix bookmarks", "nl": "Netflix bookmarks"}) + tables = create_consent_form_tables("netflix_my_list", table_title, df) + tables_to_render.extend(tables) + + # Extract Indicated preferences + df = netflix.my_list_to_df(netflix_zip, selected_user) + if not df.empty: + table_title = props.Translatable({"en": "Netflix indicated preferences", "nl": "Netflix indicated preferences"}) + tables = create_consent_form_tables("netflix_indicated_preferences", table_title, df) + tables_to_render.extend(tables) + + # Extract playback related events + df = netflix.playback_related_events_to_df(netflix_zip, selected_user) + if not df.empty: + table_title = props.Translatable({"en": "Netflix playback related events", "nl": "Netflix playback related events"}) + tables = create_consent_form_tables("netflix_playback", table_title, df) + tables_to_render.extend(tables) + + # Extract search history + df = netflix.search_history_to_df(netflix_zip, selected_user) + if not df.empty: + table_title = props.Translatable({"en": "Netflix search history", "nl": "Netflix search history"}) + tables = create_consent_form_tables("netflix_search", table_title, df) + tables_to_render.extend(tables) + + # Extract messages sent by netflix + df = netflix.messages_sent_by_netflix_to_df(netflix_zip, selected_user) + if not df.empty: + table_title = props.Translatable({"en": "Netflix messages", "nl": "Netflix messages"}) + tables = create_consent_form_tables("netflix_messages", table_title, df) + tables_to_render.extend(tables) + + return tables_to_render + + +def extract_users(netflix_zip): + """ + Reads viewing activity and extracts users from the first column + returns list[str] + """ + b = unzipddp.extract_file_from_zip(netflix_zip, "ViewingActivity.csv") + df = unzipddp.read_csv_from_bytes_to_df(b) + users = netflix.extract_users_from_df(df) + return users ########################################## From 97fbdc5c70b1498f75307341b701148eb4735c34 Mon Sep 17 00:00:00 2001 From: Kasper Welbers Date: Sun, 4 Jun 2023 18:52:51 +0200 Subject: [PATCH 05/49] refactored Table to enable syncing with Visualizations --- .vscode/settings.json | 20 + package-lock.json | 107 ++-- public/port-0.0.0-py3-none-any.whl | Bin 12683 -> 13156 bytes .../py/dist/port-0.0.0-py3-none-any.whl | Bin 12683 -> 13156 bytes src/framework/processing/py/port/api/props.py | 31 +- src/framework/processing/py/port/script.py | 30 +- src/framework/types/data_visualization.ts | 15 + src/framework/types/pages.ts | 19 +- src/framework/types/prompts.ts | 33 +- .../data_visualization/data_visualization.tsx | 11 + .../react/ui/elements/search_bar.tsx | 5 +- .../visualisation/react/ui/elements/table.tsx | 504 ++++++------------ .../react/ui/pages/donation_page.tsx | 33 +- .../react/ui/prompts/consent_form.tsx | 221 +++++--- 14 files changed, 497 insertions(+), 532 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/framework/types/data_visualization.ts create mode 100644 src/framework/visualisation/react/ui/data_visualization/data_visualization.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9ef1cde0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "prettier.trailingComma": "none", + "prettier.tabWidth": 2, + "prettier.semi": false, + "prettier.singleQuote": true, + "prettier.printWidth": 150, + "search.exclude": { + "**/node_modules": true, + "**/dist": true + }, + "tailwindCSS.includeLanguages": { + "html": "html", + "javascript": "javascript", + "typescript": "typescript", + "css": "css" + }, + "editor.quickSuggestions": { + "strings": true + } +} diff --git a/package-lock.json b/package-lock.json index ab87217c..0e086bd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10843,6 +10843,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -13755,6 +13756,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13886,6 +13888,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -14574,6 +14577,7 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -19341,15 +19345,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "dev": true, - "requires": {} + "dev": true }, "@csstools/selector-specificity": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", - "dev": true, - "requires": {} + "dev": true }, "@eslint/eslintrc": { "version": "1.3.2", @@ -20860,15 +20862,13 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} + "dev": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-node": { "version": "1.8.2", @@ -20965,8 +20965,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "ansi-colors": { "version": "4.1.3", @@ -21246,8 +21245,7 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "dev": true, - "requires": {} + "dev": true }, "babel-plugin-polyfill-corejs2": { "version": "0.3.3", @@ -21993,8 +21991,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", - "dev": true, - "requires": {} + "dev": true }, "css-has-pseudo": { "version": "3.0.4", @@ -22097,8 +22094,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "dev": true, - "requires": {} + "dev": true }, "css-select": { "version": "4.3.0", @@ -22212,8 +22208,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -23208,8 +23203,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-testing-library": { "version": "5.7.0", @@ -24329,8 +24323,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "idb": { "version": "7.1.0", @@ -25015,8 +25008,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "27.5.1", @@ -25741,6 +25733,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -26789,8 +26782,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-calc": { "version": "8.2.4", @@ -26900,29 +26892,25 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-double-position-gradients": { "version": "3.1.2", @@ -26947,8 +26935,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-focus-visible": { "version": "6.0.4", @@ -26972,15 +26959,13 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "dev": true, - "requires": {} + "dev": true }, "postcss-gap-properties": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-image-set-function": { "version": "4.0.7", @@ -27006,8 +26991,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-js": { "version": "4.0.0", @@ -27064,15 +27048,13 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "dev": true, - "requires": {} + "dev": true }, "postcss-media-minmax": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-merge-longhand": { "version": "5.1.6", @@ -27140,8 +27122,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -27206,8 +27187,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -27312,8 +27292,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-place": { "version": "7.0.5", @@ -27413,8 +27392,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-selector-not": { "version": "6.0.1", @@ -27720,6 +27698,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, "requires": { "loose-envify": "^1.1.0" } @@ -27823,6 +27802,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -28326,6 +28306,7 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, "requires": { "loose-envify": "^1.1.0" } @@ -28914,8 +28895,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true, - "requires": {} + "dev": true }, "stylehacks": { "version": "5.1.0", @@ -29578,15 +29558,13 @@ "version": "16.0.3", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", - "dev": true, - "requires": {} + "dev": true }, "eslint-config-standard-jsx": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-10.0.0.tgz", "integrity": "sha512-hLeA2f5e06W1xyr/93/QJulN/rLbUVUmqTlexv9PRKHFwEC9ffJcH2LvJhMoEqYQBEYafedgGZXH2W8NUpt5lA==", - "dev": true, - "requires": {} + "dev": true }, "eslint-config-standard-with-typescript": { "version": "21.0.1", @@ -29602,8 +29580,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz", "integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "5.1.1", @@ -30232,8 +30209,7 @@ "version": "8.9.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -30689,8 +30665,7 @@ "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true, - "requires": {} + "dev": true }, "xdg-basedir": { "version": "4.0.0", diff --git a/public/port-0.0.0-py3-none-any.whl b/public/port-0.0.0-py3-none-any.whl index ca0a0aa73d1fd97c89c9b56a7a596c1fd4063924..8705d412c4516747c9ffff901b0d8260de1a91c2 100644 GIT binary patch delta 6065 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 C+ntO6 diff --git a/src/framework/processing/py/dist/port-0.0.0-py3-none-any.whl b/src/framework/processing/py/dist/port-0.0.0-py3-none-any.whl index ca0a0aa73d1fd97c89c9b56a7a596c1fd4063924..8705d412c4516747c9ffff901b0d8260de1a91c2 100644 GIT binary patch delta 6065 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 C+ntO6 diff --git a/src/framework/processing/py/port/api/props.py b/src/framework/processing/py/port/api/props.py index db8aa782..ed12982e 100644 --- a/src/framework/processing/py/port/api/props.py +++ b/src/framework/processing/py/port/api/props.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import TypedDict +from typing import TypedDict, Optional import pandas as pd @@ -101,10 +101,29 @@ def toDict(self): dict["title"] = self.title.toDict() dict["data_frame"] = self.data_frame.to_json() return dict + +@dataclass +class PropsUIDataVisualization: + """Instructions for which ConsentFormTables to visualize and how + + Attributes: + id: id of the table (same as in PropsUIPromptConsentFormTable) to use as data + title: title of the visualization + settings: configuration of the visualization. What type of visualization, what columns to use, etc. + """ + id: str + settings: dict + + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIDataVisualization" + dict["id"] = self.id + dict["settings"] = self.settings + return dict @dataclass class PropsUIPromptConsentForm: - """Tables to be shown to the participant prior to donation + """Tables and Visualization to be shown to the participant prior to donation Attributes: tables: a list of tables @@ -112,6 +131,7 @@ class PropsUIPromptConsentForm: """ tables: list[PropsUIPromptConsentFormTable] meta_tables: list[PropsUIPromptConsentFormTable] + visualizations: Optional[list[PropsUIDataVisualization]] = None def translate_tables(self): output = [] @@ -125,11 +145,17 @@ def translate_meta_tables(self): output.append(table.toDict()) return output + def translate_visualizations(self): + if self.visualizations is None: + return None + return [vis.toDict() for vis in self.visualizations] + def toDict(self): dict = {} dict["__type__"] = "PropsUIPromptConsentForm" dict["tables"] = self.translate_tables() dict["metaTables"] = self.translate_meta_tables() + dict["visualizations"] = self.translate_visualizations() return dict @@ -210,6 +236,7 @@ def toDict(self): dict["body"] = self.body.toDict() dict["footer"] = self.footer.toDict() return dict + class PropsUIPageEnd: diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index 902ed3d9..7a7d03b8 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -121,11 +121,10 @@ def process(session_id): # STEP 2: ask for consent progress += step_percentage - # Something got extracted if table_list is not None: LOGGER.info("Prompt consent; %s", platform_name) yield donate_logs(f"{session_id}-tracking") - prompt = assemble_tables_into_form(table_list) + prompt = create_consent_form(table_list) consent_result = yield render_donation_page(platform_name, prompt, progress) if consent_result.__type__ == "PayloadJSON": @@ -143,16 +142,18 @@ def process(session_id): ################################################################## -def assemble_tables_into_form(table_list: list[props.PropsUIPromptConsentFormTable]) -> props.PropsUIPromptConsentForm: +def create_consent_form(table_list: list[props.PropsUIPromptConsentFormTable]) -> props.PropsUIPromptConsentForm: """ Assembles all donated data in consent form to be displayed """ - return props.PropsUIPromptConsentForm(table_list, []) + return props.PropsUIPromptConsentForm(table_list, meta_tables=[], visualizations=specify_visualizations()) def create_consent_form_tables(unique_table_id: str, title: props.Translatable, df: pd.DataFrame) -> list[props.PropsUIPromptConsentFormTable]: """ This function chunks extracted data into tables of 5000 rows that can be renderd on screen + + COMMENT: is chunking necessary? I don't think it matters how many rows a table has as long as UI doesn't render it all at once """ df_list = helpers.split_dataframe(df, 5000) @@ -170,6 +171,11 @@ def create_consent_form_tables(unique_table_id: str, title: props.Translatable, return out +def create_consent_form_visualization(unique_vis_id: str, title: props.Translatable, df: pd.DataFrame, settings: dict) -> props.PropsUIDataVisualization: + """ + This function creates a visualization of the extracted data + """ + return props.PropsUIDataVisualization(unique_vis_id, title, df, settings) def return_empty_result_set(): result = {} @@ -215,9 +221,11 @@ def extract_netflix(netflix_zip: str, selected_user: str) -> list[props.PropsUIP """ Main data extraction function Assemble all extraction logic here, results are stored in a dict + + COMMENT: does this also make sense as the place to formulate data visualizations? """ tables_to_render = [] - + # Extract the ratings df = netflix.ratings_to_df(netflix_zip, selected_user) if not df.empty: @@ -231,7 +239,7 @@ def extract_netflix(netflix_zip: str, selected_user: str) -> list[props.PropsUIP table_title = props.Translatable({"en": "Netflix viewings", "nl": "Netflix viewings"}) tables = create_consent_form_tables("netflix_viewings", table_title, df) tables_to_render.extend(tables) - + # Extract the clickstream df = netflix.clickstream_to_df(netflix_zip, selected_user) if not df.empty: @@ -288,6 +296,16 @@ def extract_users(netflix_zip): return users +################################################################## +# Visualization settings + +def specify_visualizations(): + settings = dict(type="keyword_frequency", keyword="title") + most_viewed = props.PropsUIDataVisualization(id="netflix_viewings", settings=settings) + + return [most_viewed] + + ########################################## # Functions provided by Eyra did not change diff --git a/src/framework/types/data_visualization.ts b/src/framework/types/data_visualization.ts new file mode 100644 index 00000000..e06e9c92 --- /dev/null +++ b/src/framework/types/data_visualization.ts @@ -0,0 +1,15 @@ +import { isInstanceOf } from '../helpers' + +export interface VisualizationSettings { + type: 'keyword_frequency' | 'word_frequency' | 'something_else' + keyword?: string +} + +export interface PropsUIDataVisualization { + __type__: 'PropsUIDataVisualization' + id: string + settings: VisualizationSettings +} +// export function isPropsUIDataVisualization(arg: any): arg is PropsUIDataVisualization { +// return isInstanceOf(arg, 'PropsUIDataVisualization', ['id', 'title', 'data_frame', 'settings']) +// } diff --git a/src/framework/types/pages.ts b/src/framework/types/pages.ts index d2107e7e..2098b05e 100644 --- a/src/framework/types/pages.ts +++ b/src/framework/types/pages.ts @@ -2,23 +2,16 @@ import { isInstanceOf } from '../helpers' import { PropsUIFooter, PropsUIHeader } from './elements' import { PropsUIPromptFileInput, PropsUIPromptConfirm, PropsUIPromptConsentForm, PropsUIPromptRadioInput } from './prompts' -export type PropsUIPage = - PropsUIPageSplashScreen | - PropsUIPageDonation | - PropsUIPageEnd +export type PropsUIPage = PropsUIPageSplashScreen | PropsUIPageDonation | PropsUIPageEnd -export function isPropsUIPage (arg: any): arg is PropsUIPage { - return ( - isPropsUIPageSplashScreen(arg) || - isPropsUIPageDonation(arg) || - isPropsUIPageEnd(arg) - ) +export function isPropsUIPage(arg: any): arg is PropsUIPage { + return isPropsUIPageSplashScreen(arg) || isPropsUIPageDonation(arg) || isPropsUIPageEnd(arg) } export interface PropsUIPageSplashScreen { __type__: 'PropsUIPageSplashScreen' } -export function isPropsUIPageSplashScreen (arg: any): arg is PropsUIPageSplashScreen { +export function isPropsUIPageSplashScreen(arg: any): arg is PropsUIPageSplashScreen { return isInstanceOf(arg, 'PropsUIPageSplashScreen', []) } @@ -29,13 +22,13 @@ export interface PropsUIPageDonation { body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptConsentForm | PropsUIPromptRadioInput footer: PropsUIFooter } -export function isPropsUIPageDonation (arg: any): arg is PropsUIPageDonation { +export function isPropsUIPageDonation(arg: any): arg is PropsUIPageDonation { return isInstanceOf(arg, 'PropsUIPageDonation', ['platform', 'header', 'body', 'footer']) } export interface PropsUIPageEnd { __type__: 'PropsUIPageEnd' } -export function isPropsUIPageEnd (arg: any): arg is PropsUIPageEnd { +export function isPropsUIPageEnd(arg: any): arg is PropsUIPageEnd { return isInstanceOf(arg, 'PropsUIPageEnd', []) } diff --git a/src/framework/types/prompts.ts b/src/framework/types/prompts.ts index fc6cd0cb..d5567031 100644 --- a/src/framework/types/prompts.ts +++ b/src/framework/types/prompts.ts @@ -1,16 +1,11 @@ import { isInstanceOf } from '../helpers' +import { VisualizationSettings } from './data_visualization' import { PropsUIRadioItem, Text } from './elements' -export type PropsUIPrompt = - PropsUIPromptFileInput | - PropsUIPromptRadioInput | - PropsUIPromptConsentForm | - PropsUIPromptConfirm +export type PropsUIPrompt = PropsUIPromptFileInput | PropsUIPromptRadioInput | PropsUIPromptConsentForm | PropsUIPromptConfirm -export function isPropsUIPrompt (arg: any): arg is PropsUIPrompt { - return isPropsUIPromptFileInput(arg) || - isPropsUIPromptRadioInput(arg) || - isPropsUIPromptConsentForm(arg) +export function isPropsUIPrompt(arg: any): arg is PropsUIPrompt { + return isPropsUIPromptFileInput(arg) || isPropsUIPromptRadioInput(arg) || isPropsUIPromptConsentForm(arg) } export interface PropsUIPromptConfirm { @@ -19,7 +14,7 @@ export interface PropsUIPromptConfirm { ok: Text cancel: Text } -export function isPropsUIPromptConfirm (arg: any): arg is PropsUIPromptConfirm { +export function isPropsUIPromptConfirm(arg: any): arg is PropsUIPromptConfirm { return isInstanceOf(arg, 'PropsUIPromptConfirm', ['text', 'ok', 'cancel']) } @@ -28,7 +23,7 @@ export interface PropsUIPromptFileInput { description: Text extensions: string } -export function isPropsUIPromptFileInput (arg: any): arg is PropsUIPromptFileInput { +export function isPropsUIPromptFileInput(arg: any): arg is PropsUIPromptFileInput { return isInstanceOf(arg, 'PropsUIPromptFileInput', ['description', 'extensions']) } @@ -38,15 +33,16 @@ export interface PropsUIPromptRadioInput { description: Text items: PropsUIRadioItem[] } -export function isPropsUIPromptRadioInput (arg: any): arg is PropsUIPromptRadioInput { +export function isPropsUIPromptRadioInput(arg: any): arg is PropsUIPromptRadioInput { return isInstanceOf(arg, 'PropsUIPromptRadioInput', ['title', 'description', 'items']) } export interface PropsUIPromptConsentForm { __type__: 'PropsUIPromptConsentForm' tables: PropsUIPromptConsentFormTable[] metaTables: PropsUIPromptConsentFormTable[] + visualizations: PropsUIPromptConsentFormVisualization[] } -export function isPropsUIPromptConsentForm (arg: any): arg is PropsUIPromptConsentForm { +export function isPropsUIPromptConsentForm(arg: any): arg is PropsUIPromptConsentForm { return isInstanceOf(arg, 'PropsUIPromptConsentForm', ['tables', 'metaTables']) } @@ -57,6 +53,15 @@ export interface PropsUIPromptConsentFormTable { description: Text data_frame: any } -export function isPropsUIPromptConsentFormTable (arg: any): arg is PropsUIPromptConsentFormTable { +export function isPropsUIPromptConsentFormTable(arg: any): arg is PropsUIPromptConsentFormTable { return isInstanceOf(arg, 'PropsUIPromptConsentFormTable', ['id', 'title', 'description', 'data_frame']) } + +export interface PropsUIPromptConsentFormVisualization { + __type__: 'PropsUIPromptConsentFormVisualization' + id: string + settings: VisualizationSettings[] +} +export function isPropsUIPromptConsentFormVisualization(arg: any): arg is PropsUIPromptConsentFormVisualization { + return isInstanceOf(arg, 'PropsUIPromptConsentFormVisualization', ['id', 'settings']) +} diff --git a/src/framework/visualisation/react/ui/data_visualization/data_visualization.tsx b/src/framework/visualisation/react/ui/data_visualization/data_visualization.tsx new file mode 100644 index 00000000..abb42643 --- /dev/null +++ b/src/framework/visualisation/react/ui/data_visualization/data_visualization.tsx @@ -0,0 +1,11 @@ +import { Weak } from '../../../../helpers' +import { ReactFactoryContext } from '../../factory' +import { PropsUIDataVisualization } from '../../../../types/data_visualization' + +type Props = Weak & ReactFactoryContext + +export const DataVisualization = (props: Props): JSX.Element => { + console.log(props) + + return <>test this +} diff --git a/src/framework/visualisation/react/ui/elements/search_bar.tsx b/src/framework/visualisation/react/ui/elements/search_bar.tsx index 9d4bf714..14ced617 100644 --- a/src/framework/visualisation/react/ui/elements/search_bar.tsx +++ b/src/framework/visualisation/react/ui/elements/search_bar.tsx @@ -4,10 +4,9 @@ import { PropsUISearchBar } from '../../../../types/elements' import _ from 'lodash' export const SearchBar = ({ placeholder, debounce = 1000, onSearch }: Weak): JSX.Element => { - - function handleKeyPress (event: React.KeyboardEvent): void { + function handleKeyPress (event: React.KeyboardEvent): void { if (event.key === 'Enter') { - event.preventDefault(); + event.preventDefault() } } diff --git a/src/framework/visualisation/react/ui/elements/table.tsx b/src/framework/visualisation/react/ui/elements/table.tsx index 248019ef..bb54721c 100644 --- a/src/framework/visualisation/react/ui/elements/table.tsx +++ b/src/framework/visualisation/react/ui/elements/table.tsx @@ -1,5 +1,5 @@ -import _ from 'lodash' -import React from 'react' +import _, { get } from 'lodash' +import React, { useEffect, useMemo, useState } from 'react' import { Weak } from '../../../../helpers' import TextBundle from '../../../../text_bundle' import { Translator } from '../../../../translator' @@ -16,187 +16,127 @@ import { PageIcon } from './page_icon' type Props = Weak & TableContext & ReactFactoryContext export interface TableContext { - onChange: (id: string, rows: PropsUITableRow[]) => void + deletedRowCount: number + handleDelete: (tableId: string, rowIds: string[]) => void + handleUndo: (tableId: string) => void } -interface Visibility { - search: boolean - undo: boolean - delete: boolean - table: boolean - noData: boolean - noDataLeft: boolean - noResults: boolean -} +export const Table = ({ id, head, body, deletedRowCount, readOnly = false, pageSize = 7, locale, handleDelete, handleUndo }: Props): JSX.Element => { + const [query, setQuery] = useState([]) + const [rows, setRows] = useState(filterRows(body.rows, query)) + const [page, setPage] = useState(0) + const [pageRows, setPageRows] = useState(rows.slice(0, pageSize)) + const [adjust, setAdjust] = useState(false) + const [selected, setSelected] = useState([]) -interface State { - edit: boolean - page: number - pageCount: number - pageWindow: number[] - rows: PropsUITableRow[] - selected: string[] - deletedCount: number - visibility: Visibility -} + const pageCount = Math.ceil(rows.length / pageSize) + const pageWindow = determinePageWindow(page, pageCount) -export const Table = ({ id, head, body, readOnly = false, pageSize = 7, locale, onChange }: Props): JSX.Element => { - const pageWindowLegSize = 3 + const noData = body.rows.length === 0 && deletedRowCount === 0 - const query = React.useRef([]) - const alteredRows = React.useRef(body.rows) - const filteredRows = React.useRef(alteredRows.current) - - const initialState: State = { - edit: false, - pageCount: getPageCount(), - page: 0, - pageWindow: updatePageWindow(0), - rows: updateRows(0), - selected: [], - deletedCount: 0, - visibility: { - search: alteredRows.current.length > pageSize, - delete: false, - undo: false, - table: filteredRows.current.length > 0, - noData: filteredRows.current.length === 0, - noDataLeft: false, - noResults: false - } - } + useEffect(() => { + setRows(filterRows(body.rows, query)) + setSelected([]) + }, [body.rows, query]) - const [state, setState] = React.useState(initialState) + useEffect(() => { + const pageCount = Math.ceil(rows.length / pageSize) + const safePage = Math.min(page, pageCount - 1) + setPage(safePage) + setPageRows(rows.slice(page * pageSize, (page + 1) * pageSize)) + setSelected([]) + }, [rows, page, pageSize]) const copy = prepareCopy(locale) - function display (element: keyof Visibility): string { - return visible(element) ? '' : 'hidden' - } - - function visible (element: keyof Visibility): boolean { - if (typeof state.visibility[element] === 'boolean') { - return state.visibility[element] - } - return false - } - - function updatePageWindow (currentPage: number): number[] { - const pageWindowSize = (pageWindowLegSize * 2) + 1 - const pageCount = getPageCount() - - let range: number[] = [] - if (pageWindowSize >= pageCount && pageCount > 0) { - range = _.range(0, Math.min(pageCount, pageWindowSize)) - } else if (pageWindowSize < pageCount) { - const maxIndex = pageCount - 1 - - let start: number - let end: number - - if (currentPage < pageWindowLegSize) { - // begin - start = 0 - end = Math.min(pageCount, pageWindowSize) - } else if (maxIndex - currentPage <= pageWindowLegSize) { - // end - start = maxIndex - (pageWindowSize - 1) - end = maxIndex + 1 - } else { - // middle - start = currentPage - pageWindowLegSize - end = currentPage + pageWindowLegSize + 1 - } - range = _.range(start, end) - } - - return range - } - - function getPageCount (): number { - if (filteredRows.current.length === 0) { - return 0 - } + function display(element: string): string { + let visible = true + if (element === 'search') visible = body.rows.length > pageSize + if (element === 'undo') visible = deletedRowCount > 0 + if (element === 'delete') visible = adjust + if (element === 'table') visible = rows.length > 0 - return Math.ceil(filteredRows.current.length / pageSize) - } + const noData = body.rows.length === 0 && deletedRowCount === 0 + if (element === 'noData') visible = noData + if (element === 'noDataLeft') visible = !noData && rows.length === 0 && query.length === 0 + if (element === 'noResults') visible = !noData && rows.length === 0 && query.length > 0 - function updateRows (currentPage: number): PropsUITableRow[] { - const offset = currentPage * pageSize - return filteredRows.current.slice(offset, offset + pageSize) + return visible ? '' : 'hidden' } - function renderHeadRow (props: Weak): JSX.Element { + function renderHeadRow(props: Weak): JSX.Element { return ( - {state.edit ? renderHeadCheck() : ''} + {adjust ? renderHeadCheck() : ''} {props.cells.map((cell, index) => renderHeadCell(cell, index))} ) } - function renderHeadCheck (): JSX.Element { - const selected = state.selected.length > 0 && state.selected.length === state.rows.length + function renderHeadCheck(): JSX.Element { + const isSelected = selected.length > 0 && selected.length === pageRows.length return ( - - handleSelectHead()} /> + + handleSelectHead()} /> ) } - function renderHeadCell (props: Weak, index: number): JSX.Element { + function renderHeadCell(props: Weak, index: number): JSX.Element { return ( - -
{props.text}
+ +
{props.text}
) } - function renderRows (): JSX.Element[] { - return state.rows.map((row, index) => renderRow(row, index)) + function renderRows(): JSX.Element[] { + return pageRows.map((row, index) => renderRow(row, index)) } - function renderRow (row: PropsUITableRow, rowIndex: number): JSX.Element { + function renderRow(row: PropsUITableRow, rowIndex: number): JSX.Element { return ( - - {state.edit ? renderRowCheck(row.id) : ''} + + {adjust ? renderRowCheck(row.id) : ''} {row.cells.map((cell, cellIndex) => renderRowCell(cell, cellIndex))} ) } - function renderRowCheck (rowId: string): JSX.Element { - const selected = state.selected.includes(rowId) + function renderRowCheck(rowId: string): JSX.Element { + const isSelected = selected.includes(rowId) return ( - - handleSelectRow(rowId)} /> + + handleSelectRow(rowId)} /> ) } - function renderRowCell ({ text }: Weak, cellIndex: number): JSX.Element { + function renderRowCell({ text }: Weak, cellIndex: number): JSX.Element { const body = isValidHttpUrl(text) ? renderRowLink(text) : renderRowText(text) return ( - + {body} ) } - function renderRowText (text: string): JSX.Element { - return
{text}
+ function renderRowText(text: string): JSX.Element { + return
{text}
} - function renderRowLink (href: string): JSX.Element { + function renderRowLink(href: string): JSX.Element { return ( -
- {copy.link} + ) } - function isValidHttpUrl (value: string): boolean { + function isValidHttpUrl(value: string): boolean { let url try { url = new URL(value) @@ -206,243 +146,155 @@ export const Table = ({ id, head, body, readOnly = false, pageSize = 7, locale, return url.protocol === 'http:' || url.protocol === 'https:' } - function renderPageIcons (): JSX.Element { - return ( -
- {state.pageWindow.map((page) => renderPageIcon(page))} -
- ) + function renderPageIcons(): JSX.Element { + return
{pageWindow.map((page) => renderPageIcon(page))}
} - function renderPageIcon (index: number): JSX.Element { - return handleNewPage(index)} /> + function renderPageIcon(index: number): JSX.Element { + return setPage(index)} /> } - function filterRows (): PropsUITableRow[] { - if (query.current.length === 0) { - return alteredRows.current - } - return alteredRows.current.filter((row) => matchRow(row, query.current)) - } - - function matchRow (row: PropsUITableRow, query: string[]): boolean { - const rowText = row.cells.map((cell) => cell.text).join(' ') - return query.find((word) => !rowText.includes(word)) === undefined - } - - function handleSelectHead (): void { - const allRowsSelected = state.selected.length === state.rows.length + function handleSelectHead(): void { + const allRowsSelected = selected.length === pageRows.length if (allRowsSelected) { - setState((state) => { - return { ...state, selected: [] } - }) + setSelected([]) } else { handleSelectAll() } } - function handleSelectRow (rowId: string): void { - setState((state) => { - const selected = state.selected.slice(0) + function handleSelectRow(rowId: string): void { + setSelected((selected) => { const index = selected.indexOf(rowId) if (index === -1) { selected.push(rowId) } else { selected.splice(index, 1) } - return { ...state, selected } + return [...selected] }) } - function handleSelectAll (): void { - setState((state) => { - const selected = state.rows.map((row) => row.id) - return { ...state, selected } - }) + function handleSelectAll(): void { + setSelected(pageRows.map((row) => row.id)) } - function handlePrevious (): void { - setState((state) => { - const page = state.page === 0 ? state.pageCount - 1 : state.page - 1 - const pageWindow = updatePageWindow(page) - const rows = updateRows(page) - return { ...state, page, pageWindow, rows } - }) + function handlePrevious(): void { + setPage((page) => (page <= 0 ? pageCount - 1 : page - 1)) } - function handleNext (): void { - setState((state) => { - const page = state.page === state.pageCount - 1 ? 0 : state.page + 1 - const pageWindow = updatePageWindow(page) - const rows = updateRows(page) - return { ...state, page, pageWindow, rows } - }) + function handleNext(): void { + setPage((page) => (page >= pageCount - 1 ? 0 : page + 1)) } - function handleDeleteSelected (): void { - const currentSelectedRows = state.selected.slice(0) - if (currentSelectedRows.length === 0) return - - const newAlteredRows = alteredRows.current.slice(0) - - for (const rowId of currentSelectedRows) { - const index = newAlteredRows.findIndex((row) => row.id === rowId) - if (index !== -1) { - newAlteredRows.splice(index, 1) - } - } - - alteredRows.current = newAlteredRows - filteredRows.current = filterRows() - - setState((state) => { - const pageCount = getPageCount() - const page = Math.max(0, Math.min(pageCount - 1, state.page)) - const pageWindow = updatePageWindow(page) - const rows = updateRows(page) - const deletedCount = body.rows.length - alteredRows.current.length - const visibility = { - ...state.visibility, - undo: deletedCount > 0, - table: filteredRows.current.length > 0, - noData: false, - noDataLeft: alteredRows.current.length === 0, - noResults: alteredRows.current.length > 0 && filteredRows.current.length === 0 - } - return { ...state, page, pageCount, pageWindow, rows, deletedCount, selected: [], visibility } - }) - - onChange(id, alteredRows.current) - } - - function handleUndo (): void { - alteredRows.current = body.rows - filteredRows.current = filterRows() - setState((state) => { - const pageCount = getPageCount() - const page = Math.min(pageCount, state.page) - const pageWindow = updatePageWindow(page) - const rows = updateRows(state.page) - - const visibility = { - ...state.visibility, - undo: false, - table: filteredRows.current.length > 0, - noData: false, - noDataLeft: false, - noResults: filteredRows.current.length === 0 - } - return { ...state, page, pageCount, pageWindow, rows, deletedCount: 0, selected: [], visibility } - }) - - onChange(id, body.rows) - } - - function handleSearch (newQuery: string[]): void { - query.current = newQuery - filteredRows.current = filterRows() - setState((state) => { - const pageCount = getPageCount() - const page = Math.min(pageCount, state.page) - const pageWindow = updatePageWindow(page) - const rows = updateRows(state.page) - const visibility = { - ...state.visibility, - table: filteredRows.current.length > 0, - noData: body.rows.length === 0, - noDataLeft: body.rows.length > 0 && alteredRows.current.length === 0, - noResults: body.rows.length > 0 && alteredRows.current.length > 0 && filteredRows.current.length === 0 - } - return { ...state, page, pageCount, pageWindow, rows, visibility } - }) - } - - function handleNewPage (page: number): void { - setState((state) => { - const rows = updateRows(page) - return { ...state, page, rows } - }) - } - - function handleEditToggle (): void { - setState((state) => { - const edit = !state.edit - const visibility = { - ...state.visibility, - delete: edit - } - return { ...state, edit, visibility } - }) + function handleSearch(query: string[]) { + setQuery(query) + setPage(0) } return ( <> -
+
-
- {renderPageIcons()} -
+
{renderPageIcons()}
-
- +
+
- handleSearch(query)} /> +
- - - {renderHeadRow(head)} - - - {renderRows()} - +
+ {renderHeadRow(head)} + {renderRows()}
- +
- +
- +
-
- -