From 5ed478e298303e64a433de1ee5bf4a0347d5d053 Mon Sep 17 00:00:00 2001 From: jaylin <chieh.lin@rwth-aachen.de> Date: Wed, 3 Apr 2024 17:33:35 +0200 Subject: [PATCH] add Index_constraint, remove none feature values, add recommand index column, fix time frame filter, improve outlook --- __pycache__/main.cpython-311.pyc | Bin 87647 -> 91460 bytes __pycache__/main.cpython-39.pyc | Bin 41145 -> 43116 bytes main.py | 252 +++++++----- models/__pycache__/models.cpython-311.pyc | Bin 15303 -> 15303 bytes models/__pycache__/models.cpython-39.pyc | Bin 9304 -> 9304 bytes models/models.py | 18 +- templates/app.html | 445 ++++++++++++++-------- 7 files changed, 451 insertions(+), 264 deletions(-) diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc index 3d15975a5b1a12e3a15f7be1710a1cb8b3dfbcf9..483948315bf54879fb3460e718d1ddb0d09fb88d 100644 GIT binary patch delta 14426 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 delta 12299 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 diff --git a/__pycache__/main.cpython-39.pyc b/__pycache__/main.cpython-39.pyc index d378229d7ae9fbc0df103aa88b1b69fcb108054c..7b94461d4db815a99be3ffbd99e161d707761ef0 100644 GIT binary patch delta 18496 zcmdmakm=0@CcZ>oUM>a(28QFMylFGlCi2Os9Ajl*NMT4}%wdRv(2P+`DNHHMIn22% zQ7nuMDNHFW6N8j_S#x-E`J(t3L5kQWHfb}mPh98i%9x`ar481u6Qu*Db)$5_v|f}R znAVTdPhm*m$T7$@j55qMiZTM5z?oy5YZ7GwW^+xpV@zk~&aur^h*Fr`z^HC%munwo z50>N2amaOyas;#aa-4FVqnvYHqFfjmQutF@vRqRHY#37cni-<pQUp_3;@v0zVU(`- zND)mDOA$|JN|ESgN|8*FYGG(*jPgun$?`~%P7zCI1dGe0$b!Yaz~XXnarqPlu(&r^ zToEp=l%fn4_W_Hmz{OQl)WG7tU~%;nv3jTl8Y!A!2|uue7F>gNiVj%ZKSei1uZ1Bh z04%Q$mp4c;1j`44#f{+N#wjLX@gT6cDO}tv#T+ai3>LRYsfYQ{GQ|ok5dxO5hHJ1% zu?35Vg2nCN;`S*HVDT`pxFcNLDa9Eq9u5|Ffs4DQxPiqZz~b&O@%j{x6i={3Bv`@= zCXwQu;sX|s0*m{?#r;zJ!Q#<i@c_7ZU`h~JJO(Tt3>Obc2?dMCg2lt&;^8R~Eey@| zj8SnZkttCv3{ml^2`SNY7*k?WVp~|E5>w(*;#(M^l2VgX66P?bB&H;_utcS#rh?TZ zr=+y7M5U#qrlhqnM5U*sr)0D+L}jEVq$a0i&S6T)O37|viONjLNy%+th{{S$PEAP3 zo5PfnpHk4m5|s_-7uKf~wXj6xq$a24rn;m$rzT4>K*%&ENrsf-Im{^~DWxqeQF$pV zQ_52+S{S49Qz}!cS{R}VQmRvGS{R}VQ`x{ys!geDVTmeAsZVKWVTdYDS(Vb5($vBj zRg$tgr8%Xgg)yo$r8T9kg(0deWld@_7$&6FC!{8%GNdx3w9ny4=}75pVT~$JSqqi~ zDS^tSCZu%D;Y{gH>1kn&sz_M}azDtgU^+E5H8mlncMfMtUrK)qYgA=wa%xq|ggHzp z6H_L&utZgZRHjUx!;~^5WoipcR87kI)LO8Ur=?7fZ()t9OWBYzBV}d_V^n?0td!X; z3{ed!b5iEEFhn(`%uAWy!VuMzvLIz)3qw?M%A%CTEeugDDN9n8wlGAsrnaRl%VwIu zT68FtC96GUdCH1jMn;Ad#$X1`jW0n(*XHxA2N~<785kIf<QW(kZV9;fhdTMV#s>ts zI(r6t`ui2>faF9$ax6ivjy^?NAU;SfL`D<D1?vZ?DUtwjr9gxnSWAd&L`ac1h_3)5 zWI%)-0|SF5XAxKvSiL-4Ly<m60lNChdK~KfnoPG?d@_qmZn1`x7UZOE4&~U-%xJn< zl!ud%@z!K{-YiC^$z8lB%>+R9vgM|hWaOvZVoA#{%Du&$o?22Q2r^k2M5uxYbr7Kj zQp=Q*RHQh$lP^}5A0%c76618ONKP#%$;{8Y#avucq$yftHJO>;kSzwJB6hMBzpQBz zh?xu`;y{EGNE>@eVp2|Ov0)TfdQoCQM!buEh+$CzNQNaZF*mg+adHp89Aow5)%@m+ zijyDncQe*b&KG#iXg4`pP_o{Zfq}uTD4KzRAw-k0C=nD&yhR}I6bXZD$N&*wU!;Oe zWJ@e4NX<*R#a5J>n_re%1om1ENKGz?$O94iAc6;E5a%tHl*E$6D9+51)ZBQmW9mVY zU|$r0SS8?~uK+PCK?KPDqG}KeOw@qv<jzRUOUX%%PbtbTD4DDzq{-Ma*+=NEJjmrm z)*!WZAi@zufC2>So5>3uL?_1zOBsPp0ud{jZZYW@++qcX7rHH6HaVHaCCT}@1$J{L zuM{?}2RkJm?2RIj-yv}c4i5spF0ukS1?)d~EESc2#L7Sfe%}fer52}_#FtEt7U5>> zo}4bC%GkZRS7ZkxqxEDrF-10ekP)_%bH(J;R6wD_S(2HXT3nKtTX2h|v@|oNh>3xL z;TNNhCUa3QNNL~Xjbh^ZXb}pEom;}`sd=eIi6yD=86~+n@sLmfN44_g?_%C;jUesi zlRd=c8Ji|&h|Ag)g6v~2N-fAqOitD0FKPxU=m8NRzusa`&d)1J%_{-hdy5ZjUVLU= zaY<rca%xfE<ip|yj8i6m5jU0xMFu4N!4b@t4)QQ45yyj)%fiWS5{`_^CwE9Z)&R#b z*vp`ZjuI|OECMOcNlZ%3iBHbYDb3Bh#b{CFFnO}1rYM>x!LcMb`J$wRJ2?J8p4McC zxOxT5)kPrp-(mwBSRBQRY<_W(FGv-VV<#I(i87v<>?WnmxNdTm)VdT01_p*GW^;4% zqSc^;!j3iLfWr~t0v3o1)`QFj=`TXJ1r*Ahi76@ZU`IWiTq7;ZxMA}w=~Iky;NSp- zL=h-VZt;LsK*OQPcXE@gN&Q5SxsyNyC?OP02C=4qoXL}zl$Md0otBi6n31L_49U9S z=mw{0MBu@lS~MG^c{+%g0U~CC2(WL;L9AIIq6y?$UQnEP1^fHOXJ@5W78lJ0$$(Nz z(L7MF@D_p62gKW;;4az-k_UTx0f@B-L@WdmAcqz$2C<fah@~K68HhlN1Q2%<0|Uc{ z$@Ar088>ZyDp$(LST#9XL6RR-#@=F2$xO^iO)e=apWLjVRu9f39BHW~$r*_`Ik#Ao zbMlK*i+n)NW=Y9RF1f{80xH>yK)$)fk_~b`sNlQBnv<WHQd|V~E+|Aa`5?XkdjRY@ zgfEIgrg3Bz2M79OmZaWdNzTv9D~bdOvKkharWRF#%FE)?oRT7tk8Uv+<(C(MgAmPI zn;94w#3n~7>N0NL+^)#Wq~;0otP6+$IT%vdfx=Cb8Nvo91?$PXl;!I|38KgqWGAC* zksFxyEpi8G4FVA!AR-tf%$A>&1qvQ#P}DFLfvmVC2+DWy8L5dWsYQ^2K$8opyo0BW zTP&$%i8)0aAamGD^5a1P2o9oKlOt4QG{8BG3v47Lwn47G#RW;FpyIXY6v!E;C(lt) z6*v!KA_c(Yiz*_F7L%W;xH4XytfQK21ok(`c|{<ln#>RfT>@zXS+tT7EDkA0z~<CU zKBy`qn83)uP|OBuW-tjcvN081pZrcWKoM*NQxRNu(KV1cU~OQcW^%lmG=Bz2FAG>N z8)MOp&7EptjP+nAfE@!0GO(q$Sj!S~N+HRp=q$)2P>2_u0kJ@FPy`NhaOi`=ya*in zfeZ``RlF&wIjJS73i*&o@CHQ!a!?~22n+N;hFg3^sfj5b;Mnm9@%4dJN8p^tT##R& z$qoteD<IoJ{)B`*DF5AJE=ny1Cw7!zTCX9_m^b;jhPA?7kehh)(^E@yeO+`j^V0G` zjSEl!fHB`>CC#&pzb1dsRH#qkZ)S>Ns^zTZD&eYOui>cStl>%#n8VV{7|c+^nj)CZ zG=Z@wri66?Pl`}F0|cfB&tX}}RLfn$QNxnLCCSjtSj$nu4PmE<K+WR;b@o~qYPeIx zB^g{mYIsYyz~=MT@T5pUY)GkR2xicf^eegvs@}LkHI`mMVo`Bw(JiKu6o?l=IirXX z6bk+z0vsFQ$N`1&E#~C>9B}0bYSV!#&7yN4RiKy((PSxl2;zW6YC)-+qaZszGcP5z zq6i#;o(voe3`L+s;5IohU23w5w!8_*x|iTW7L<ZCnTpy$Mt~z7BmfC<Pz6`i$-uy{ zl97R-sB3b$wyGOQi6#rgdXQz{Y<i0$4Qit%a{J2w?3lF7yp;IFoSdRtAS=LycIxDp z+G_QnL{J3E4Y&ByQ%gKS4t56Dip52sQeBe|DJ`%R=a&}TVlGK3F1iCU29!A=MKP-1 ziynbw!KwZUhy`*k#4})TfqZm}8&u-^x^TyXl1v{c*O*N1*HMsU<YVMu<YDAt6k-%& z<YE+qvWi|$KBFVg2P#-WV;7)&R6J+$DuaN@p1Mgahf3J9Ca=|%VtD{!oYZx$ui;5y z0H>$48eSNygsX<3hPj5Nh83JPxN3N6c$+z6SZeud`D+Dg1xvVV_?j7Og-Up8glYt8 z1WR~p_?sD1gdph$hi)d2Ze*QcU9pTQ!gH8F30qUduLx1;f|HdOC`>>p3Y_R}af6Z) ztO^DdZ?|~!3rf<9@=FVRK&=!=_3j5swTv3S7&VJ_PClS#$jCDJlb%M<EzY9+^7!QZ z(!7$DOhsQnCVvN&2z<C}XHbm_DRMbd5=&A+w%_6?E-5Miv5HbBhw5uMgHuEir~uFu zhSXZ%rW)A!;Q9+3^>0Adfm~el7L>@iAw^O=xSYN``J%qH8OSX~UqQ;gfe5hsz-a(X zfD-}_I3c(~Dvh<EgkU$>&R9~Lg^`C*0F)A#co^jv<(N1a;gCs$QGt<zQH)83k%Os- znQ?Nxfh=m$$e6s!ppK=lgf(ljmZ22O8W6+H5Gh$0!IOni2?sb?aMpl|4E7psjAX$H zO%`0xWWfzi7SMtVhi*u+K+;(P(q+h)A_z?uLIjcpq@sH}S=h*=9#l>geE<dEM-cG| zM0^GjKR{8<gFC8!f)xD%5ugUhE&kMslA^@qlK5aqXczqfNr5sy-l)z7se(sy(O(dk z1ymTZf(SMcaTz4W0AewM2qq9gMBIZ50jOiHf}-ARvYoN49Ny^Wm|Siw&nLpbzyL1@ zW^G<#EXr69kJ6MHK4_j#DdDVPs9~z%s$r?&so}0+t>LZV1Lt{gj;|Fe;Q~cwt#Ao< zjc^H1jX*PFjbM!ssMY~xUL3laK;;F7P6@^o5oqL!qD1awT~i4KQ2s4?2MVV5AYvzo zK!hm^<KzTWa}E}04xT*AR7x8hxxDC+3(vhsJ%!ZC_f4hi(IQg>l5f$nEI6Y6fh_+I zBEY#C;S5C7a)M%z8$|Gc2t<VPg1CGj0xdK1gK~2b7b641Pf$!cOrB_F&WDmwxi()h z3ufdE03})`NK@zRWKK&Zb}<G9h7yLX$p)5UlM5|ujS$HeR0XYMyv107UVU)b<Rs=M zr6k%JGJ$Kw2NoKP%O=-aN>28*;6yS|!AJ<?R$&k!0wP2~1i1786JSS+GcqukfXxAg zN$}(*ORF$Y!&s9MTn856(qj(OqsatmuYqfNP`GGvB1I3lwgvg5D0Q-sl@13e!)dY= zHBWZ4QZ*K2WMH_(n0bo}(%S_!OVItw2kOd!+t*+<s*~qgX*0@CK4lf70Se0^kP(_p zkj5?8Y(}sUvN4nWtYro5nPBZ&W*$(%sj#`mdOD*4sIRpW+V(gJ3IK4xfC*6O+~TwW zb@M9<itPNs{+;Y>t1AF*wSd~A;5w|xcXETRlRj7wq!DHuxNQOwfVc%@9JnbH1u^KO zts!IWWM#WZMuEw-cJcz?wg|{M;OePJYVsO81;!bZ&)O+4@=ktdCn*6+uD7^z6Dy$A zWpNSM!RC|Y?d4Qa?YSkAT2YW+R1yzq^~RUv$0rw;c}z~SH=TUJiFNZFdqzekMaIdE zw!)L&I;&_XfzpLCh)@9$sD>4(GBPk!Kpg$gK|=u1+yU2vAm3YS7OBBCs>4}ebKw4N zg%~l*QG-!q@@aSZ`uwEg)S|LP(1=~JUUG3+ktRr|7KqShWMEh+1W86ZAOT$vp$8%m zL1X~p8iEKZ5TOquz&&jv5X%@un1BdV5Wx$ITr&{M9Aq|oQ8Re-zyc%!4o6E63nW)$ z1!7r)2(Smh1jrY+c%h*R3C@Da3!U^C?KfX^Vka>$=R-om(nX66++=p2oaqv-OE?7A zfc1g;9LkgJT%;!Ncjso*m@Mq7V~HAaMYbTv+kptM<G}>j2mDYU1VfX=^vMgI6erJg z<zsZ-yu#Ihk%*LV80?5i?%Dznw}Zmf0b~`?3E=|Rgvs;WwZRF&pdQurnu192PQ55E z!WJF^pzxSn<-x<acXGRjHlxet%^uN=_4h!Yx(^~iC1DY`%?i%n;D+mCkOVmGgUW!H z%%BM20u@oZkZvug#qtu=4Tkil!1W%e%Bx~l*HI_}*MOi3QIoR>)IBMB3L0MG05t?l zKur{328Mcuq9Y(Xz@sPNWDQRApk|CF7r3=j1eypadIeGh>Wth1TOSXx9$dkJ9e*0s zFl9_Ek_AN%I1zveP)Y!Ig&u**L?cjFm63~)i;0g>0Nm*0o6O^_!gy=4q4!e7tspDF zeg%!VXmS^U28oJxgG50BMSCZI_g1S1D*(?8fTtF~?gqP-15yuzTg{+h9`LXZc!(;B z3o`x)njR<ujb24@Lj>YWDhpB}MJB?vprJHoW(J00P}7Hjk%Li+k%fr|+$fe{6kuiK zU@V%wd7+OcWBnG8)4;CY24Zao5j#KxxM#ZyRQ>WGISM=!1$Nm{kfLKC0yOkgbR5Jw z0U|(yPLQG&>|?N_zyvrP=P)oZ2!dP&8YyF7ob2hR%mEV9WGu3pT;i9{26oGq$uIrn z1mMATi@CHYrzmE!xWBeJcyJFq<PA0yOn^<!Vqjn}2AK?M>M<~ae9ObA#8d<lPiCAP zFhgSUME|w*prnM<wgM+pP-+7Yas6V_QP5;BIt=m$xDq}DVg-TH1Gp^(jyO=_f{udR z;)K*MrFkVqppnB{oS-qPc+dpIEmm+x3p|2wi#e|}7ZOK`a0lNK1uKY8OHC{(ElP#e zQZ|$C1;{c*Fizf=p)grC(9j?37_gnpkdc=#kaIw`K}<v#C<rkSl(nFvYhIx60F9q9 zFoL>BLQF-GljjD?GSx9n-m4}#`D$Pee_t(I4MPnZXe?v`V{F~zfFPHg8ukT@DWWNY z3z?c385v607jPg%7Bbdy)G()r)o{#aND)Vfr$|UL%w|Xtn#*d=P|FM!mrQ3^$mGHh zd!v>!MXH9gj9~&}(X|w57^{YJ0cVQTWPxCDW2uFVDV&lF3pi7x7c!>sNHQ$oPLToG zzkoAEb|GUkV=Z?HTMakJH8tEW46$CdJd=}xmFuU0-Nw6+v6dg~CVsG+co6QGz*yLk zrvg%yB3Q$j&V;6t6Gf#;4QmRgIYTXL4ReYd$QOlug$*SP3m8-6GZ_{#)e6)IEMQ&8 z0Jfj4hB-wc3Z$rpIfV~O^VhI}4P=J$6`?eLjQ}WwQRKiDOkgTht6@%2f~X5-&{Xz= z#Ieidw2+^SOp_Br_p5>nQBcA9i`7QIB*m_Z$EGAj0Tj!+nJIR#HkD79lp46CMN8n| zv>nXIzyQ`=WLM-oc}|#?8={>V0ZLAhAOc);f|DDlFapoW-C|8C%`GS{0%rkmW&q~{ zP>H1}STuh!Pq>=897r#?%mg>#z$r5n)KcY!48=l<*_)Fi!_|yHCD$$HlH7u#2cVqg z4$=x9Nd%{2FagfYpn=ICP~uIPyfIvpUw~1FQHfE2Q3O2Vqc!<`xE2>=coo!^-K-kX z#l#&2G7D70RH;s8jFy>vEozc7-Zt6{W^iwyJKB;PTna>k3<#QhK6(mc@Z<(tk;!kJ zIVU&9$eX8uB=8!w9BdTGM=>C^ph4lPg30x<4wGMm3Qg9Gl{bn71qeH&$TkL-HAQhC zrQj%&0I^&_gd~Un1yWJ`<f_<N(ct<V950{}0Mf$%jkJIVWi{C#ZTD1=CQvLy@qmgW zXn|4;smH*LHE_U!32^AjFfuUoPtK1^XO!7|Jx+qL9_)3n)4?GDa(@xX#Sk~fgUnB4 zWMI%_DuTO3ldWhz$iv_kB*=0|jDpia9Y_Lf9hd;yIhBEd;Up+VLG$CF;)sQbgOP=i zgHecyV{%>sA0J48CKGsutjLpba$HIaH)1So>*NMo*~w;!+ScGA7&P$?p3_*#2(I5@ zeF$*Kf$annV2eS+p^w4wGkH#;Z9Qb34&*>^0gqlkgCx8_{`CeCpnRao25}R(;sIBE z;0h1q?<hggFc^3`6jpqrmB~fV7#JAtfxHbGU}s<yn5>h=$M|e=Y)UJJ??jTb-N3$6 z067A@UH}wUMfxDFEQkPCsF@&#Wq}B=AHf9NqwgUeJ(_Igk`0P5JZ|TMj=(|EWHGwK zpEEEpyqc_-CRq<+X)=QY;TB(Jad2vSZfagh2xtbi$di$QAsLirK&>JW235YS3=9n5 zLDxMDpuTzu;{v7>#uO${ZBolr!d%1D%vj4@!<@yE#hSvL%~YgQ!dAo3%$Ubg!<@xl z%TmL<05r-58p>xa;jF1=s$pnms%1-OsAaEVEGnsC%i?ljh?T14sNu-sPGQMrDhjJ% zU%*qtk;S@@k&&S=EKdnV4QNc4xu^<7O;rkO3R??94J&vCfU|@zg&kD6l<?JXg6JB~ z8um1%U<OSNzaoC5R0vwPF!@7DsUtX^5K#mglqe!Hir87e#m=%+^LkBw^bs5U$q_y| zdW$8qxF8$c^S#BAmYtslDG#{7kyMleN*q{{Yf%TN>CBs!9iLwmUyvOi2_Ad}6~s}z z;MoUUiTDKr1H(5^B31%LF{1#uLCM3&#l*tM0%{5~3NUdoaxwBTfyU!Gn2KIZwobbz z25QMw@ddm3xH^X@Xeqb_`THsq?U<~eF2>k1*)83{1e}~f=@&Me0dfj@Is|23RdA4j z`VA(N|A;9}zLhRs4@z4Yt!q#w0q5R*AQq^JS+pO-IshWTIT}=R-C_ZGT$3BAU0cA& zz!1d)T~YuUlm@qL;rTs^ySNav1^}|=-~@vr149w02!OP7L7sp(9GnxUF)%RjO`e#c z%{XoH-i-b6;PyPIL9NMJ1a?#=$UWdH8064fkO|}byi`~=V=gYtfwcY6Oy0}Dz@X0p z8=7I{V3lGl+6%6%-e*cNDoy6flF|XYITmCCnr6`0tR+Y@sI$eu!ogUSJvkuD!v&n^ zKr3oAS&)JRYz!#HKs*LE3+y{kDs%)H1D^I|<X{wGESf&~PS#2RP>dHL^&`Lzlh)>( zoSiPM0rEe@C~Si!plA*PNAu*b*?RS0CxN3F-U%oI`Mn4fUXU<^N313%lB*FN6ma~3 zi|S%fegO4NZn1(^1z{9*YZw?9+(90K^_DpF!JQ=uCIKcsMmf;%7f4Ams9XbO01yU^ zkMP0DxlfZX=E&ElFx9e^u%s~8FwJI2VF8n@HLSB4QrK!3XEUU*gByqJH7v6kQaI-F zHZ#_;r*PIVXM)<(44`&pze+7<4d((j&~)cQrUmQ^85VFXWT@o|XGmdSVPIisW@co_ z6EI{b?l53r1j9%MMur-$qDM7cDO{3?3|X8dTv^;%JSp70j0<=dGSqU{aDf`r7~(u7 zd?~y&+|5jk3?+Oid^J2E7HBd9ROXfNrSOBwz!ZUCh7^WihLwVTMc|0Fo;*3%RTI=X z{Kcs7i_y9$8I)=Gic1oUN<d3#z_UY|jJMcIszCE}li%n1)$=3weL+bQoIAmJ035rZ z=)A>|n^;hgnU`K%1R4PVui_{I4eQ?G09CJ5`FW|gm`h8NZ*gW8=YtlzC6+*nJLpOe zaP9(6FM%9%ixZSl%QI4oQi~!%>%VSsfCRGgGxNZm)LU!?iA9OI#kaUX>n2i@LBnZ~ zOpKU%0;hE_0ZQ<<L_mWLxtV$Kpw<;^8Y}{oU_kXV1EUNR3!@$r2QwF=5i<*;1fvAF zOypqVU{ok-1yzvXi~>qEpmG6<!4)JZ;mFmqr7+d9Bc&YX*$gSH;M4+3Iqcx{14>Jf zl*5t2S;LaaSj$-hOF3LMTnpGhQ<X?5hnqmk;Vybq!=1uaFNv9Qcxt$@rW{^y$^oYr zaLVBYryQOdUTi6+s0@_xYC!D}<mD`gnIKSn6*Ynf5y2U?38Wr8lF$rdwSWk4`xR@{ zOkQXt<yt^YG!(UgVgYCNy)v-b_Xs%qdN9;*rEp6!Ffr6}*YcEbrtm;BC}#>US_b7z z;R9z-en<w@6o6$=PzEcXtX^m&0LrCB6`=5|1QE%T;|u-kt3ffrMMRD)ss(8RXGU-f z7&)VXC-Wc?2U3JT-VhT&)eH=|43Od<oTmhl^Ax!8(E*A{P?HKhLxHCDxfns&si*_w z0}e<l2iyb$XM6C#Drgk32;BFDOf61+-zdwtVlr=&+UC9@W0v|7_7vR`u^N?TMsSj4 z2T%I36wRsOfU!!%YZz)6YnW^JYUG<)W0-2$YdLCEAbLP$avDo81B?$!?wWdj;1wQ4 z;65CvgI)v<c2Grki={X<Ck@givH(R3qw{3;axHc6;3jxT(+3oKpnf^HaQ(%iYiFna zi&b60F<7B!%jAf1MaHPfRptKr+d;ZPG5L#2!9U2wHAulJQsEbif@84rEmlJ#V-r(w zIa~zsHRGfTllotrLHXqh;K3kGi6U@03$9NQRVk<nzr|WyQj}Q$X=Z?;`4(q-Y6)aL zM^PTgb>PY$+)4p=l|W4=@Ib&V7I0|_D!Y-Y;sTIE3mF+0>Tj`S<`t(Fl|WYJfl5G4 z4WufXK*WJ;2G`x72FNX5&^Sj@VsR>DK`(d(A9yJoC<)#YM(M*q26S&tmaEieJT=*? zGFkvUZwYP^fQL1fP2O6mty9zp>WhP`3Gi~cB5+{?8W%3A0*wH3re)?O=EQ^6GM$_( zS|u+Dt_i>dDCrjcW?*2L4@#`_CI@ziPfn?7v|{075@8f!6kq~NYH~tSD59VPXBtE@ zg~k;FLy_lX>1tU<6L2z(uincHN~S6$qBU}8$y5WLOf^cxz{!*eoKPj3*`Ntk4x$U5 zP@#NqLRH0`P>VW2;SFwhgVHHek@Mt>^%AC_MK(qKASrMXoeE-20}<0185pWW6zpsj ztRNLXsESr_^m9=tnlX7@y+r*?kg8cA0-kb<W`nqMK*U@S0g5_UdSzfJng<e_4<Z(T zh=rg$E{-$V7A*oP0v9`=L|e2N#6=|6dbISq1SE|sr4}s($%02#KsAJ>5>hH9BJ$v= zv}hT~9B|sKhbPXW<siWoAOgG^YZZtEo<3g<VyyuYpd~Ct8$hg$AOe&yi#CB+s7bPD zGe`_v6K(;qwt@(7f&>%bRLRcBz`)7Mz|2tGF*&fqWO8<+UnDqLih+_PNEVhX5$O;- zaSu+L;DQq|g%9r5b3$7zAPTfY0yKgFZnb!^Zsu#+!dRcqP%BZx=E5+6F;=8jGDV<9 zvW#H@Q_+$XK^Ut>GKC>UsFg{QVF6!?z(U3trdp|5=^E(;d?~^U89^);hS(LgGBw;O zBH2t6n2G{w1ZRU9&r-7)QbcQHW;3LSfylY+wX!u53wTjv>p{}uC_*3+iMc$WVSqfB z8txQHu%S~@q>xlegA7cO0g-b#AjV>mmz~QFGNnd*0dI{gh&6$+@JF6jjS$=slJ&D0 z<}!n%kX$A?n;}JRE(=I>0%PH>6yXK@H9|Gw;tUHJYvpPLYUR@zY87gjQ&=Uz4Yw5b zY^Dj!MFKSnaASFAGo;8vOsHX>z+9w*BnN5$DnR8pCe$+*d7w!uLM0_8Fc+nP<WdAu zlos&UNGt^PKm;MqEfJ_;X=bcdtWli6T+{+`Vu@ghP>o_U(?Z5PrWEE{r5YvBxXc9R zqGcuQDaxR>Mmj?ZD+q$?K&WHuQ&_;=8<iTR*$gRybJ;+?PEk!!TfkqV0J3l{Q>}7} zdX3y{h7=8O1ZdXC&t^!`tWloLkfH_Ft38*yR;7eJMF(U_4IilVO3|IeT&r5bo}yQy z0;;M@*i-atR6(pNi5kuY!X+Z0O1wsOVLj6V(S;1PYT*od7Ay=UViQ;@^B5Uw)fpM8 z<Vx663~JP8Gt33gV5BpIGq5p4GNdrns@JK5T1_C@fWd$vouO8vM$LsGwysvQM7&0` znXy)@h6x@$Y}rf`Sc(?aXf2RPVXfg`$i&Eih`jm~i1a-a4a^f*icY0~0+O*-8>F#D zyM{T1Ns<Aq8DtPs4GWm9v4FQm3lwG(SPCmj*i#HkBx^KK%6--%J7~GjT4Yxu1upkl zYxruE!OasNQ2Rs!q6rjm;JAVE!NtB2(ZxQb)UQXB`ncLDMH4_p&qPoqAf&F~8LZ&v zAEMwF>f-~h0;<^66%=h1)YXf?(~6ToYA1t;DIg7e>YB(ps(6rjMbj7=CbP6F)>pA8 z_y;Le$@}FilqKe5rYICc#v2tf^HMUCQ;QYS@=Nnl^r~1DJpB|jioi7!s8TAzTjyvp zLt06QA`V-vBVDuy)MG>+Rsyw_ittxEXtfS_x&&19+>!ycb5k;lOEUA4OX8t^0j>1~ z`Q;XKZejs=02@B8QO^Jwv4Xdzkm@LK>sJS<jv}HY$5uz-tD@=|Ad3mn>Zo;~uD(cC zaeiKW9t5YxCugK4XBQWtx3NIY!1bU=hu3RSe4rjUxJwQmT8k0|$1Fx@uxI9_c!R<P z+(IL_A{1a`V8~)+U?^TOIk3abg&8tNDZ(hm$i>LP$i*ndWWdP9D8nSjD8MAbB*o0f z%)-pVD8wkn$Ogg8Y>X1jpeb{ZdLBj@CV|PjJ2ce5l_|K52eLtv=@u_6Ho?<ekfCtK z$p>5|>On05@Vp@-bYVKv9Oi{gwTv~43m8+FL9?Q@OeIXs47JQ9%qc824B`yn;tw=e z>Q&29!VD^(85wF=7BGT_sTVRWU|Gme!vG?)nTpK7;_NAG5b;{p66O?+8dj(o%nMj+ z>LKIcW+luikkM>r7lv4#TDB7A8a9wcg?%;5!3>&Qevs)S@VYS2!t^RWsE-uDK2j(G z&qjeeHlVEvRh*s*j=l;Wu0gI<+Ah!}jhQ`*^6PUIg2D5X3g9(pR-kz*P^tj+B5pAk zr$UBrz&Yv`b7D%0CVSCNP=W!iO)ClkjZyQ%a}0R$5V~{=yle)vA|KL^M6`XjF)}bb z1w}bzJf>cRk%x&5yaIrONdmk^gNGTkAef7(2&7b#39?Qd+{yv<)<7*B5C)Blg2rmV ztsGE@q%hVp)_^8_U22(X7*m+c8EP48K+&XC%Ur@x!(76c#n{YLWK_evfGLG}A!u5I zF@<Hacc%iQ_2it+5N_~VLdZ<o<kOu}@sMGy;*z2?P`g<ZvO6n^Cj+!44m$b_9*nxh zlAM~8gYHkzs?FaZe+q$I%ftfq6bB;@6BlUxJ5$l0$u3<b9H7-{nyf{IlMi&2iGrsA zz^&y{P!xc6EEdI2_U@Jv1qUCP0Jk4O<G(y?ldHOA7;`5t^by&-x|^Ai12pirlCdZf zJd%5=M@<W?0Zg=kw;2>22E`m$0Bj6s9M_T!Ji{T?Yh1qvBnzIf1kbync0;51L4_kY zjevW1#YH6`<=}oMc(4~dSO|&~NKX>f#n*(cMge;XY&U4rWD#0l5ws>IiYqZ^@|j*4 z#-7P9d*!9TomsFukAf@#J9a-K1A_|NWTiec#{H8c`(830ne5!Js(1|41K};g?e@u? z{j%C%7vOOr++9V-CtvK>VC<OutKS7Z)DJK+Fu1c#4xXUJcwlqg1Zk%FBOup-XGXyN zWbiIz(5Ofec&QM0(GYm{6TC+mJW~kXN(@T*ke$h(IU2}Z6nIt&)QT?x&B_(AgA#x! zh|mTRx*!77uD`{RSDKRpn$Ek$o>HHhmI&H92FmrC%#g(e=Ri(64+<1{tbuY8BoCfl z0fz}iP6pS%gxy>O8qAC0E>10RNi0bW0WE<7&&Gh#4lIcvBH%R0f#3uHRs`<tf`b^m z#}Mv4(7Gxy=pYq1rGOL3K}H6K^vOC?Y8eksUNuFQana;C6I3U^opP3O-Q?X<r5Pto zzCKl-F=jIRG-GMr8kQ`fGR6svMJ?$JAa)I}4V0doI87!P9J!zkh#}zF>nP#m{Ji3l zqQuO+)D-yS+X>L{gg{YhT53@dLReE6d)QCDFioZ&9PXf=a1=*oacN#=VQFd<JA{U8 zDvSb;w-sc^!!0Z+$_3TwJO$ZELZ?7hGgoC6Kvo)nyVYkv5f9zifh9<zgi7<gKo%ky zT^z+!np^}PcLB}FNAaRs3^K9kEXa1QG?0rx`)Z4eK;AC8FnQZ_eGzcrg9&g99Aac( zSTp(ObT1AbaQ$6$XtVc>G)Bhw$(v@HNP;60tQbsyb%1t;-<kY>rXpkBWW`xeHNY}p z0xS*MGya~9fuR_bFBrh>0uDw2#-jYmdb2k%T1<X8Tb|K!GUFU+#^}kibHr^xbpldZ zj4%ka7yUcPAn?o)69*&Te<ldY24;UR%9&g+M=CHM6o!m`nv8y$OnwN=?C0kO7U%%a zcSNxjB<7_g78ik6>Vk4$5oobw6mM>RN@`BA9)v0a6^N5>%#l-$5=hHQEY6NEF3d^H zNzO>ktt{dO4Pdc?g~8i$CQHuM5RBqNQUNLeCI`(8lmzXeD7ps98GfKxXV1w`PtVLt zFEW~ZaIOd&=-7ZF^~tyAYO-m7YGk>|59Z3Tf(xO^8uR3sqNFB2m@DE~T$qztk{ZRF zRBQrXoe6I0K^>!)T9I622yzfxNhN50rwFumvd9=LlANEHmYH6ZIeEf7@p|yOp%zei z)D0q*gNSt?VgrZ(ZNDu7ZB{M13gTV|5jQ{tXgguiYY>YW<P8oG0oqegBoAUKfCw89 zVGkmlL4*s40Pku84OBvQ9M55>XJCLd!a+*~i}*om5R0NfE0Kyo3ucNy3sk`CIf_6l zB5tuH<>%)Vfu_cbK=Zwj=}FKOYteL&lRz_u;MpJW<O6u*8ay~y1R9Dd0yQCvc7bf# z#t2%!3aZ(Q4ubT7>fs_##Z&~U8;U@sPZ1~s6oJxo5h&#rfl}Qqj(BjL86SU(xu`S` zTk5>UVUwGmQks(rTFJ{$3|b-!Y6S5xsxa~}>M;5+YJi(Y8H|(p7qBuh3QQJRV8<)R z$H=A5$H=A3$H=8UIevkvxvhW|ho}IL0FROcmm&uj2L~S;hai^(mjs8nT$VzjLbZUH YfS`Z`mokR}hbxBxhcE{R2R|1h0D`a<NB{r; delta 16319 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 a6p{r5xg@xhITSctISe?2IXF1@xflT&-xa6; diff --git a/main.py b/main.py index 7b8a849..25942d3 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime import pandas as pd from . import app from .models.models import CustomTable, CustomColumn, Theme, CompressedDataType, Observation_Spec, RegType, RegRole @@ -12,6 +12,7 @@ from sqlalchemy.dialects.postgresql import JSONB, TSTZRANGE, INTERVAL, BYTEA, JS from sqlalchemy.dialects.sqlite import JSON, FLOAT, INTEGER, TIMESTAMP, TEXT, BOOLEAN, VARCHAR, NUMERIC, REAL from bs4 import BeautifulSoup from sqlalchemy.exc import SQLAlchemyError +from typing import List, Tuple # Set up database (call db.engine) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False @@ -411,21 +412,18 @@ def get_MD_info(): object = getObjectColumns(res['tablename']) # list if res['label'][0] == 'col' and res['label'][1] in object: object.remove(res['label'][1]) - return jsonify({'time': time, 'object': object}) elif type == 'S': time = getTimeColumns(res['tablename']) # list object = getObjectColumns(res['tablename']) # list - index = getIndexColumns(res['tablename']) # list - return jsonify({'time': time, 'object': object, 'index': index}) + (index, pk_index) = getIndexColumns(res['tablename'], True) # list + return jsonify({'time': time, 'object': object, 'index': index, 'pk_index': pk_index}) elif type == 'SD': data_header = session.get('data_header', {'event':[], 'measurement':[], 'segment':[], 'segmentData':[]}) object = getObjectColumns(res['tablename']) # list - index = getIndexColumns(res['tablename']) # list + (index, fk_index) = getIndexColumns(res['tablename'], False) # list segment = [segment['label'][2] for segment in data_header['segment']] - print("Segment options") - print(segment) - return jsonify({'object': object, 'index': index, 'segment': segment}) + return jsonify({'object': object, 'index': index, 'segment': segment, 'fk_index': fk_index}) @app.route('/get-ME-table', methods=['POST']) @@ -447,8 +445,8 @@ def get_ME_table(): features.append(feature) else: features.append(feature) - start_time = datetime.strptime(data['minDatetime'], '%Y-%m-%d %H:%M:%S') if 'minDatetime' in data else None - end_time = datetime.strptime(data['maxDatetime'], '%Y-%m-%d %H:%M:%S') if 'maxDatetime' in data else None + start_time = datetime.datetime.strptime(data['minDatetime'], '%Y-%m-%d %H:%M:%S') if 'minDatetime' in data else None + end_time = datetime.datetime.strptime(data['maxDatetime'], '%Y-%m-%d %H:%M:%S') if 'maxDatetime' in data else None print(table_name) print(type) @@ -457,14 +455,15 @@ def get_ME_table(): print(label_list) print(features) - query_result = extract_ME_table(engine, table_name, type, time_column, object_list, label_list, features, start_time, end_time) - table_HTML = get_ME_table_HTML(query_result) - if start_time == None and end_time == None: - min_datetime, max_datetime = get_min_max_datetime(engine, table_name, time_column) - return jsonify({'table_HTML': table_HTML, 'min_datetime': min_datetime, 'max_datetime': max_datetime}) + query_result, row_count, min_datetime, max_datetime = extract_ME_table(engine, table_name, type, time_column, object_list, label_list, features, start_time, end_time) + table_HTML = get_ME_table_HTML(query_result) + return jsonify({'table_HTML': table_HTML, 'min_datetime': min_datetime, 'max_datetime': max_datetime, 'row_count': str(row_count)}) - return jsonify({'table_HTML': table_HTML}) + query_result, row_count = extract_ME_table(engine, table_name, type, time_column, object_list, label_list, features, start_time, end_time) + table_HTML = get_ME_table_HTML(query_result) + + return jsonify({'table_HTML': table_HTML, 'row_count': str(row_count)}) @app.route('/get-S-table', methods=['POST']) @@ -482,8 +481,8 @@ def get_S_table(): endtime_column = data['endtime_column'] label_list = current_data_header['label'] - start_time = datetime.strptime(data['minDatetime'], '%Y-%m-%d %H:%M:%S') if 'minDatetime' in data else None - end_time = datetime.strptime(data['maxDatetime'], '%Y-%m-%d %H:%M:%S') if 'maxDatetime' in data else None + start_time = datetime.datetime.strptime(data['minDatetime'], '%Y-%m-%d %H:%M:%S') if 'minDatetime' in data else None + end_time = datetime.datetime.strptime(data['maxDatetime'], '%Y-%m-%d %H:%M:%S') if 'maxDatetime' in data else None print(table_name) print(type) @@ -492,14 +491,15 @@ def get_S_table(): print(object_list) print(label_list) - query_result = extract_S_table(engine, table_name, starttime_column, endtime_column, index_column, object_list, label_list, start_time, end_time) - table_HTML = get_ME_table_HTML(query_result) - if start_time == None and end_time == None: - min_datetime, max_datetime = get_min_max_datetime2(engine, table_name, starttime_column, endtime_column) - return jsonify({'table_HTML': table_HTML, 'min_datetime': min_datetime, 'max_datetime': max_datetime}) + query_result, row_count, min_datetime, max_datetime = extract_S_table(engine, table_name, starttime_column, endtime_column, index_column, object_list, label_list, start_time, end_time) + table_HTML = get_ME_table_HTML(query_result) + return jsonify({'table_HTML': table_HTML, 'min_datetime': min_datetime, 'max_datetime': max_datetime, 'row_count': str(row_count)}) - return jsonify({'table_HTML': table_HTML}) + query_result, row_count = extract_S_table(engine, table_name, starttime_column, endtime_column, index_column, object_list, label_list, start_time, end_time) + table_HTML = get_ME_table_HTML(query_result) + + return jsonify({'table_HTML': table_HTML, 'row_count': str(row_count)}) @app.route('/get-SD-table', methods=['POST']) @@ -535,14 +535,15 @@ def get_SD_table(): index_from = int(data['index_from']) if 'index_from' in data else None index_to = int(data['index_to']) if 'index_to' in data else None - query_result = extract_SD_table(engine, table_name, object_list, label_list, segment_column, index_column, features, index_from, index_to) - table_HTML = get_ME_table_HTML(query_result) - if index_from == None and index_to == None: - min_index, max_index = get_min_max_index(engine, table_name, index_column) - return jsonify({'table_HTML': table_HTML, 'min_index': min_index, 'max_index': max_index}) + query_result, row_count, min_index, max_index = extract_SD_table(engine, table_name, object_list, label_list, segment_column, index_column, features, index_from, index_to) + table_HTML = get_ME_table_HTML(query_result) + return jsonify({'table_HTML': table_HTML, 'row_count': str(row_count), 'min_index': min_index, 'max_index': max_index}) + + query_result, row_count = extract_SD_table(engine, table_name, object_list, label_list, segment_column, index_column, features, index_from, index_to) + table_HTML = get_ME_table_HTML(query_result) - return jsonify({'table_HTML': table_HTML}) + return jsonify({'table_HTML': table_HTML, 'row_count': str(row_count)}) @app.route('/add-data-table', methods=['POST']) @@ -557,10 +558,10 @@ def add_data_table(): if current_row_type in ['E', 'M']: data_tables['O'].extend(selected_rows) - data_tables['O'].sort(key=lambda x: datetime.strptime(x.get('column1', '9999-12-31 23:59:59.00000'), '%Y-%m-%d %H:%M:%S.%f')) # Adjust 'columnTime' to your time column key + data_tables['O'].sort(key=lambda x: datetime.datetime.strptime(x.get('column1', '9999-12-31 23:59:59.00000'), '%Y-%m-%d %H:%M:%S.%f')) # Adjust 'columnTime' to your time column key elif current_row_type == 'S': data_tables[current_row_type].extend(selected_rows) - data_tables['S'].sort(key=lambda x: datetime.strptime(x.get('column4', '9999-12-31 23:59:59.00000'), '%Y-%m-%d %H:%M:%S.%f')) # Adjust 'columnTime' to your time column key + data_tables['S'].sort(key=lambda x: datetime.datetime.strptime(x.get('column4', '9999-12-31 23:59:59.00000'), '%Y-%m-%d %H:%M:%S.%f')) # Adjust 'columnTime' to your time column key else : data_tables['SD'].extend(selected_rows) data_tables['SD'].sort(key=lambda x: x.get('column2', '')) # Group data by 'label' @@ -870,11 +871,9 @@ def generate_html_header_table(): table_html += "<td><button type='button' class='btn-close' aria-label='Close' onclick='deleteRow1(event, this)'></button></td>" table_html += f"<td>{dict_item.get('tablename', '')}</td>" table_html += f"<td data-id>{dict_item.get('type', '')}</td>" - print("723723") print(dict_item.get('label', '')) label_value = json.dumps(dict_item.get('label', '')) table_html += f"<td data-value='{label_value}'>{dict_item.get('label', '')[2]}</td>" - print("823823") for value in dict_item.get('features_name', []): if '(' in value and ')' in value: feature_cloumn = value.split('(')[0] @@ -882,7 +881,6 @@ def generate_html_header_table(): multiple_columns = tuple(value.split('(')[1].split(')')[0].replace("'","").split(', ')) print(multiple_columns) for column in multiple_columns: - print("624624") tmp = [feature_cloumn] for col in multiple_columns: tmp.append(col) @@ -906,7 +904,7 @@ def generate_html_header_table(): table_html += "</tr>" table_html += "</tbody></table>" - print(table_html) + # print(table_html) return table_html @@ -947,13 +945,13 @@ def generate_html_data_table(data_tables:list, table_type:str): table_html += "<td><button type='button' class='btn-close' aria-label='Close' onclick='deleteRow2(event, this)'></button></td>" for i in range(len(row) - 1): if i == 1: - table_html += f"<td>{row.get('column3', '')}</td>" + table_html += f"<td>{row.get('column3', '')}</td>" # object elif i == 2: - table_html += f"<td>{row.get('column4', '')}</td>" + table_html += f"<td>{row.get('column4', '')}</td>" # segment elif i == 3: - table_html += f"<td>{row.get('column2', '')}</td>" + table_html += f"<td>{row.get('column2', '')}</td>" # segment_index else: - table_html += f"<td>{row.get('column' + str(i+1), '')}</td>" + table_html += f"<td>{row.get('column' + str(i+1), '')}</td>" # label or feature table_html += "</tr>" else: for row in data_tables: @@ -1013,16 +1011,28 @@ def getObjectColumns(table_name:str) -> list: return object_columns -def getIndexColumns(table_name:str) -> list: +def getIndexColumns(table_name:str, isSegmentTable:bool) -> Tuple[List[str], List[str]]: engine = create_engine(session.get('db_uri', '')) insp = inspect(engine) schema = getTableSchema(table_name) if insp.dialect.name == 'postgresql' else insp.default_schema_name columns = insp.get_columns(table_name, schema) + table = getTableInstance(engine, table_name) + + fk_or_pk_list = [] + if isSegmentTable: + for col in table.columns: + if col.ispk == True: + fk_or_pk_list.append(col.name) + else: + for col in table.columns: + if col.fkof != None: + fk_or_pk_list.append(col.name) + index_columns = [column['name'] for column in columns if str(column['type']) == 'INTEGER' or str(column['type']) == 'NUMERIC' or str(column['type']) == 'BIGINT' or str(column['type']) == 'SMALLINT'] print("index columns") print(index_columns) - return index_columns + return (index_columns, fk_or_pk_list) def query_database_for_table_content(engine, table_name, number=200): @@ -1076,7 +1086,7 @@ def getSchema(insp): return schemas -def getTableInstance(engine, table_name): +def getTableInstance(engine, table_name) -> CustomTable: insp = inspect(engine) table = importMetadata(engine, None, [table_name], True)[table_name] return table @@ -1099,19 +1109,22 @@ def showDistinctValues(engine, table_name, column_name): return names -def get_min_max_datetime(engine, table_name, time_column, start_time=None, end_time=None): +def get_min_max_datetime(engine, table_name, time_column, label_value, sql_where=None): schema = getTableSchema(table_name) if engine.dialect.name == 'postgresql' else engine.dialect.default_schema_name # Formulate the SQL query using the text function - query = text(f"SELECT MIN({time_column}) AS start_datetime, MAX({time_column}) AS end_datetime FROM {schema}.{table_name};") + sql_join = '' + query = text(f"SELECT MIN({time_column}) AS start_datetime, MAX({time_column}) AS end_datetime FROM {schema}.{table_name} {sql_join} {sql_where};") + + params = {'label_value': label_value} # Execute the query with engine.connect() as connection: - row = connection.execute(query).mappings().fetchone() - + row = connection.execute(query, params).mappings().fetchone() # Extract the min and max datetime values if row: - min_datetime, max_datetime = row['start_datetime'], row['end_datetime'] + min_datetime = row['start_datetime'].replace(tzinfo=datetime.timezone.utc).isoformat() + max_datetime = row['end_datetime'].replace(tzinfo=datetime.timezone.utc).isoformat() print("Minimum datetime:", min_datetime) print("Maximum datetime:", max_datetime) return min_datetime, max_datetime @@ -1120,18 +1133,22 @@ def get_min_max_datetime(engine, table_name, time_column, start_time=None, end_t return None, None -def get_min_max_datetime2(engine, table_name, starttime_column, endtime_column, start_time=None, end_time=None): +def get_min_max_datetime2(engine, table_name, starttime_column, endtime_column, label_value, sql_where=None): schema = getTableSchema(table_name) if engine.dialect.name == 'postgresql' else engine.dialect.default_schema_name # Formulate the SQL query using the text function - query = text(f"SELECT MIN({starttime_column}) AS start_datetime, MAX({endtime_column}) AS end_datetime FROM {schema}.{table_name};") + sql_join = '' + query = text(f"SELECT MIN({starttime_column}) AS start_datetime, MAX({endtime_column}) AS end_datetime FROM {schema}.{table_name} {sql_join} {sql_where};") + + params = {'label_value': label_value} # Execute the query with engine.connect() as connection: - row = connection.execute(query).mappings().fetchone() + row = connection.execute(query, params).mappings().fetchone() # Extract the min and max datetime values if row: - min_datetime, max_datetime = row['start_datetime'], row['end_datetime'] + min_datetime = row['start_datetime'].replace(tzinfo=datetime.timezone.utc).isoformat() + max_datetime = row['end_datetime'].replace(tzinfo=datetime.timezone.utc).isoformat() print("Minimum datetime:", min_datetime) print("Maximum datetime:", max_datetime) return min_datetime, max_datetime @@ -1140,14 +1157,17 @@ def get_min_max_datetime2(engine, table_name, starttime_column, endtime_column, return None, None -def get_min_max_index(engine, table_name, index_column): +def get_min_max_index(engine, table_name, index_column, label_value, sql_where): schema = getTableSchema(table_name) if engine.dialect.name == 'postgresql' else engine.dialect.default_schema_name # Formulate the SQL query using the text function - query = text(f"SELECT MIN({index_column}) AS start_index, MAX({index_column}) AS end_index FROM {schema}.{table_name};") + sql_join = '' + query = text(f"SELECT MIN({index_column}) AS start_index, MAX({index_column}) AS end_index FROM {schema}.{table_name} {sql_join} {sql_where};") + + params = {'label_value': label_value} # Execute the query with engine.connect() as connection: - row = connection.execute(query).mappings().fetchone() + row = connection.execute(query, params).mappings().fetchone() # Extract the min and max datetime values if row: @@ -1160,7 +1180,7 @@ def get_min_max_index(engine, table_name, index_column): return None, None -def extract_ME_table(engine, table_name: str, type: str, time_column: str, object: list, label: list, features_name: list, start_time: datetime = None, end_time: datetime = None) -> list: +def extract_ME_table(engine, table_name: str, type: str, time_column: str, object: list, label: list, features_name: list, start_time: datetime = None, end_time: datetime = None): conn = engine.connect() insp = inspect(engine) database_name = insp.dialect.name # 'postgresql' or 'sqlite' @@ -1223,7 +1243,7 @@ def extract_ME_table(engine, table_name: str, type: str, time_column: str, objec if end_time: sql_where += f" AND {full_table_name}.{time_column} <= :end_time" - sql_query = f"SELECT {sql_select} FROM {full_table_name} {sql_joins} {sql_where} ORDER BY {time_column} ASC LIMIT 500" + sql_query = f"SELECT {sql_select} FROM {full_table_name} {sql_joins} {sql_where} ORDER BY {time_column} ASC" print("12345") # Executing the query params = {'label_value': label_value} @@ -1244,7 +1264,7 @@ def extract_ME_table(engine, table_name: str, type: str, time_column: str, objec return [] # Append object and label values if necessary - final_res = [] + final_res = [] # List of rows for row in res: modified_row = list(row) if object[0].strip() == 'self': @@ -1255,10 +1275,15 @@ def extract_ME_table(engine, table_name: str, type: str, time_column: str, objec modified_row.insert(2, type) final_res.append(modified_row) - for row in final_res: - print(row) + # for row in final_res: + # print(row) + print("Row count", len(final_res)) - return final_res + if start_time == None and end_time == None: + start_time, end_time = get_min_max_datetime(engine, table_name, time_column, label_value, sql_where) + return final_res, len(final_res), start_time, end_time + + return final_res, len(final_res) def extract_S_table(engine, table_name: str, starttime_column: str, endtime_column: str, index_column: str, object: list, label: list, start_time: datetime = None, end_time: datetime = None) -> list: @@ -1315,7 +1340,7 @@ def extract_S_table(engine, table_name: str, starttime_column: str, endtime_colu if end_time: sql_where += f" AND {full_table_name}.{starttime_column} <= :end_time AND {full_table_name}.{endtime_column} <= :end_time" - sql_query = f"SELECT {sql_select} FROM {full_table_name} {sql_joins} {sql_where} ORDER BY {starttime_column} ASC LIMIT 500" + sql_query = f"SELECT {sql_select} FROM {full_table_name} {sql_joins} {sql_where} ORDER BY {starttime_column} ASC" print("12345") # Executing the query params = {'label_value': label_value} @@ -1347,10 +1372,15 @@ def extract_S_table(engine, table_name: str, starttime_column: str, endtime_colu final_res.append(modified_row) - for row in final_res: - print(row) + # for row in final_res: + # print(row) + print("Row count", len(final_res)) + + if start_time == None and end_time == None: + start_time, end_time = get_min_max_datetime2(engine, table_name, starttime_column, endtime_column, label_value, sql_where) + return final_res, len(final_res), start_time, end_time - return final_res + return final_res, len(final_res) def extract_SD_table(engine, table_name: str, object: list, label: list, segment_column: str, index_column: str, features_name: list, index_from: int = None, index_to: int = None) -> list: @@ -1385,8 +1415,10 @@ def extract_SD_table(engine, table_name: str, object: list, label: list, segment print("123") # Adding index columns to the select clause sql_columns.append(f"{full_table_name}.{index_column}") + # Handling JSON extractions json_extractions = [] + json_non_none_checks = [] for feature in features_name: if '(' in feature and ')' in feature: column_name, keys = feature[:-1].split('(') @@ -1394,31 +1426,48 @@ def extract_SD_table(engine, table_name: str, object: list, label: list, segment for key in keys: if database_name == 'postgresql': json_extraction = f"{full_table_name}.{column_name}->>'{key}' AS {key}" + # For checking non-None JSON keys + json_non_none_checks.append(f"{full_table_name}.{column_name}->>'{key}' IS NOT NULL AND {full_table_name}.{column_name}->>'{key}' != ''") elif database_name == 'sqlite': json_extraction = f"json_extract({full_table_name}.{column_name}, '$.{key}') AS {key}" + # Adjust the check for SQLite if necessary + json_non_none_checks.append(f"json_extract({full_table_name}.{column_name}, '$.{key}') IS NOT NULL") json_extractions.append(json_extraction) else: sql_columns.append(f"{full_table_name}.{feature}") - print("1234") + # Non-JSON field non-None check + json_non_none_checks.append(f"{full_table_name}.{feature} IS NOT NULL") + # Adding JSON extractions to the select clause sql_select = ', '.join(sql_columns + json_extractions) - + # Constructing SQL query sql_joins = join_clause + + # Building the WHERE clause with segment index filtering + sql_where_list = [] if label[0].strip() == 'col': - sql_where = f"WHERE {full_table_name}.{label_column} = :label_value" - if index_from: - sql_where += f" AND {full_table_name}.{index_column} >= :index_from" - if index_to: - sql_where += f" AND {full_table_name}.{index_column} <= :index_to" - else: - sql_where = '' - if index_from: - sql_where += f" WHERE {full_table_name}.{index_column} >= :index_from" - if index_to: - sql_where += f" AND {full_table_name}.{index_column} <= :index_to" + sql_where_list.append(f"{full_table_name}.{label_column} = :label_value") + if index_from: + sql_where_list.append(f"{full_table_name}.{index_column} >= :index_from") + if index_to: + sql_where_list.append(f"{full_table_name}.{index_column} <= :index_to") + + # Add non-None checks for JSON and non-JSON fields + if json_non_none_checks: + sql_where_list.append(f"({' OR '.join(json_non_none_checks)})") + + # Add segment index filtering + valid_segment_indices = get_distinct_segment_indices(segment_column) # Set of segment indices with segment label = segment_column in the data table with type S + indices = ', '.join(map(str, valid_segment_indices)) if valid_segment_indices else '' + if indices == '': + print("No valid segment indices found.") + return [], 0, 0, 0 + sql_where_list.append(f"{full_table_name}.{index_column} IN ({indices})") + + sql_where = f"WHERE {' AND '.join(sql_where_list)}" - sql_query = f"SELECT {sql_select} FROM {full_table_name} {sql_joins} {sql_where} ORDER BY {index_column} ASC LIMIT 500" + sql_query = f"SELECT {sql_select} FROM {full_table_name} {sql_joins} {sql_where} ORDER BY {index_column} ASC" print("12345") # Executing the query params = {'label_value': label_value, 'index_from': index_from, 'index_to': index_to} @@ -1441,15 +1490,42 @@ def extract_SD_table(engine, table_name: str, object: list, label: list, segment if object[0].strip() == 'self': modified_row.insert(0, object_column_value) if label[0].strip() == 'self': - label_index = 1 if object[0].strip() != 'self' else 0 + label_index = 1 if object[0].strip() != 'self' else 1 modified_row.insert(label_index, label[2]) modified_row.insert(2, segment_column) final_res.append(modified_row) - for row in final_res: - print(row) + # for row in final_res: + # print(row) + print("Row count", len(final_res)) + + if index_from == None and index_to == None: + index_from, index_to = get_min_max_index(engine, table_name, index_column, label_value, sql_where) + return final_res, len(final_res), index_from, index_to + + return final_res, len(final_res) - return final_res + +def get_distinct_segment_indices(segment_label:str) -> set: + data_tables = session.get('data_tables', {'O':[], 'S':[], 'SD':[]}) + data_table = data_tables.get('S', []) + + distinct_values = set() + print("segment label " + segment_label) + for row in data_table: + print(row) + # Ensure both strings are stripped of leading/trailing whitespace before comparison + if row.get('column2', '').strip() == segment_label.strip(): + segment_index = row.get('column3', '').strip() # Also strip any whitespace from the index + print("I AM HERE" + segment_index) + distinct_values.add(segment_index) + + # Print the distinct values + print("Distinct segment indices from S data table:") + for value in distinct_values: + print(value) + + return distinct_values def get_ME_table_HTML(data: list) -> str: @@ -1460,7 +1536,7 @@ def get_ME_table_HTML(data: list) -> str: for row in data: html_content += "<tr><td><input class='uk-checkbox' type='checkbox' aria-label='Checkbox'></td>" for cell in row: - if isinstance(cell, datetime): + if isinstance(cell, datetime.datetime): # cell = cell.isoformat() cell = cell.strftime('%Y-%m-%d %H:%M:%S.%f') html_content += f"<td>{cell}</td>" @@ -1469,7 +1545,7 @@ def get_ME_table_HTML(data: list) -> str: return html_content -def importMetadata(engine, schema=None, tables_selected=None, show_all=False): +def importMetadata(engine, schema=None, tables_selected=None, show_all=False) -> dict: # -> Dict[str, CustomTable] tables = {} if engine == None: return tables @@ -1670,9 +1746,9 @@ def fetch_constraints_for_tables(engine, tables): if fkColumn and pkColumn: fkColumn.fkof = pkColumn - if fk['name'] not in table.fks: - table.fks[fk['name']] = [] - table.fks[fk['name']].append(fkColumn) + if referred_table not in table.fks: + table.fks[referred_table] = [] + table.fks[referred_table].append(fkColumn) return tables diff --git a/models/__pycache__/models.cpython-311.pyc b/models/__pycache__/models.cpython-311.pyc index 780e177e25900a2bae7265a22e612d24724b20c3..5392b414bd04ce2f81d54089b3d43f92814b8e68 100644 GIT binary patch delta 18 ZcmX?Je!QG>IWI340|Ntt$wtn7)&Mzz1vmfz delta 18 ZcmX?Je!QG>IWI340|Ntt(nijG)&Myu1ttIh diff --git a/models/__pycache__/models.cpython-39.pyc b/models/__pycache__/models.cpython-39.pyc index 4dcb5a8fa921e8dc45deed97019797f6eac214e2..1c63b8dea427ada5d48f89e1fd38f8e40f3f28fa 100644 GIT binary patch delta 223 zcmccNal?Zzk(ZZ?fq{YHL@96DdzFoRBAkqdla)DR7!5WzaE3E7I!wO7^_9_k@;UCQ zjDeF=c;+#NY-Z*)XJL%mY%Tbnk+E{Jw(u9muE`1_XBm4ZzZ1!1?42ATTFuzH`HH9k zBjc3GHWFHs^~KjSPM>^VJce=lWE+WE#tD;mNz7oh-yAHN!^k*i@&Tz<##Ni$q_;CM zuG{=iR+EwO%w`RFdq&2alQR_R7-vs@u5g9%<>Wnz(Tp!9ODN4?d^>rgl0D;_$sd(w fFnwg0+^uZK^nqdWY2_wv0X}}d7(QPC76BFjCrnGz delta 223 zcmccNal?Zzk(ZZ?fq{X+q@FR2QGFwy2q&ZCWM$46Mu*J}oZ*a&0h4cVePxWEe2#l6 zW8&l#o_UNZo0)mdSs1f6TMNEtWbB-*E&PRX)?@{dvy5{mzZ1!1oI5!}w3>11<}0EG zjEqYr+em0l))!yTxP0<`@fgPClWinw85d06B{751e{-;84kP25$p@rb8Fy`Vlitq6 zxNq}6SxrX9JDWA+?HL(gPR>xMV_ZG?xxy93pOg0}Ml=4HETJ@m@$ck~O7@I@CVy0# f!NkZoxm($eiGgwQY2_wv1wMYh7(QPC76BFjiCaoE diff --git a/models/models.py b/models/models.py index 9c517d5..2017989 100644 --- a/models/models.py +++ b/models/models.py @@ -6,7 +6,7 @@ class Observation_Spec: def __init__(self, tablename:str, type:str, label:list[str], features_name:list = None): self.tablename = tablename self.type = type # 'event' or 'measurement' - self.label = label # (self-define or from column, column name, column value or self-define label) + self.label = label # [self-define or from column, column name, column value or self-define label] self.features_name = features_name if features_name else [] def add_feature(self, name): @@ -44,22 +44,6 @@ class Obesrvation(Observation_Spec): def add_feature(self, feature_name, feature_value): self.features[feature_name] = feature_value -# class Segment_Spec: -# def __init__(self, type, label, features=None): -# self.type = type # 'event' or 'measurement' -# self.label = label -# self.features = features if features else {} - -# def add_feature(self, feature_name): -# self.features[feature_name] = feature_value - -# class Segment(Segment_Spec): -# def __init__(self, time, type, object, label, features=None): -# super().__init__(type, label, features=None) -# self.time = time -# self.object = object - - class Theme: def __init__(self, color, fillcolor, fillcolorC, diff --git a/templates/app.html b/templates/app.html index 44e755e..36462f1 100644 --- a/templates/app.html +++ b/templates/app.html @@ -160,12 +160,12 @@ } .resize-handle-left { position: sticky; - margin-top: -100px; + margin-top: -550px; margin-left: -13px; /* Adjust this value to position the handle to the left */ width: 8px; /* Width of the handle */ - height: 100vh; + height: 100%; cursor: ew-resize; - z-index: 10; + z-index: 100; } .custom-buttom-bar { display: flex; @@ -293,6 +293,49 @@ height: 83vh; /* or adjust as necessary */ overflow: hidden; /* This prevents the overall container from scrolling */ } + + .key { + display: inline-block; + width: 15px; + height: 15px; + margin-right: 5px; + line-height: 200%; + } + + .primary-key { + /* padding-bottom: -3px; */ + border-bottom: 1px solid black; /* Represent underline */ + width: 15px; + height: 15px; + margin-right: 5px; + line-height: 120%; + } + + .foreign-key { + font-style: italic; /* Represent foreign key */ + } + + .nullable { + content: "*"; /* Represent nullable */ + border-top: 2px; + } + + .identity { + content: "I"; /* Represent identity column */ + } + + .unique { + content: "U"; /* Represent unique constraint */ + } + + .normal-fk { + border-bottom: 3px solid black; /* Represent nullable FK relationship */ + } + + .optional-fk { + border-bottom: 3px dashed black; /* Represent nullable FK relationship */ + } + </style> </head> <body> @@ -303,9 +346,9 @@ <div id="sidebar1"> <!--class="uk-panel uk-panel-scrollable" --> <legend uk-tooltip="title: Show the database name.; pos: right">Database:</legend> <div class="mb-3"> - <label for="disabledTextInput" class="form-label" style="margin-top: 10px;">{{database}}.db</label> + <label class="form-label" style="margin-top: 10px;">{{database}}.db</label> </div> - + <hr> <ul uk-accordion> <li> <a class="uk-accordion-title" href="#" style="text-decoration: none;" uk-tooltip="title: Provide a valid databsae connection here.; pos: right">Database URL</a> @@ -394,10 +437,9 @@ </li> - <li class="uk-class uk-open"></li> + <li class="uk-open"> <a class="uk-accordion-title" href="#" style="text-decoration: none;" uk-tooltip="title: Reorder the tables by dragging the rows to the designated area below.; pos: right">Tables</a> <div class="uk-accordion-content"> - <div class="border border-secondary rounded" id="table_list" style="margin-bottom: 5px; margin-top: -10px;"> <div id="table_list_source"> <div id="show_tables1" uk-sortable="group: sortable-group" class="uk-list uk-list-collapse"> @@ -409,17 +451,15 @@ </div> </div> </div> - </div> </li> </ul> - - - <legend style="margin-top: 3px;" uk-tooltip="title: Activate the selected table to populate the data in the scrollable terminal section and for Step 2 procedures.; pos: right">Target Tables:</legend> - <div class="border border-secondary rounded" id="table_list" > + <hr> + <legend style="margin-top: 2px;" uk-tooltip="title: Activate the selected table to populate the data in the scrollable terminal section on the bottom and for Step 2 procedures.; pos: right">Target Tables:</legend> + <div class="border border-secondary rounded" id="table_list" style="margin-top: 3px;"> <div id="dropped_items" uk-sortable="group: sortable-group" class="uk-list uk-list-collapse "> {% for item in dropped_items %} - <div class="uk-margin" style="height: 15px; margin-bottom: -4px; width: 230px;"> + <div class="uk-margin" style="height: 23px; margin-bottom: -4px; width: 230px;"> <div class="list-group-item-action uk-card uk-card-default uk-card-body uk-card-small" style="height: 15px; padding-top: 5px;">{{ item }}</div> </div> {% endfor %} @@ -431,10 +471,10 @@ <div id="content"> <button class="uk-button uk-button-default uk-button-small custom-push-button" type="button" uk-toggle="target: #sidebar1" onclick="toggleSidebar()"></button> <ul uk-tab class="uk-flex-center" data-uk-tab="{connect:'#my-id'}" style="margin-top: 10px; z-index: 0;"> - <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Display a comprehensive summary of the database contents.;pos: bottom">Step1: Overview</a></li> - <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Construct the data header based on the selection from the prior step, to be utilized in Step 3.;pos: bottom">Step2: Create Data Header</a></li> - <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Generate a detailed data table corresponding to each row of the data header, with options for additional refinement.;pos: bottom">Step3: Create Data instance</a></li> - <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Display the generated data tables, with options for exporting to csv files.;pos: bottom">Step4: Established Data Table</a></li> + <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Display a comprehensive structure of the database contents.;pos: bottom">Step1: Overview</a></li> + <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Construct data row for header based on the selection from the target table section, an overview of header table is shown in Step 3.;pos: bottom">Step2: Create Header</a></li> + <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Generate a detailed data table corresponding to each row of the data header, with options for additional refinement on the right sidebar.;pos: bottom">Step3: Select Data Instance</a></li> + <li><a href="#" style="text-decoration: none;" uk-tooltip="title: Display the generated data tables, with options for exporting to csv files.;pos: bottom">Step4: Data Table Overvieew</a></li> </ul> <!-- <div> --> @@ -443,7 +483,7 @@ <div style="display: flex; flex-direction: row; height: 85vh; border: 0px 1px;"> <div style="flex: 0,3; display: flex; flex-direction: column;"> - <h4 style="margin: .0em .0em .6em .2em;">Filtered ERD</h4> + <h4 style="margin: .0em .0em .6em .2em;" uk-tooltip= "title: Show overall Entity-Relationship-Diagram (ERD), optional filtering tool can be applied for constraining this ERD with the table instances showing on the Tables section. ; pos: bottom">Filtered Tables - ERD</h4> <div class="uk-panel uk-panel-scrollable" id="canvasContainer" style="margin-right: -1px; min-width: 30%;"> <canvas id="erdCanvas1" style="min-width: 30%;"></canvas> <div id="zoomButtons"> @@ -454,7 +494,7 @@ </div> <div style="flex: 0,7; display: flex; flex-direction: column;"> - <h4 style="margin: .0em .0em .6em .2em;">Target ERD</h4> + <h4 style="margin: .0em .0em .6em .2em;" uk-tooltip="title: Build a customized Entity-Relationship-Diagram by dragging the table instances in Tables section on the left sidebar into the Target Tables section below.; pos: bottom">Target Tables - ERD</h4> <div class="uk-panel" id="canvasContainer" style="overflow-y: scroll; min-width: 20%; width: 100%;"> <canvas id="erdCanvas2" style="min-width: 30%;"></canvas> <div id="zoomButtons"> @@ -471,7 +511,7 @@ <div style="display: flex; flex-direction: row; height: 87vh;"> <div id="scrollable-panel" style="display: flex; flex-direction: column;"> - <h4 style="margin: .0em .0em .6em .2em;">Target ERD</h4> + <h4 style="margin: .0em .0em .6em .2em;" uk-tooltip="Build a customized Entity-Relationship-Diagram by dragging the table instances in Tables section on the left sidebar into the Target Tables section below.";>Target Tables - ERD</h4> <div class="uk-panel" id="canvasContainer" style="overflow-y: auto;"> <canvas id="erdCanvas3" style="min-width: 30%;"></canvas> <div id="zoomButtons"> @@ -484,10 +524,10 @@ <div style="flex-grow: 1; padding-left: 10px; margin-top: 3px; display: flex; flex-direction: column;"> <fieldset> - <legend style="margin: .2em .2em .4em -.4em; color:#5ea9e2; border-bottom: 1px solid gray; padding-bottom: 5px; padding-right: 3px;" uk-tooltip="title: Define the structure of the header table to be used in Step 3; pos: right">Data Header</legend> + <legend style="margin: .2em .2em .4em -.4em; color:#5ea9e2; border-bottom: 1px solid gray; padding-bottom: 5px; padding-right: 3px;" uk-tooltip="title: Define the structure of the header table to be used in Step 3. Active one table instance by clicking table in Target Tables section on the left sidebar.; pos: right">Data Header</legend> <div class="mb-3" style="padding-bottom: 5px; border-bottom: 1px dashed black; margin-bottom: 3px;"> - <label class="form-label" style="margin-top: 8px;" uk-tooltip="title: Select the type of data header; each type requires different information to be specified; pos: left">Type</label> + <label class="form-label" style="margin-top: 8px;" uk-tooltip="title: Select the data type for the activated table; each type requires different information to be specified; pos: left">Type</label> <div class="uk-grid-small uk-child-width-auto uk-grid"> <label><input class="uk-radio type-radio" type="radio" name="radio1" value="measurement" checked> Measurement </label><br><br> <label><input class="uk-radio type-radio" type="radio" name="radio1" value="event"> Event </label><br><br> @@ -569,11 +609,11 @@ <li> <div style="display: flex; flex-direction: column; height: 87vh;"> - <div class="mb-3"> - <h4 style="display: inline; margin-left: .2em;">Data Header Table</h4> - <button type="submit" class="btn btn-primary uk-button-small headerButton" style="margin-top: -3px;" onclick="resetDataHeaderTable()">Reset</button> + <div class="mb-3" style="display: flex; justify-content: left; align-items: center; padding-left: .2em; background-color:#add8e6"> + <h5 style="display: inline; padding-top: 10px;" uk-tooltip="title: Provide an overview of the header table. Select one data row inside the header data and fulfill the column forms in Data Table section to extract corresponding data.">Header Table</h5> + <button class="btn btn-primary uk-button-small headerButton" style="margin-left: 3px;" onclick="resetDataHeaderTable()">Reset</button> </div> - <div class="uk-overflow-auto" id="data-header-table" style="flex-grow: 1; overflow-y: auto; max-height: max-content; margin-top: -20px;"> + <div class="uk-overflow-auto" id="data-header-table" style="flex-grow: 1; max-height: 27vh; margin-top: -20px; margin-bottom: 0px;"> <table id="H-table" class='uk-table uk-table-small uk-table-hover uk-table-divider'> <thead> <tr> @@ -585,14 +625,15 @@ </thead> </table> </div> - <div class="mb-3"> - <h4 style="display: inline; margin-left: .2em;">Machine Data Table (LIMIT 500)</h4> - <button type="submit" class="btn btn-primary uk-button-small headerButton" style="margin-top: -3px;" onclick="resetMachineDataTable()">Reset</button> - <button type="submit" class="btn btn-primary uk-button-small headerButton" style="margin-top: -3px;" onclick="addDataTable()">Add</button> + <div class="mb-3" style="display: flex; justify-content: left; align-items: center; padding-left: .2em; border-top: gray solid 0px; background-color:#add8e6;"> + <h5 style="display: inline; padding-top: 10px;" uk-tooltip="title: Show all the dataset under the selected header instance and specific columns. First, select one header row by clicking. Second, fulfill the following column options. Then the data table will automatically be generated, and you can start selecting and press add button to send them into the final data table in step 4.; pos: bottom">Data Table</h5> + <button class="btn btn-primary uk-button-small headerButton" style="margin-left: 3px;" onclick="resetMachineDataTable()" >Reset</button> + <button class="btn btn-primary uk-button-small headerButton" style="margin-left: 3px;" onclick="addDataTable()">Add</button> + <h5 style="margin-left: 10px; display: inline; padding-top: 10px;">Row Count:</h5> <label id="row-count" class="form-label" style="margin-left: 3px; padding-top: 10px;">0</label> <span id="addDataTable" style="color: red; font-size: 80%; margin-left: 2px; margin-bottom: 1px;"></span> </div> - <div class="uk-overflow-auto" id="machine-data-table" style="flex-grow: 1; overflow-y: auto; max-height: max-content; margin-top: -20px;" > - <table id="MD-table" class="uk-table uk-table-hover uk-table-small uk-table-middle uk-table-divider uk-table-striped" style="cursor: pointer;"> + <div class="uk-overflow-auto" id="machine-data-table" style="flex-grow: 1; margin-top: -20px;" > + <table id="MD-table" class="uk-table uk-table-hover uk-table-small uk-table-middle uk-table-divider uk-table-striped" style="cursor: pointer; height: max-content;"> <thead> <tr class="uk-table-middle"> <th class="uk-table-shrink"> @@ -670,29 +711,34 @@ </div> </li> <li> - <div class="accordion-container"> - <button type="submit" class="btn btn-primary uk-button-small headerButton" onclick="resetMachineDataTable2()" style="position: absolute; right: 13vh; top:32px;">Reset</button> - <button type="submit" class="btn btn-primary uk-button-small headerButton" onclick="exportSelectedRowsToCSV()" style="position: absolute; right: 1vh; top:32px;">Export</button> + <div style="display: flex; justify-content: space-between; align-items:end; margin-top: -15px; z-index: 10; padding-left: .2em; padding-right: .2em;"> + <h5 style="margin-bottom: .2em;">Data Tables</h5> + <div> + <button class="btn btn-primary uk-button-small headerButton" onclick="resetMachineDataTable2()">Reset</button> + <button class="btn btn-primary uk-button-small headerButton" onclick="exportSelectedRowsToCSV()">Export</button> + </div> + </div> + <div class="accordion-container" style="margin-top: -2px;"> <ul uk-accordion> <li class="uk-open"> - <a class="uk-accordion-title" style="text-decoration: none; background-color: #ccc;" href>Observation Table</a> - <div class="uk-accordion-content" style="max-height: 60vh; flex: 1; overflow-y: auto;"> + <a class="uk-accordion-title" style="padding-left: .2em; text-decoration: none; background-color: #ccc;" href> Observation</a> + <div class="uk-accordion-content uk-overflow-auto" style="max-height: 60vh; flex: 1;"> <!-- <h4 style="display: inline; margin-left: .2em;">Observation Table</h4> --> <table id="ME-table" class="uk-table uk-table-hover uk-table-small uk-table-middle uk-table-divider uk-table-striped" style="cursor: pointer;"> </table> </div> </li> <li> - <a class="uk-accordion-title" style="text-decoration: none; background-color: #ccc;" href>Segment Table</a> - <div class="uk-accordion-content" style="max-height: 60vh; flex: 1; overflow-y: auto;"> + <a class="uk-accordion-title" style="padding-left: .2em; text-decoration: none; background-color: #ccc;" href> Segment</a> + <div class="uk-accordion-content uk-overflow-auto" style="max-height: 60vh; flex: 1;"> <table id="S-table" class="uk-table uk-table-hover uk-table-small uk-table-middle uk-table-divider uk-table-striped" style="cursor: pointer;"> </table> </div> </li> <li> - <a class="uk-accordion-title" style="text-decoration: none; background-color: #ccc;" href>Segment Data Table</a> - <div class="uk-accordion-content" style="max-height: 60vh; flex: 1; overflow-y: auto;"> - <table id="SD-table" class="uk-table uk-table-hover uk-table-small uk-table-middle uk-table-divider uk-table-striped" style="cursor: pointer;"> + <a class="uk-accordion-title" style="padding-left: .2em; text-decoration: none; background-color: #ccc;" href> Segment Data</a> + <div class="uk-accordion-content uk-overflow-auto" style="max-height: 60vh; flex: 1;"> + <table id="SD-table" class="uk-table uk-table-hover uk-table-small uk-table-middle uk-table-divider uk-table-striped" style="cursor: pointer; overflow: scroll;"> </table> </div> </li> @@ -706,7 +752,7 @@ <div class="custom-buttom-bar"> <div id="terminal" class="terminal"> <div id="terminal-header" class="terminal-header"> - <span style="position: absolute; left: 5px; margin-top: 7px">Table</span><span aria-hidden="true" uk-icon="menu" style="margin-top: 4px;"></span> + <span style="position: absolute; left: 5px; margin-top: 7px" uk-tooltip="Show sample data rows for the activated table instance from Target Tables section on the left sidebar.";>Table</span><span aria-hidden="true" uk-icon="menu" style="margin-top: 4px;"></span> <button id="myModal" type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal" style="position: absolute; right: 1vh; margin-top: 1px; z-index: 1000;" onclick="closeTerminal()"></button> </div> <div id=terminal-body class="terminal-body"> @@ -718,70 +764,76 @@ </div> <div id="sidebar2"> - <ul uk-accordion> - <li> - <a class="uk-accordion-title" style="text-decoration: none;" href="#">Filter</a> - <div class="uk-accordion-content"> - <div class="accordion"> - <div class="accordion-item"> - <h2 class="accordion-header"> - <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse1" aria-expanded="true" aria-controls="collapse1"> - Time - </button> - </h2> - <div id="collapse1" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> - <div class="accordion-body"> - <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> - <script src="//code.jquery.com/jquery-3.6.0.min.js"></script> - <script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script> - - <p style="margin: -4px -21px 5px -17px; padding: 0px;"> - <label for="amount1">From: </label><br> - <input type="text" id="amount1" class="form-control" style="display: flex; border: 0; color: #5ea9e2; font-weight:bold;"><br> - <label for="amount2">Until: </label><br> - <input type="text" id="amount2" class="form-control" style="border:0; color: #5ea9e2; font-weight:bold;"> - </p> - <div id="slider-range"></div> - <button type="submit" class="btn btn-primary headerButton" style="margin: 10px 0px -5px -10px;" onclick="filter_ME_data_table()">Submit</button> - </div> - </div> - </div> - <div class="accordion-item"> - <h2 class="accordion-header"> - <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse2" aria-expanded="true" aria-controls="collapse2"> - Segment Index - </button> - </h2> - <div id="collapse2" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> - <div class="accordion-body"> - <p style="margin: -4px -21px 5px -17px; padding: 0px;"> - <label for="fromInput">From: </label> - <input type="number" id="fromInput" min="0" step="1" style="width: 8vh;"/><br><br> - <label for="toInput">To: </label> - <input type="number" id="toInput" min="0" step="1" style="width: 8vh; margin-left: 20px;"/> - </p> - <button type="submit" class="btn btn-primary headerButton" style="margin: 10px 0px -5px -10px;" onclick="filter_SD_data_table()">Submit</button> - <script> - var fromInput = document.getElementById('fromInput'); - var toInput = document.getElementById('toInput'); - - fromInput.addEventListener('input', function() { - toInput.min = this.value; - if (parseInt(toInput.value) < parseInt(this.value)) { - toInput.value = this.value; - } - }); - </script> - </div> - </div> - </div> + <div class="uk-accordion-content"> + <legend>Filter</legend> + <hr> + <div class="accordion"> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse1" aria-expanded="true" aria-controls="collapse1"> + Time + </button> + </h2> + <div id="collapse1" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> + <script src="//code.jquery.com/jquery-3.6.0.min.js"></script> + <script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script> + + <p style="margin: -4px -21px 5px -17px; padding: 0px;"> + <label for="amount1">From: </label><br> + <input type="text" id="amount1" class="form-control" style="display: flex; border: 0; color: #5ea9e2; font-weight:bold;"><br> + <label for="amount2">Until: </label><br> + <input type="text" id="amount2" class="form-control" style="border:0; color: #5ea9e2; font-weight:bold;"> + </p> + <div id="slider-range"></div> + <button type="submit" class="btn btn-primary headerButton" style="margin: 10px 0px -5px -10px;" onclick="filter_ME_data_table()">Submit</button> + </div> </div> - </div> - </li> - </ul> - - + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse2" aria-expanded="true" aria-controls="collapse2"> + Segment Index + </button> + </h2> + <div id="collapse2" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + <p style="margin: -4px -21px 5px -17px; padding: 0px;"> + <label for="fromInput">From: </label> + <input type="number" id="fromInput" min="0" step="1" style="width: 10vh;"/><br><br> + <label for="toInput">To: </label> + <input type="number" id="toInput" min="0" step="1" style="width: 10vh; margin-left: 20px;"/> + </p> + <button type="submit" class="btn btn-primary headerButton" style="margin: 10px 0px -5px -10px;" onclick="filter_SD_data_table()">Submit</button> + <script> + var fromInput = document.getElementById('fromInput'); + var toInput = document.getElementById('toInput'); + + fromInput.addEventListener('input', function() { + toInput.min = this.value; + if (parseInt(toInput.value) < parseInt(this.value)) { + toInput.value = this.value; + } + }); + </script> + </div> + </div> + </div> + </div> + </div> + </br></br> + <div> + <legend style="line-height: 120%; margin-bottom: -3px;">ERD Feature</legend> + <hr> + <p><span class="primary-key">pk</span> Primary Key (Underlined)</p> + <p><span class="key foreign-key">fk</span> Foreign Key (Italic)</p> + <p><span class="key nullable">*</span> Nullable (*)</p> + <p><span class="key identity">I</span> Identity Column (I)</p> + <p><span class="key unique">U</span> Unique Constraint (U)</p> + <p><span class="key normal-fk"></span> FK Relationship (Solid Line)</p> + <p><span class="key optional-fk"></span> Nullable FK Relationship (Dashed Line)</p> + </div> <div id="resize-handle-left" class="resize-handle-left"></div> </div> @@ -790,36 +842,38 @@ var currentRowType = null; // Defined at a higher scope, accessible globally var jq = jQuery.noConflict(); + // Function to parse date string with milliseconds + function parseDateTime(str) { + if (str === '0000-00-00 00:00:00.000000+00:00') { + return new Date(0); + } + var date = new Date(str); + if (isNaN(date.getTime())) { + console.error('The datetime format is incorrect:', str); + return new Date(0); // Use Unix epoch as fallback + } + return date; + } + + function toTimestamp(strDate) { + var date = parseDateTime(strDate); + return date ? date.getTime() : new Date(0); + } function initializeSlider(minDatetime, maxDatetime) { // Default values if min or max datetime is not provided - var defaultMinDatetime = '2022-11-01 16:00:00.000000+00:00'; - var defaultMaxDatetime = '2022-11-02 16:00:00.000000+00:00'; + var defaultMinDatetime = '0000-00-00 00:00:00.000000+00:00'; + var defaultMaxDatetime = '0000-00-00 00:00:00.000000+00:00'; // Use default values if min or max datetime is empty minDatetime = minDatetime || defaultMinDatetime; maxDatetime = maxDatetime || defaultMaxDatetime; - - // Function to parse date string with milliseconds - function parseDateTime(str) { - var date = new Date(str); - if (isNaN(date.getTime())) { - console.error('The datetime format is incorrect:', str); - return null; - } - return date; - } - - function toTimestamp(strDate) { - var date = parseDateTime(strDate); - return date ? date.getTime() : null; - } // Function to format date to string with milliseconds function formatDateTime(date) { - var hours = ('0' + date.getHours()).slice(-2); - var minutes = ('0' + date.getMinutes()).slice(-2); - var seconds = ('0' + date.getSeconds()).slice(-2); + var hours = ('0' + date.getUTCHours()).slice(-2); // Use getUTCHours for UTC time + var minutes = ('0' + date.getUTCMinutes()).slice(-2); + var seconds = ('0' + date.getUTCSeconds()).slice(-2); return jq.datepicker.formatDate('yy-mm-dd', date) + ' ' + hours + ':' + @@ -832,25 +886,28 @@ range: true, min: toTimestamp(minDatetime), // Use minDatetime from the server max: toTimestamp(maxDatetime), // Use maxDatetime from the server - step: 1, // Step is now 1 millisecond + step: 1000, // Step is now 1 second values: [ toTimestamp(minDatetime), // Set the lower handle to minDatetime toTimestamp(maxDatetime) // Set the upper handle to maxDatetime ], + // Inside your slider initialization or update functions slide: function(event, ui) { var startDateTime = new Date(ui.values[0]); - var endDateTime = new Date(ui.values[1]); + var endDateTime = new Date(ui.values[1] + 1000); // Add 1 second to include the last millisecond + jq("#amount1").val(formatDateTime(startDateTime)); jq("#amount2").val(formatDateTime(endDateTime)); }, create: function(event, ui) { - // Set the initial datetime values when the slider is created var startDateTime = new Date(jq("#slider-range").slider("values", 0)); - var endDateTime = new Date(jq("#slider-range").slider("values", 1)); + var endDateTime = new Date(jq("#slider-range").slider("values", 1) + 1000); + jq("#amount1").val(formatDateTime(startDateTime)); jq("#amount2").val(formatDateTime(endDateTime)); - } + }, }); + } @@ -888,7 +945,7 @@ function addDataTable() { // Collect all selected rows from the Machine Data Table var selectedRowsData = []; - document.querySelectorAll('#MD-table input[type="checkbox"]:checked').forEach(function(checkbox) { + document.querySelectorAll('#MD-table input[type="checkbox"]:checked:not(#click_all)').forEach(function(checkbox) { var row = checkbox.closest('tr'); var rowData = {}; row.querySelectorAll('td').forEach(function(td, index) { @@ -909,6 +966,13 @@ document.getElementById("addDataTable").textContent = ""; }, 2000); + // Remove the selected rows from the Machine Data Table + document.querySelectorAll('#MD-table input[type="checkbox"]:checked:not(#click_all)').forEach(function(checkbox) { + var row = checkbox.closest('tr'); + row.remove(); + }); + + // Send the selected rows data to the Flask backend fetch('/add-data-table', { method: 'POST', @@ -1180,6 +1244,15 @@ }) .then(response => response.json()) .then(data => { + // Set row coount zero + document.getElementById("row-count").textContent = "0"; + // Set the time frame slider with the empty min and max datetime values + document.getElementById('amount1').value = ''; + document.getElementById('amount2').value = ''; + // Set index inputs to the default value + document.getElementById('fromInput').value = 0; + document.getElementById('toInput').value = 0; + // Set the select element to the default value var selectObject = document.getElementById('table_object'); selectObject.value = "no"; @@ -1270,6 +1343,12 @@ const optionElement = document.createElement('option'); optionElement.value = label_value; optionElement.textContent = label_value; + // Check if the current label_value is in data['fk_index'] and apply a style + if (data['pk_index'] && data['pk_index'].includes(label_value)) { + optionElement.textContent = `[PK] ${label_value}`; + } else { + optionElement.textContent = label_value; + } selectIndex.appendChild(optionElement); }); } else if (type == "SD") { @@ -1285,7 +1364,13 @@ const optionElement = document.createElement('option'); optionElement.value = label_value; optionElement.textContent = label_value; - selectIndex.appendChild(optionElement); + // Check if the current label_value is in data['fk_index'] and apply a style + if (data['fk_index'] && data['fk_index'].includes(label_value)) { + optionElement.textContent = `[FK] ${label_value}`; + } else { + optionElement.textContent = label_value; + } + selectIndex.appendChild(optionElement); }); // Update the object select with the new object columns const selectSegment = document.getElementById('segment-label-select'); @@ -1360,16 +1445,24 @@ }) .then(response => response.json()) .then(data => { - console.log(data['table_HTML']); - const table = document.getElementById('MD-table'); - table.querySelector('tbody').innerHTML = "" - table.querySelector('tbody').innerHTML = data['table_HTML']; + document.getElementById('row-count').textContent = data['row_count']; if ('min_datetime' in data && 'max_datetime' in data) { // Initialize the slider with the min and max datetime from the server console.log("ME: initialize slider with min and max datetime from the server"); + console.log(data['min_datetime'], data['max_datetime']); initializeSlider(data['min_datetime'], data['max_datetime']); } + + const rowCount = parseInt(data['row_count'], 10); + if (rowCount > 10000) { // adjust the threshold as needed + alert("The current column settings return a large amount of data (" + rowCount + " rows). Please use a time-based filter on the right sidebar to reduce the data size for better performance."); + } else { + const table = document.getElementById('MD-table'); + table.querySelector('tbody').innerHTML = "" + table.querySelector('tbody').innerHTML = data['table_HTML']; + } + }) .catch(error => { // Handle any error that occurred during the fetch @@ -1414,16 +1507,22 @@ }) .then(response => response.json()) .then(data => { - console.log(data['table_HTML']); - const table = document.getElementById('MD-table'); - table.querySelector('tbody').innerHTML = "" - table.querySelector('tbody').innerHTML = data['table_HTML']; + document.getElementById('row-count').textContent = data['row_count']; if ('min_datetime' in data && 'max_datetime' in data) { // Initialize the slider with the min and max datetime from the server - console.log("S: initialize slider with min and max datetime from the server"); + console.log("S: initialize slider with min and max datetime from the server", data['min_datetime'], data['max_datetime']); initializeSlider(data['min_datetime'], data['max_datetime']); } + + const rowCount = parseInt(data['row_count'], 10); + if (rowCount > 10000) { // adjust the threshold as needed + alert("The current column settings return a large amount of data (" + rowCount + " rows). Please use a time-based filter on the right sidebar to reduce the data size for better performance."); + } else { + const table = document.getElementById('MD-table'); + table.querySelector('tbody').innerHTML = "" + table.querySelector('tbody').innerHTML = data['table_HTML']; + } }) .catch(error => { // Handle any error that occurred during the fetch @@ -1466,16 +1565,25 @@ }) .then(response => response.json()) .then(data => { - console.log(data['table_HTML']); - const table = document.getElementById('MD-table'); - table.querySelector('tbody').innerHTML = "" - table.querySelector('tbody').innerHTML = data['table_HTML']; + document.getElementById('row-count').textContent = data['row_count']; + if (data['row_count'] == '0') { + alert("The current column settings return 0 rows. No valid segment indices found. Please add data for header row with type S first."); + } if ('min_index' in data && 'max_index' in data) { // Initialize the slider with the min and max datetime from the server console.log("SD: initialize slider with min and max datetime from the server"); - document.getElementById('fromInput').value = min_index; - document.getElementById('toInput').value = max_index; + document.getElementById('fromInput').value = data['min_index']; + document.getElementById('toInput').value = data['max_index']; + } + + const rowCount = parseInt(data['row_count'], 10); + if (rowCount > 10000) { // adjust the threshold as needed + alert("The current column settings return a large amount of data (" + rowCount + " rows). Please use a time-based filter on the right sidebar to reduce the data size for better performance."); + } else { + const table = document.getElementById('MD-table'); + table.querySelector('tbody').innerHTML = "" + table.querySelector('tbody').innerHTML = data['table_HTML']; } }) .catch(error => { @@ -1521,10 +1629,16 @@ }) .then(response => response.json()) .then(data => { - console.log(data['table_HTML']); - const table = document.getElementById('MD-table'); - table.querySelector('tbody').innerHTML = "" - table.querySelector('tbody').innerHTML = data['table_HTML']; + document.getElementById('row-count').textContent = data['row_count']; + + const rowCount = parseInt(data['row_count'], 10); + if (rowCount > 10000) { // adjust the threshold as needed + alert("The current filter settings return a large amount of data (" + rowCount + " rows). Please set a more limited time frame on time-based filter to reduce the data size for better performance."); + } else { + const table = document.getElementById('MD-table'); + table.querySelector('tbody').innerHTML = "" + table.querySelector('tbody').innerHTML = data['table_HTML']; + } }) .catch(error => { // Handle any error that occurred during the fetch @@ -1544,10 +1658,17 @@ }) .then(response => response.json()) .then(data => { - console.log(data['table_HTML']); - const table = document.getElementById('MD-table'); - table.querySelector('tbody').innerHTML = "" - table.querySelector('tbody').innerHTML = data['table_HTML']; + document.getElementById('row-count').textContent = data['row_count']; + + const rowCount = parseInt(data['row_count'], 10); + if (rowCount > 10000) { // adjust the threshold as needed + alert("The current filter settings return a large amount of data (" + rowCount + " rows). Please set a more limited time frame on time-based filter to reduce the data size for better performance."); + } else { + const table = document.getElementById('MD-table'); + table.querySelector('tbody').innerHTML = "" + table.querySelector('tbody').innerHTML = data['table_HTML']; + } + }) .catch(error => { // Handle any error that occurred during the fetch @@ -1598,10 +1719,16 @@ }) .then(response => response.json()) .then(data => { - console.log(data['table_HTML']); - const table = document.getElementById('MD-table'); - table.querySelector('tbody').innerHTML = "" - table.querySelector('tbody').innerHTML = data['table_HTML']; + document.getElementById('row-count').textContent = data['row_count']; + + const rowCount = parseInt(data['row_count'], 10); + if (rowCount > 10000) { // adjust the threshold as needed + alert("The current filter settings return a large amount of data (" + rowCount + " rows). Please set a more limited time frame on time-based filter to reduce the data size for better performance."); + } else { + const table = document.getElementById('MD-table'); + table.querySelector('tbody').innerHTML = "" + table.querySelector('tbody').innerHTML = data['table_HTML']; + } }) .catch(error => { // Handle any error that occurred during the fetch -- GitLab