From dfd99d1575ef2d14d6f808cf2882d18be726dca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Wed, 21 Aug 2024 11:32:36 +0200 Subject: [PATCH 01/39] Metais projects added --- app/assets/images/acf.png | Bin 0 -> 164017 bytes app/assets/images/icons/ai_logo.png | Bin 0 -> 3313 bytes app/assets/images/icons/metais_logo.png | Bin 0 -> 1776 bytes app/assets/images/icons/sd_logo.png | Bin 0 -> 4324 bytes app/assets/images/pontis_white.svg | 1 + app/assets/images/skdigital_biela.svg | 31 ++ app/assets/images/us_embassy.png | Bin 0 -> 123900 bytes app/assets/stylesheets/application.scss | 53 +-- app/assets/stylesheets/footer.scss | 85 ++++ app/assets/stylesheets/google-fonts.scss | 2 + app/assets/stylesheets/navbar.scss | 29 ++ app/assets/stylesheets/projects.scss | 11 +- app/assets/stylesheets/schedule.scss | 100 +++++ app/assets/stylesheets/statuses.scss | 83 ++++ app/assets/stylesheets/variables.scss | 8 +- .../metais/project_origins_controller.rb | 111 +++++ .../admin/metais/projects_controller.rb | 91 +++++ app/controllers/metais/projects_controller.rb | 92 +++++ app/controllers/phase_revision_controller.rb | 1 + app/helpers/metais/projects_helper.rb | 33 ++ ...ink_metais_projects_and_evaluations_job.rb | 16 + app/jobs/metais/daily_sync_projects_job.rb | 13 + app/jobs/metais/initial_sync_projects_job.rb | 13 + app/jobs/metais/sync_project_documents_job.rb | 17 + app/jobs/metais/sync_project_events_job.rb | 248 ++++++++++++ app/jobs/metais/sync_project_job.rb | 30 ++ app/jobs/metais/sync_project_suppliers_job.rb | 49 +++ app/jobs/set_metais_codes_for_projects_job.rb | 93 +++++ app/models/combined_project.rb | 15 + app/models/datahub/metais/codelist_program.rb | 3 + .../datahub/metais/codelist_project_phase.rb | 3 + .../datahub/metais/codelist_project_state.rb | 3 + app/models/datahub/metais/project.rb | 7 + app/models/datahub/metais/project_change.rb | 5 + app/models/datahub/metais/project_document.rb | 7 + .../metais/project_document_version.rb | 5 + app/models/datahub/metais/project_version.rb | 5 + app/models/datahub_record.rb | 4 + app/models/metais/origin_type.rb | 12 + app/models/metais/project.rb | 34 ++ app/models/metais/project_document.rb | 16 + app/models/metais/project_event.rb | 18 + app/models/metais/project_event_type.rb | 5 + app/models/metais/project_link.rb | 16 + app/models/metais/project_origin.rb | 51 +++ app/models/metais/project_supplier.rb | 17 + app/models/metais/supplier_type.rb | 12 + app/models/metais/value_with_origin.rb | 12 + app/models/project.rb | 1 + .../metais/project_origins/edit.html.erb | 205 ++++++++++ .../admin/metais/projects/index.html.erb | 110 +++++ app/views/admin/metais/projects/show.html.erb | 63 +++ app/views/components/_footer.html.erb | 92 +++++ app/views/components/_navbar.html.erb | 45 +++ app/views/layouts/application.html.erb | 109 +---- app/views/metais/projects/index.html.erb | 380 ++++++++++++++++++ app/views/metais/projects/show.html.erb | 244 +++++++++++ app/views/phase_revision/show.html.erb | 11 + app/views/projects/index.html.erb | 155 +++++-- app/views/static/index.html.erb | 37 +- config/clock.rb | 1 - config/database.yml | 18 + config/routes.rb | 20 +- db/migrate/20240715175807_create_metais.rb | 14 + ...715181117_create_metais_project_origins.rb | 34 ++ ...40715182749_create_metais_project_items.rb | 50 +++ ...me_and_uuid_to_metais_project_documents.rb | 6 + ...40716120919_add_uuid_to_metais_projects.rb | 5 + ...oved_finances_to_metais_project_origins.rb | 7 + ...is_created_at_to_metais_project_origins.rb | 5 + ...54_add_date_to_metais_project_suppliers.rb | 5 + ...40731124741_add_metais_code_to_projects.rb | 5 + ...80110_create_metais_project_event_types.rb | 9 + ...event_type_ref_to_metais_project_events.rb | 5 + ...20240820193320_create_combined_projects.rb | 10 + db/schema.rb | 14 +- db/seeds.rb | 10 + 77 files changed, 2917 insertions(+), 213 deletions(-) create mode 100644 app/assets/images/acf.png create mode 100644 app/assets/images/icons/ai_logo.png create mode 100644 app/assets/images/icons/metais_logo.png create mode 100644 app/assets/images/icons/sd_logo.png create mode 100644 app/assets/images/pontis_white.svg create mode 100644 app/assets/images/skdigital_biela.svg create mode 100644 app/assets/images/us_embassy.png create mode 100644 app/assets/stylesheets/footer.scss create mode 100644 app/assets/stylesheets/navbar.scss create mode 100644 app/assets/stylesheets/schedule.scss create mode 100644 app/assets/stylesheets/statuses.scss create mode 100644 app/controllers/admin/metais/project_origins_controller.rb create mode 100644 app/controllers/admin/metais/projects_controller.rb create mode 100644 app/controllers/metais/projects_controller.rb create mode 100644 app/helpers/metais/projects_helper.rb create mode 100644 app/jobs/link_metais_projects_and_evaluations_job.rb create mode 100644 app/jobs/metais/daily_sync_projects_job.rb create mode 100644 app/jobs/metais/initial_sync_projects_job.rb create mode 100644 app/jobs/metais/sync_project_documents_job.rb create mode 100644 app/jobs/metais/sync_project_events_job.rb create mode 100644 app/jobs/metais/sync_project_job.rb create mode 100644 app/jobs/metais/sync_project_suppliers_job.rb create mode 100644 app/jobs/set_metais_codes_for_projects_job.rb create mode 100644 app/models/combined_project.rb create mode 100644 app/models/datahub/metais/codelist_program.rb create mode 100644 app/models/datahub/metais/codelist_project_phase.rb create mode 100644 app/models/datahub/metais/codelist_project_state.rb create mode 100644 app/models/datahub/metais/project.rb create mode 100644 app/models/datahub/metais/project_change.rb create mode 100644 app/models/datahub/metais/project_document.rb create mode 100644 app/models/datahub/metais/project_document_version.rb create mode 100644 app/models/datahub/metais/project_version.rb create mode 100644 app/models/datahub_record.rb create mode 100644 app/models/metais/origin_type.rb create mode 100644 app/models/metais/project.rb create mode 100644 app/models/metais/project_document.rb create mode 100644 app/models/metais/project_event.rb create mode 100644 app/models/metais/project_event_type.rb create mode 100644 app/models/metais/project_link.rb create mode 100644 app/models/metais/project_origin.rb create mode 100644 app/models/metais/project_supplier.rb create mode 100644 app/models/metais/supplier_type.rb create mode 100644 app/models/metais/value_with_origin.rb create mode 100644 app/views/admin/metais/project_origins/edit.html.erb create mode 100644 app/views/admin/metais/projects/index.html.erb create mode 100644 app/views/admin/metais/projects/show.html.erb create mode 100644 app/views/components/_footer.html.erb create mode 100644 app/views/components/_navbar.html.erb create mode 100644 app/views/metais/projects/index.html.erb create mode 100644 app/views/metais/projects/show.html.erb create mode 100644 db/migrate/20240715175807_create_metais.rb create mode 100644 db/migrate/20240715181117_create_metais_project_origins.rb create mode 100644 db/migrate/20240715182749_create_metais_project_items.rb create mode 100644 db/migrate/20240716083355_add_filename_and_uuid_to_metais_project_documents.rb create mode 100644 db/migrate/20240716120919_add_uuid_to_metais_projects.rb create mode 100644 db/migrate/20240724071253_add_phase_approved_finances_to_metais_project_origins.rb create mode 100644 db/migrate/20240724072736_add_metais_created_at_to_metais_project_origins.rb create mode 100644 db/migrate/20240730180654_add_date_to_metais_project_suppliers.rb create mode 100644 db/migrate/20240731124741_add_metais_code_to_projects.rb create mode 100644 db/migrate/20240820080110_create_metais_project_event_types.rb create mode 100644 db/migrate/20240820080303_add_event_type_ref_to_metais_project_events.rb create mode 100644 db/migrate/20240820193320_create_combined_projects.rb diff --git a/app/assets/images/acf.png b/app/assets/images/acf.png new file mode 100644 index 0000000000000000000000000000000000000000..a9e0602e64099f67672794604d0510ec71790f14 GIT binary patch literal 164017 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�WGRy+2$Il%f`K~BWy^rfjHWMdXDff7=-vh235=-BhR04G+n zGMHB|r9J4T2i1tVaItDdOn5|B2c`GMm0aVg__HzfRADJSuW`^nLgrzoL*%CGZB@=o zjSAl>oM?PCAKqheq>{xA%XrOR!7o=F_NIEYUv^D#-9cGYBH2ix$we-`df8)+e=v59 zcw36fuc%qCayPyc5-6d>4L*u05~L@kW5BI3a1AL7!E^dwV@C$QDDmN59Xx-BusW#h zfe-F-+8mDug2xZd*HS<;7PrCw5_YIDr8b|O$TF_3r*uqXc%8 z%2Z^_hi3q0Hf_Xf^zU5&49ur9zTNe0>SeSTXNh+-z}F4D#5xAev>2uCGK`h79Y_^s zs6B@r8j5*)MBK6Aa$Qh^b_Q*+QDKPb1T@*RNwm}DjUKrGJM4=xtAxwb_!1&^l~6Pf zQ7^6|Pi8GWVSZKNCzvjK_q@_c4iFv=v3k*PKM_W9Ig-X5USt(>REe2vxKkZ^b@5Pu z^=_&A+T~{r+*QI(=lG`fFRlpO$tNCCo*H^|TbJ;4m|lT$)S7HKzTX^?y-U)kuR3ZM z>v^tx4w|>A7Nqa04V`dXuUz91Jyf1st)58yp=~$irO=SG7QXzhGfvdmjx)ztZ_Od# zin4<^&H5DH5A#EN zDq#I7);D5zfz*EzG3QjUnyNNYaQk(vqo&np^v%L4l#);sOOZpfQ;@q(zw%PkUl&u-D1=xSvm*3gEFe{wbJ%_ZD`HY*`p^fQ^&LO`WE2Gdnsm` z@_R3S2r=DmrW8y*8|P9EJ&`mUAvmqskd*sYO)$PCW{Fa3*W)Azhja=nJEg3;t8KE5o1w>=6+I2czi5_xt zJ(ZLdVI?wC3Ap-S>Lv3EZk>4K*=Oj*l~i^MA{HRPX5;d-rgbetY@1V_nz5-_9x6Kz zq!S~V?J~AlHg~T_fV#pUEqJx?Me0P*TpC^3n9UP4ntJ;l1UpG2+&W$F2~#-?Hbbqc zVe&=P&)-2NeOxf4;rY|^Zl|l4eYCyedv`cnx(r!O_TyXN_uxm;Zd4~!dVi{wvmtZ>Dw#j4Ikz^!n<>j}2G(Y{>I;m{(D zq{pf}9Qy1IFi`c7s#kcW^679@{w6s*W7t}~~48??D&;{JSVuO=nJd%%fAy@2G3%{4n}{O_ zEpb6R%bkD;-r(IP#%4TFRL@1$pyQw>83ku5)JR#-jAdD7YP}@*ETe5==`L(}+MwqQ zKf|`6i_2(6ylGnJB&G8ms6lU+d{^$RH^eaJ&@`I1p!wCd2tfv!Vz+zUaDK@lj2&Ks zdW}f9uiH_U!2JDh_jd$2l7*ImQ2ia_5_0ZX-=x}1`8vH&a4M0GV{GQF#h2I8`Y7wQ zUAeGb6CC3ceIy|**KB-NUr}>Mb@t%pSjHzF_U_A{DUi-H)4*xeea$$$W|MsfQ7J88 z@3{PCxni0HI;PiNr1loZKJqC!Bc{VoQb3QsPd<6(;f!vLA@%B{V;+9}cLZ&o`Upd- z65b@`N!j0+Mu3F$!l~Nc%_;&nh0btS8hyIcr0{4Jk2|1HCDD5n`oT9{z-fAgrSfv0 zh<_A6@8+H zf|9-hja>D!4`XcZE~Gow6-+LqZ_k_7lfJE=@R?s%aEd6au1eHZWC)sgCtICrb1ZW; zA1cWc=`yui0|^!$hAu4!HCTWDWr?v8A%SI3_vvzGai9DrK-P}S<7YXYK|9A>;9@t_ zm{=g7_o>Rd<^5eoMdRn=8pGG$#83ml!m5DQzuXCZ7s>I~|xI+t?otW-5WJ2;7 z#oyX1e7w42O3(NTt@2ZMQ932s>UWj#e4GhxGN}~K_{Ywoxac2@Yg89dWU{l7ov?Vz zwih>_1+~jxf@@`551@-wVZC|@+nrIt?BgwLJ=r$hd<|1s#VObEi{}Dw7Hr;OkzLy6 zhivX3-fGtlN$Jb7mD{q_0LcM_L8?vPKaeAVrZCyA7PJ#QUbA#j@(r-Y`r?xNeI z{Z$GxMegAb*zw%oGs{OW$_T6jBNS~^qE8(-6#cFi@7%t>kg>S~rH@=K5?i-cq_+|` zFMbrCtiBLlGw}BM^GYcOY^aL*SlEi&>$jcZi5v3Um5*nKozA+kXeMs3VV5PA;SYh` zLD^p2X$kAVRl%x>&>(TI0Z(F_V=>P{Chn!8H{}II-00tOp2^3*zFOGTE&N{lg1%qHm3+a=i#!R0s5wFd?HrH7Acof;#px9HPq#dr0rUY)t z`ktY8b|Hla7nNHE&y(|V!hC}Ha!Oi@Ltu$wE-swr3M9eR*zZ-tS(6rJUZ-RFyFR@; z0rlD%QDIIwsekK=??=a;rY#Ah9NwAN*(Gw#CenR6@>^k3s>|6yJ68L~Wtn0>{m@tI z;2bT_G3!tiOOAx@1!b^sefUZIeMQwnb>pI}w^j_rOs>$NM_+@8k_5RmipFZFjC)^K z7s5N%gIVJmNL80)CSZvwIaljxgtV?4##Hp->EX+Vt%jQiF=g1hSG$+EB=_P|c?k8Y zw!v3qBG$jl_AWTg#uoRfRdL@m0XrVSJtH*-Gr>97g zyYOpc7R^552Eh={q>1uWde5|+C~laqkwb5CuQC-Vv=r;#lsi^YDA-n+B(Nkn713-2 z!!L`^*74$&^5VVf-67q#o2@B<46omr?j4VgxZl*PL=5D zPQrwx)o|W5$u6_WGXn*z17~h1lkX7Ie`)6oO;x)zl6X)eIt*)hAh6|&a;BE!n?>cX z`{q-N<8QFs(veUwf7i4tDxL25a3X%+WsU3tqkFP1%A!TSgx`$fg|zh;J`GI1D6-x2T_)1(0!(#G+^48&vqgnFKb@`vFAp(~~nZYW1$m z)=r3QqID1jlBV=|&Ro3rsc~q%h;HhVM@T$fz@0Gw2nXYa0H$}gw!QWI%l?NDWEhn2 zZ~4dSn#<79jHpNA^1njld2}riA(7c0ZQ~NVKxsS)2s(Eb;{ZQIqj3sQbn_0}a@>vn zLS8WT%-0Y|9zjDV_8kz~dAFKW$dwyY1xZ9EmTr}!S3^?E8=O~bDNmCXIR>(VGL5G~ zzQ@uEJ_q9D3_2a_T2S-zj^KS;o32)q!W;=5NgImPCSI5O8_)8qa1F9LEtKN(8hg(scX}?~TXO8VIj^Vw&b-=XWu(QP z$-9*%`&$WAWZN%8bu%Ta^21=L;7^e>3KP2 zlZ87}OH4E}TRc#{K1Rjys!ZHSzJWssc62dc8Br2iby){sNOP_%}3*p2X9OoKOv72UO>gMET=SehR4cA^D zQSH{$eP`l(EKYUuLw{m#S667oLJ;w}nt`+!V#~5KK|&*%!JL(O7c1#S$4r-|CZ|px zbH66?4i_Wxgms@K`E zV;3ZOMwGvnKNECf?h1OUzAMYorjB{aXZ{7pYYB7e$@JaY>?QbHrth?5K~t7!B|sGx zniT_=L|rCjv?x5nDRRal(Dh-Lp}z{|f@V}td zK^+CBG7N+szNXvHJ8K;R&bjk!*@W%;ry)Zj^RyBk*9iw4o#`8LSMJKr6V7rfEzTaD zxGj&1mZ9YQx%-5!??@nJbX#nP@1v&=$RRCg-bNz%AQnwMEgoi$W>yRzM*kiGeA_t% zPJ|`B|LTMRh|TCI1ppJ(QKhiX-sF0&0QAJBB_kz9t2$o!01=Uzmg&h8SdPWe+M^S4 z@@{CprxsSFt{Hx|oL8QuUZ=#gG4N#5eF1@&oNF2H*xG6EB<1>Zd}?IXJJz z9O;PT&TX9uI7X%6KGAnlgZS|GG}iB;1OzyOGd97;wXtShq)!;1I?CDlIX5>~|L&9n z$)?oppfYa`tz%@Y9y8vb%NyLUN%}T+O|k|Z6`0PydB4;MLZ$2sTXxQVFSffIjCc;t zX%kloP3(IQ#Mm25~WZ0+*r02D@Fm1LnuY1(oJ5`%Zks_|1#kGij`t@QjxU9ZO1rD@u=RKFiM zVXX=l(z%s4C-#GCDIU52Q#_eY<$`BF&J3dC>w#OS$l-NDJy%99pV>}JpydJY# z&zt)AiGokb$KAgF8YUUm zy{p~YzOKDg%43P5glm^Z3!UvW8Z?7kAGs_P;)Y0TbwOrwdk!uBYc0vJZjss34zTaX zJBr4@(xe=S57%_vll9aIz#jv33ejtRl-S`Tvz7EY#zn;^6u${lxeb z!4X^A-2zYZnJs#caSdEb?XkNR+4QHvrop+RSZTSzA1ZZ9-|k$%VO+Sl?*$wt;N){7 zM{~>W(L`)a%GV>zur7E|x%G^#?X>YG`n2n~*_T%hFaj~(Mz(bEmyI^qHW)vgV-#ut zBQ2Vgvh6N6eg~I@9Y?u&m^YdH_H}>*PjRh4X8%fGw&u!5=V1iIZT>B)|G~o;oFG;4 zkkrQG4e=k>XIMCf@kJ9U_M#|8w~N(YL#h1~VF#hK8) zMUgFXabKAmW0&2pHD$atcZ`Xx{DIi4^WZe#3HdAEhx;|;O5g6B1$5Y0EWQX4y|!ZN z;5mg{@kwq#TT3{Ns}!erOYM5~vOF zm#7JrUr*vY6AeVQ(qi8_9bYe2h{^0%!@tEK0e<@NOtMgFfS-xt*M1Ja#{#h@O8R=H zwr&!RZ1#x~@>JXU^4<>*a&Gy;ba=IhHCS=6FSOM3)@b@?lf@ z$5zi7gw&O&r$}DfUv6%|0@_wo4K&-T6`aZL4u4hQ!!0K)>)HDqD1?cdbSDJbuf_Dm zPe*1RnlTr}=aKMj{FOxa?L4J((SZg7!Z{L>oTgt*mpJYRlclDP1S$GDnIp4 zrXmmoK3Qmi$QRu2zej`kd;9DroaJq=SrB&cbJtDfMfj;_tW=X+~XTkKG@74dR`_q@p*jJnrKFNE~8q*4p#dOC~VjhbhHaMHf*WJB)^b6EijOt#IB-;c> zk7N;)CjxlaH{M2%1l)_RQ6p!R)6X7iJdv%=j&_6TS;MMfZ-Ke7(5>;*Xxi`jGm1Bo zh`E__tIlkf`ksIGR)LxE)6R-@-`d-Kbg5 zo_ZB64c8p;zTrRw7gTSXerSh=u^=aO8)iIc+pcmC@sAOhL7U_C9*Um1Gx@HRrnA59WU zm8{g%{3m$DFu6t%S>o%)aX1C%N3BPWBX$76Oo1pmyr|ogdPn?_sIUM{QgRo?a0$^z zNO0A%LvC(7y-})sLBaWE-&66%5akt@!d#e3@9l!>bB~?t9cB%lhzfgkXp7sv#~;fV z%AV0%-Lhz2tY6Ak_TT{5ZFDr6 zK0kq*j8LVyIhiRszA(59iZb!n!kWRy{u z23~kJ-*C+i6e*6ABlOXX7#=ZrXyHb_u8gKaAAo1wI~o5^q!EBVSMXpcx;pOtezlK? z^WgH3HQPIBB~E#3Nj*Kh?!rm){3;orAuSxf#{~pB`-fqHB6TBmb7o@JccKdQGoMWb zRoAUQdr{fJNLM*!#ntA@=;NJ}&0ry)4w;F4`U0~&Gd12|*FtDffvgE#`X!V@)yNpX zK?M!~o4w8aSkyVtOyB*Y)^#b%yA-5MKxRTMD6uXS98IlKQ zDPRbZ>jcZv`XaUEuc7p|{A!#}<}}fb=w*t)%4&-^y*?9QhAOP(tY78Jdtu(0=lE@F zi`O9KJoBrSPoL|H3Ui*myL8^)b=Bz%m8pp&MP!#^_Jh%oq0Kwktd@@Yub*7{9!)1u zJZl(98kt{}bG69C8?OJ@G`GHB;)nn3ySyor8?U^?FT;r0-wL5Cc@ zC#wUIb6bK{wyY)4;|+scXN3h9pO?x(R=bPs$J?>iY3TfAhYSy6KZ(q-{C-oFi3mjr zt0-CL(?YTRf(xPC9k(MWKwjLJ-5JrNj$N$i=aA9dMdL$nN8w=*haHjs%`p2eHA3Wl z$NM#p@H0&x!dCUBC+9XyE2iU3N`_WSGBy=`*~D&tlFaeFWBrNpbokJV@=s7ZCVsCr zZMCGj!7`pY+s)fW!h8|Vp$@{NN=_8G=25{Yaq}4Pqh*5 z%r=d1J-Nk!+hNwhzTWRRo^r|Em9io*yDUi+C-`~4)ag9Q?_rQrnzRFH`m$QN{oIQK zx5xuA5PRU9DY-4c2F=m0X}4ZT)z0N@UNN$4ODR}7fK%6T=ngqhp>pO}+Uu|I`mY`% z;3vu)1p1|yuvjJFN@T_oADDX`L#(>N4W%K|yh+(hNiRhk)c}(mE}zG6EVFu4u%0~T z%XTuiS`|LMr9H2n@o@p$TfKu-q&2g=M zrvIIbbPA`x8e;P=Tlb$pKj8x;HmsRmwRq5oK8W+@zF98PK1Qlu(S3WY@BE8UM{So; zT7D0E>QA$vy2}_Y$NYHJ3P{L?!Lm#m+uo;sBb}GM)jX0?UP2Z$!=ch>JI6eXhC&V2 zx1Y(GE-eCFHKxw_o$XO|GpmwtN zCKvO8#I#|F>2s5o?v~K`IldVwr^SpS-RS^xEqCiHtCkVcek7i8kD4haUkhqJ!{dBQ z9T3MvDc;G}zayyzj;~Jom?zO1Be>Ugyc{y&Axck((a$sWK)``|ge?9njX4 zRBZolL;Xq^C&5QgAc$#E<;*PdS_f;Iqn;26P-bc?&wG}jD^t{ZJ;R>0FVW|H>PREz z^HYvwTOOZMYD+k-wXutKN+W+I`hXp`SQ47TLri)_^=6h>BAL{*guC#G=+hoo9nduU zRv&w_zjqDDJRrj&dVBVFTA!40p!L)h9O)G;i&FF=CG+_xy@w)trOVL>0fz#VKtjuzJCi{YKer4;_UZ)<$MZSL#nlFW(l5G>meQ+xIX zm@2?c)z=TNy6T5*{T!IeIn{bT9b48WNti} z%wX#fHW7ETkJz2tA7Dd=;}sYuovP9sn~UDrwqG)5H?}O)4c*OeCKbXIJj35S8BARJ z*Ob0bm`-nKm5b0P50ez{vcIJzHO|~5m*V=78GQu0_*eLtQ?XL_?G~U?kLCB3dRhku zx%c>kv~BWU!#gX2WL(!(G_5zU5v_=3mq@S6OoWp(=j~4|@>2=0v+i<}3z!EnYfqN- z85x|PbmlOoAb6@?(*_1=*Dz=Bs;#n!$K;yn~3?Sz#tDVP9u9=A)Qx^?V)d;d7jEq z+p@CRnNYk?ShDXkz}GQ0kj3j~d;JMD6%qHz;nBtTsd%;)9iY-(7+QO>r`Fq4(u8@A zQ~0DAbC%OB_HXQC&{`mRIFGn*bNzSl)inX495z3_`+L>X57Z?(8^jm=I+DJW3tvJ)7Vkio%fubV!a-^ zjHszNxQcHwDs4H}YM! zf~}SHwpA%JVNs1=6sTzRpfnzVHlOxxki8aa*_`j4X-Ezhz_i6z@_;F+@(SxKqq&V! zZrYLAZ$k{KcXJvLa7dIh<#fT5jJAiww%3o%==(|1z$wINQbOBykuMu5zL$QAasdan z9Q!bSoJM0h{ghY(|BzGT{~Pu)cECmTI?5Gz-In_XWQ2pq%f_PyJ1U9mGJ8or$N!?uJj5D2XhR;~5Aa28GY%s5eSdcTMbKPb> zHzppF@5_D4EMJ9+JAUFjnP8U-)TbFpoX7F@{FXFtIUcagGp^3LKx*FJGi0P_d~}_~ zx%9fK?Wg+ow#*|wuh!8QdP}ORtc; zdSV=8B&fODhw}k)phdlQ%Bfu)0ps>EkXRoUi>)sCf-M_LJ}7kubUz6K_y8Q9nGh?1 z-y$IouqXPDB?7N^Pzr;xG>Y!8Umg0Q{_LKkGqBWYabR25(o3kni{)R1x`u9d>aY?F zV;bQ6U>N1gcDbOv@?uco(k?M6EWJmpQhhzg7$wQ;8HQ^Vy- zPuh}3BC&?~N@#LlYeb~W_clIVCVdCYai=ODi~U)TMl_GX>&+fQk*@7p zk^$+b^_52mrT+Wt?eLoZ`<851fK#bFiokufaxsy4`g0T=EPTllO?WPr97Fb1qNWx^ z=*=(oVXoCyZ{6|buWZWHc3?t9tSQyM*=@Rx0RccVa& z!&8Hcq)S^>%o3V6)8*d@JQ*J8O%$?GuDto7k*1x-;`1v{Ue6kbnZZW|1w-kuh=Nd# zD5pz}Pl~3itRMJxvQ$nx0QU{#`8Cn&W&{b$Ju)EaSDaICiabOsA3ri%s{MWWZ$ft@ z`}9}4bocHzv~~YBgKqW-ptK~iR{~jDP44YN-Xp0bK|owLF+l7k$!ceE!}!%^l=Ak& z`Tp&Z=T;ENx^4LyW_Q6YuenWD&e|nfiD>kUOSLv+yB3bp;mjSjhGOK&!{nVrFSC&0 zI=d^%6F^DN&&|G*&hHh%FRF$b*zScl3>U{9C0xq!NpITBAQ?2S9W&&1glT+pF`)E+ zO%bHoTVw(V!Sv1#e;eWZN~p;EoZD|?&q%odkHSi!V9yGeS#3&MWZFkuY?&Z!;$lwa z$pbyDZ=?CZ+56$CH$8F(2UwR9%+!tvo4aXW>xeeD5YJ~@-#7h)YP5Q=#~qRT^a_tW zbUI%~Yw1x~C)C6l8}sTse}eLu3gxb1yAcI<(|qdKH)Y`p`q=opo+@L-uDFeRD;A?*?%GMd}w;UCd}3f_rpEJ=9o#+RHre&IauOYD6sM zj74%r>fy572ttYbaDFp8q{F??R7^_HL(6d4^#@}ngyP12`WsN_2B9Xkz(4alInsX( zWAbL=`mHd2Z}Tw7$AZ61EEv#mQ}Dsr9d@3Dwy4S}$_EF2%WLml-( zaQ?F0sw$-rgCJS_8McbZr^S5teETl`J=q(3rS@VcP+he0D9w8PG>`lB%6D?)*?8dYtmAY5>2CuX-eZ?&C#o?TVF%ZN%4l-#!(YavY( z*F0Yt*V%LT0?i00v$c)l+i2Gm7{@AyJu^*Y(o271MWg{<;|74&I8ci9Bq_Q>FfdAY zzHQ)kZ&;fA`s@@_g<-=oLuuS__lGaG?HqyjMLe4`wnrI=+0q*~-Y>qdu@B|W?=EDM zlL^2jS6=HG&n@duw)NQm1eZiaZ~A`m-R5Z#Y>(!s;@QV7rzy(WR<{IdFY0bop5MO$ z>~+()^>WIpm2^0`n6CV6^@S@l=PRjd1uKVCuRmA5_7$P^+QhA!`SxL&qfHFR0r^DD z-_o8n0p!aM8`Zcx%35xBkLhsisTYQduO;fWd2VzEKty4ZbkmDhkQ{vYLig$m1XR)E zH22}bmCz(-ShQbHkXWFOy}9A}@E|L~^R;N-_E2u%dQ+!O*{<>Y3^()@((-PgGb*|G z<%%GEc4b_1o%+fLGF;p}-{GMlcGRm?7aZ#2{66PWVy^{)W}rkGj@hi<2PLK7U6e5VSWX#|y^Jy24OnVtulRBBQO^5+VGZmgo{d z<8T4EKN)>D|GRcBr*r{>MdKg$?7ZchdAZvqcpdGr$i@UcpHalKSQ~GA;Kqz+ZtOCz zkbPn-Z}d5rDv^M+AcGc%b`x(Z@OD{OvvN0epM1tCz(ZV zzRSJdsfsp#zNp~Df=V%21%sPlgjrRhwvATneq<3+Z-79k$r-?DF(6?;u583LAld=9 z&R-qHSLT&PGSyGJtiQ_>9ei~;ZZ#p7>C2wZyl0KQ(U}4TM5M=emxG4`x;nxRETt`O ztl{~ybfMot&r|_8F7`d`yc&S9N^XdK1LO1!5#3XxOt$_!JvDGs%=O)l;*|UZP?Wk|ci*6Uae!!MzXK1ZVl8eEVVLwQ zYQQTSsgDDty*@2hxf6^-zsRqu&QL7~k>N7ty5gcl*Be&ORN5g2b_#cQ6LRpV9C4u; z9W{+~Fp<*jSq9FsYM*>#YfP{8eW13tsKTR{X*d{|FeeS!_gjr} zMopZ{w!W7B+SgdpR~>`*>oG-w0ysp3is1~*=3g8oo}DknZt4_qAvpPx$Wp1xz|+8 zM3X;>7nM_58n(&dAy5}dd!9kp2rDt5Jh>%h*MOce)+i@ho59CNLpB@0M|mmFf{rRs zo;5%Eneqe(dV?&s$dr+crYI?p@o&f>4dPqY&sBq%#28K7r&i&Ej541_oX@;7{w|j)qT%W*0b+F% zyEaQoBI50fH=1t;ng@AK^rL;!I?X=)_R8y(etmuN^Xzih&`#4AHRPnw@V`wmG@6`` zd?vVdS3JzTWN`VJUm^w z-l#M=ys`v|cu*ey;Eu_lUap_5@L=}?KNuFP$LT+QzLNvlL5B-OUckD*^hU$+WxER7 z%pIN@80l_N2ev#A!a{_|gQS~9Dg!jS_d!|Gt{69unZkLKPVH9pXLKwzb)ztijElt$ z<+$;z)Ro}yPT%5!)^ISAsNAHX%k^zHaDuDvA!!x46-3?Y`B|J2MDM4RppXAu^tWD{ z=(qu=WwJNP9**oX!LM%fqx+~&f-*Jw@=`&!%{m0eP#)I)F^i=$YzhKv-dB))<7d?& z=zC?_;Rpi~>bC6vJo4Fm@(7&6EkcwXo8DtIgSFDzSnapG+{(?hD`8ou+dhWdATEN{ zs?4KxoMZRA3oj9SyxDN+ah<@VN5QQJ_BwD|OB^__5j1`5Qr1RHXWYZ;@?!<(amtuo z`2jz_O!3j{T^Z74>pC%30m-jHT|##iB*6&xB>OXzbQjhBSgIE0w=kcjeSXgf?sMIAyvu{By5{po#*p)l)F)QWMPKdJ zUb>@6>K|_*I+({l#+|@#Js0j3>BGf`Mz`0=zZh7WvBxF^r(Kgz@61DY$$XF~jiX9i zRDXeR*C0(yC^6cZa?%vlj=H6g=Hg=P(fzzN>D9!rJ3e(kN?K24(su(T_FBq2Yy(%9 zmf~Zh^E$&<}ziM2Vs5itDMM(JDIeaA&we?F$S`m-Yav zeawegkm8eIrmHasQdf2!h2b;3j%nSGm`vp&J;z+Y_qh5LCEHNyn8ql>h0ef;Xm`(T zUjOIH9&+_X-wbel2{C1}NpX19`uibdsFwb3wphk@arJu+fqr$a)QYRS)p2!U9l=r= z#S_)-QMu*;-FmJope4xhHP)ly9jQ3+>C3ZcxTuasfG6to#rL;E@bcGJCs&)IxV>cw z1XRoh0+XUB=H~+G?ni^e{hw>!n(%{X=A?`6=Cjnb;LZe0u1-&n^Z8TA8^nN%mYqja zc@eme_EbSs*vyi&C`4zq7j1V^vTpn2s+Ue&F6K3Oto(IW7GK_*eXx!7Obi&-+-cWV z*T<1YmvO2%)iacYW{jL=cQuec14A5klxQYk5-WNV1J@w>o9P;-=}*=3~e5#goq%GY-v_<%4`aRY_bH z4*?nt|1&`L?}!yNh=gWI7e@Y6NGbeE!YNV-$IMG5{c3MdP9&!lY9H@QrkJmNM1`Fe zPZW@p{!+Um=(F^Z1{c-Q-rFFVdP}C!{Z+y{WO^`Pgfh;E{BL&uvFSNY?bY3Q2O5PH*v z%P-*F4qQc!IsTM_Z~_;>Jx?lXF=9FzC!+1? z{q;%D)F^UTX3v2Exd@PO0^7~kNRK3x){nCwp#6doXS2!rt$^P$OSv*ScHN>6>v-!L z{)wOL132t?f6?;X6hWbub(`sV*d_iF3pTV;C4xI}2kz53d5_31e7z$n`E{FqLS@fg zy+ng{i`@o9QXINuPrbcekF<0bGPDs>wwqt6ntI1nlt|Abve|+mLu3B6ylq$4JwvPS zl~=Iyx*~hqlUbgS-_|aiC;{xBs&|=*hO%q`--#)vgz5b^LT-^;Y`EF5p`i6nn1qkJ z2JtCcFZs;(ok*KK$Xs{~layfe!lkgP-6jIa@3px-qs>j6Vl&0AzCvjO?{?*mx(3|U zC%+`Jh)z6Vc|3WnwJP3b<`zh9l3y!#(kKmUsOv@+4tWarA2~s-wZ-;vfe4c&uX{Sg=t?Xbdn>=d&(#E}4F@Hn3$)^{1Tx!!vV?HA z%7Ci-qB;Y{wRTrIZO%q#OAlBp`qXX9verRnG*OSgM2ca{;;#z_(>PGLQ{bEP`9gs^ zQ_RV5tVg=u^r+Wmm`f7gt1>3=zXlY3SRq{s9><4TDIaI%Qt^^>EJ0ytFrAfwi#7tv zU@dTbz{4Jp>nTZLzU2iwrJZwutL!q(X&1Ut9uir`ro4}4XXcNMNYoFRP}@#Y^|bmj zVfRrZGQ+IT^2Bzr;W^Jv@-wM!ieE!{VO>qWsX zRIocjYWDXQy#boSt~%);33nRtaF>1Dp28KP-)&Dk;zy6Y@X9TAjjqt&A!L9pVn((WO)8;&4M~TjynCiF7c`s8*x~vdceDHFC(_w5yyy$n-JdH@!J`L{ygkE; zJHtw_p)Y9C%+TOam|wn%RBPQEy5Cy2Ly?=JURH((7nih-WlqdLerAN6Mt(F36s5LJ zTy_2iwvsj&t;4nl6PI#}0L$Vzf~$ixI{M0$=py6BMhtzTXvCatS+52f9_SrjvrhAk;-cx z&!Xsb5Ra4jX3@A2YWS#P(xp*qY5uZ~j&jC8lS|tVct1JR#)MYdQQiOvuX}|y?<>d4 zfa=EVhpN`Nd}FLE>E>>j_&*nY5~(Cb1k9&MNt6b0q zZqMLqg#P7ozgn0JWS+D_7FVB2+&2;wWkS!gnPH5L*!39Yu=cpf zVp}o13aji~Jp|_N((7&|kecY`3llb8qETq08LM5QK~^CECZ+bd_wU=Q+oYaiM>Fh) zMth4ugjKY{fc3>{s+hqoLdcp5Uuh8?YHujB2#mDY$!iuZpd|DZw^ifHJQH?kVlz%z zaS%E;^-KzAWzu-fz_WQW1-^>2WBb zY|;}fnPzuXY*{b-clJV@m8U-ltrBT(aWmKT?G69G2j|a@mgrNjITg;%It{o^bbISF z?!7RnT}ZGvd(TElhWd`Cs92P{hUmVLxRJceD=kyr$Fm%0Z!JCQC{W3%hjBWTL&klH zDT*@zIW4!y)7q#R*B2llG{qTcK5=VR1oUKv-jYg~b>6Ydr+XH&Z+-gDUY7sepE`91 zIQ2m_du*fUDnt!@QZ_i2P<_dJ<`V|K>0n){wQvzs_QOi9oL$s5$6VlWyr^&jA_UeuE?R*ktm@pqkbKvWTPQI$_x-o_$L-_+1R->vc5jB9G&&+}kSa z`?8-u-q9sJPnS_V%h4Hpv*uGz@7Rk5$C!?6oP{naWL9@HVkZcnIvTWUcZd3p>yW#A zwaqF@4))Tabi{CecIRSLXB3f5x#d~4K`eH|A#!tQcr?OYpY(PlXvl~9V;8-~b9B=C zqQpnGM*GpLMtR``=Z9ztZfHJ9t7xP+qEL50P@*(Q;G8k_4OdSW7oJY(rXom{?Nv3K z-+8hBuyB*WbRIrkkL^g6W$ja&tJi-SMV?k3?CBM5gEWa@tt)aGa)P@ub>eCo8I#%R zv;vzd_7-i#(vG>#oi*AMq}E;gA{Dp*;Yx_Z?YImodG-zd#l%=RYH(iZYBdq0Ovw5ALSb$k)Bfc{y?J8}B>!_DHNY)4P+m$vGxUs{b)Tw!D3B#mKTcBgIw9k1fd}WO_wS)cTdJ?> z!+y#R+Co-AE{JD2@~!rCjU{9t^>lfrM34C+Hj0OqJ^{uLdGhnZXSw8Kq$u^8PtodMXtn>}N`EP({xRtkyrV#uHn@;H{425k|CKl}ZO};|EhaM8^?#?Qs#Z#q zPS7$ppdw!21>hJArnrDx{6FXU?b*=lz~H<>Kb`u`;4_*GmsaM;F18=*C|XheB(C%%^z;T$E0z#Y4C zrR6tv#a~JoxMqOvkvx}NwgSkkK%`Q18{DkF&g<#n1oF{F)~|{F6QBRS$KhvFB^|aH zZaFd;23#s0COePGJ5jAuixt(h=7EJh+8t;?h0x9X*aj#a`$iC z`Q`Npv}uaT-@dwlz9-={{iSBOG68xc+g0>yd-3Cu(uqXJ zj|*8trpL@ixp8*#DFo&;${MEc4t6yJVpWEB>-Gw?Ze2F_nFDg%6P^R)B z6dg@vO964KZHrBX8IfK-bc0?f+4l+xi0?XQF!6?hRd>QFeF zM9J!(a(=n`773>=knng;_Y{rSZhM^Z;dc)|EC4d@Qud)dB>3;YljNf@zPGpj*&H!t zus@C0?!uenewe*koqEX{6TixzD%G!PN>nH4st}5Knc=y*xbe&nXnCMD{wNa~_^_%X$67e5J+)B=1tet6x_-2ury;(qq@iQhGH_pknpO$@ik&soR)Wg*=@ zkd8Rzx)+5GB!>YzD#OE_4=g-3&>g-(r*x{Yd#aCqe*GI(q{)a#$x9!zwM>&ZIFRRW zY=R~E-#Bye8w7-#%jf8-~aM+!U8?6U$Bge_IKGT!oLnb$rrlwVbjLfDRWOcE^yh6 zRGu!aO{bli6==a$b+&)34k%Aivi0sI$r1k{)I$^DU7K_F+E3eXhmr20Re}Z6g>SD< z(gI$n?aBNzpEyYkdMZhz?MQ6-YZlSR4bG#^f{%Hg-G5w92emJ-@)%tW{g;v_9R*NN zBOOls15ouyqRKBbsFN@&zT;p2mT3%V%r@J3#vk7}0xcjyqn_8+Mt6n9mn#+xU&uDQ zOP4H7JsWLSL;K+K#eex5UDbfv)aXEu{Fq_b0U-Fa@{$gw%Hs^otC|T-1sU< z)8oMN>6STu)?_A`LuuACMz5BqK+@-A1jPiMxW-Y>_hjN-qQg$T6}6``1X^b;FomqUmT3_EG&qL}`sl#t zkoEjo;L{aS?<6yK|9e-ef14g%1z-lss92|+n>7dcv)F0WyiN3x@{_47>hg1=XK(+L z7J-e5$1!vDuk-ZZXJ9%@gio(e3IEdAg}CyK{DH>}kW*Iq5%l42FTIow8bL_MM9Bn{ zV};LpXR`ex#qsU!ZDFCm>Ejx|`?K{&h!>E@RpoAfXKp%iHX1IC zPwP?yvz&1{&Ny>3!_&@p1_X4n&6GAR99d|Y)_ zhpJF%2kmyY6EY{hXz*^?5tkda`Y9*}G`R|-OyJcL@P3s4qp!>Pv-$pHs1g0Rs z$tLmRKcHFwWGb@1eCyA6DIa)&BSv}ZaE=$weX37J23&L-GOK;{*?#C5r|e^C9FgAj z;L!P;X^qIMhtvJ>p*|b1jX5;1!wK@UIYJ;3cKo*jzK^@+1h9>M;QG#`cz{(qG7izP zIz4Y_Ku5Go95R5?sPLpF>oOWKn%i1W{IC7%QVj5lYdNZoKm4#BrgwF9OQ@5!*=GyC zDn68xWC2b0&qlAmDG>jz zWmdqv;?~1ye*EGHH2vFQ+otxR=m-cYCM^8OB(kPB#SF_6KXoNz%QtcO$TN)On?e3Q=%sWN)yG z?@NP6t&cG>B4Q3-_5B0jKS5<6nTQ`%(>%`5;WaVEb2)SG zc$^&?zeOnWPWES%-(!F9b7@|mn4-GfNh6mTb7$|-hW(P6K$gG1KbZ7)8JM>37gc0iau-uQbQU$D8Vt-}L2~9e@z1&0IicXpPAqt~XFcFSQRBQEK3}4cJaLp{fEj zsDQbtSNN2nZtrruvMt&|-yqBQ zMjC8-8?e6@vhD944)s#1H0gGt_QkE{Qw^1HZrDsG_&E8l)0nkH-KH7uR`v{!g$hu8 zR@vtm4{B6FvKEH^+)Tc!a% z@6QlY8t_$-y11O%OALX`Kr_J&Pm@F~!L4j=QvCT@;H+^=E^p|G@(~N5oDd2lZgnyr zAl(eFvH!=G1JsIpGCQ3(4E&iA69r!Ov%YrY<<)Q2M~4+^|nxG2`2B*6{+>JC(#o$5amqv!K}|&RF=4ZLDG5yBI z>h^|Hv!BV3ZMU~ZxBRwLMDvsZy(FOa^xO)^F>Nk-qiY^@^-vE_%Bhyfx3866J!a?}!4DqePQ+i1k{;|- zlh>@pg0$r$t7;)_$P~qLs?^m-gecD_6)*#pkeT)v;Tm`AT{h4|Ez7r^QA6p@pZSS% zhJd;O1&?yMM3>@%k^)2tvh#xFNM?VQzjN|-#%oGG+&D=j1~&QRy^f_8iEbJD&{}qO zTB)h}STU1th5DFmV4DM2oeNe#Sbo5p0zPJ}vjD@ZU*DM9taydbL%XB)eA-HL?Ul$l zP;n8vIwDjOZBw)*HuiI0!R|pM;vgm>+&>$oQLB*BBIHcBzAHM17R3-2WXeTQ2G{X< zGjfI*dRz7wVvha*X%U_FLX^FeiA^MmdleoAXZSSbw#tpggUnd{16n0=JYk*1%7#k>98{GQ-W}rd0@cGEd3|L=hghEWmZnai9@HCkN5310OAA4BgL9}Rg2M(8 zBkG3UbE`^5Q9?q;qqu}~LE62qvg$q(YjW8_b~obSzMmh2l!G#TkOr$9RC$sf(VM14 z^1Bxk6Ttlql`E4ko6qVDqE_#HsY)oOSJ+4fy4u$bFN;MF(T}KT&Zk^LBNH%J=7h=N ziIp!sG^B#avNHpmNYu)%u&Rkzy;EY)>Gjo#?O9h7tw@7`F;(#f3-GZmuCV5@W$fR< zgoTK8UZi+2)=)^US#tN}Qmv%&NPptyGw!kXe}!YspLoFe`e`l-1Mq5M4#CYJ!XM6< zZ#>tOoE~%(sWZTE7n)c$D54K-8 zfu?D|haW!|vCU5EpFu6gdr#FcFWTv(9b70HasogsSeC&6^ zVr)T9rx2YZ4vmQNsFd8-&{+Y6bly9bBzPZ{d-<<$K^W#lMqm)K)JV4f9lwe7ezY4J zQX9Q+6}`Q)KJrFVjxO@3j5X_M$0qEad`Xo1vP`-s5{;;GLAu5crI%x*^TN||*}2Qw z>t3VPt@k75vR{|Hh_<>*Sd;1=C4ccS8if9Amgo_?)%>oL5VAW``MzJq{jQ^I3w^8adjO_Q+etTZHJ4s5c^t~WL(eby6UE^xNfyC;!8r^ z?<-Oh*L;<{*=+jOJOoPPZUvFq+WG>W;RGRu>v&Zg?Rn4p%`eh~!*<;pdSW2kGN--j zmxdD0RIEN7&8ctoZ4-G|AaLdD;jlu5F~POdu1Yypch4lfbFggd`>sz>$Ft8hkd?_WXg zWZ?yO#W%Yh1dY=3@E$eb=v^Z{6<>SyT2Qx=*k+>#yQ2+Q#}n_2&NGIk8~b{0Ea(mR zotoz6?c|Gy9y`S{+fKwusNO{Tg8L?O+9?Hoc#8+eh95URm#Hq@Zr*4j8@csFM2~yS z)HyP29ci)o)}LeO)7w)=Sg8h#Tp!A8Liu$DJLIh9cAtzNdyr4qNImY`1BVz5-w;>2 zTWeg-=N%9BJX$jRX=~V=Ho+nRRR6_nUyaN5jN5);nO3V-jU+)dloiITOkrBV#C34L&t zF|CT}B9Y@GrS6;mQy;#F2k*y`u}ZN7Trp{b#PIECwyM|K za0km8gy>o*LAN5yT-4xHAnQ{Jw@uRgG5e{CI_T@YxJMN%F9!6_-=_;VDXd~!59Kf} z4_89A)au#?-Wv&07m^KJpUMsw`denykAH-eG|qUw*Xg~fUBO;G2#G03F}kKmXIB|QNIgS>MDyb7v?TtR$Lc#Y26hh z*BTpA^syXolFQ0*2J;{-)8TG7?{q)T`TXIJSyt60mSH8j&xmYHqE2GWEt6{$hZ40Y zHMqZ67&s0pG>utrJAKx!-A{e%AzvLZu)2-Ud3oDTB0OSugdi;3VQX{{dioKgp{E22 zF&|t`JbNjve&Wd;YM!;Fi|-6S(e;kC-aUToZ<_o8G0=?C#}-uRFcYb7&l89Vt&Mea z%NZA2U)R4!EsPq^yAQnYcY1$f zzPRjaIOqk)3slrUb$gXuZLV&=tCDW9vJ^`{`Hl=E(!BD=wG-A$gYGceQ9qLz*TlFD$wM(kD88a-d&vA5w%t+JI<)*=egW05=YNkrL}PM z$q;bz7+HV;!eZA6uI@n*@b$pAckbu6Cr9iT5Ds3J3Ebz!w?Un-LFBG+}QY|P8%{G48G z=s7&9WhVq)d>2b@M`vEAh~KXf*3M5418y~%^OBkuy7v}3A!0VcoQ_+1)^yHrz8zc5 zgZo=Wb;cP+YXOQK;1KH*^~l}GHwvDpyP+jw{K$-+0nDh{@qlus3+;!3CgLcs0C|9BPQ%k9V7R8}zXD9xvV`0|9A37SKpYvVu}d-q6cb z2w@uBLg#Fq3f4zG;gvc;<-XjTG`fc}18(sXfK7cw4-p_eL&SC8huMUDFS>bZ7E=uq zzwvJNlvfP(sJJk)MDhkzz5}>NF{d9J<^H)Z-LZJta{uJDGbv$lNYaJfhCB}Tj6uUXUg?k zOK4;}ASya0-nBK-l`OCg=ZK5L;K9ednC4(7_Dz9cA>FoI;(M$i(DWrcc3sY6*4vIP z?sGTr+Dkt!mQ5p{K&p?YJc(DiGTPH#{}W7jTO!^76_3 zuprysk?~7R=}9O`-CQu0r)$cDK`DyY$#6NZMZ16!)FudFuld3e?gfi*V>wqxqui5l zXZtFb(4BIm7lPUU}3~=;q1<>C{@TygWGRk}|!!^hv#4 zfROIXc?0#L(=PXU!+II5RL`X$RWp5BKcjHFiGa%3GO>$wcjM6t*y#=rQf<2rAVbEX zDQBfs^U0G-k>i-$MA>R)-nKNVW+nmReW%ZYwtri4W4{P%*yMn-PdwDUHN3bq;lY>t z?(Si1McVYyfouvo?kJ9{LE{yp$RRp0EhGViG-D3Y#l+?or9_GpS9kHi7IAQmzUL5f ziDWnvKWdTU(cRJd4CzUkJu#Fi_*X47#^oHLITt*0nu+-&k;-2dMlOcS6`_+)njx^A z9zh7r&{|{pv3;@Aw`CKL_q|*gLkL}qyhYc+OV6{qu|c#kqcS*sZi27EQeED-N(#KuM%rlh@K(*eErpFvY+?AQD|ITXH&%US zb{FO3p!JRVg8u;z8IIPwckaj;jUnX0Hr(^MjB zW=9gHx9qY2Mg$_HGdSej)s?d(8Q*o;^0LKp*Hbol##>P?U4~Jo_uJYzE`#a&vgQjz zYkUtMu3+nPdYACKMBpN0;pU35rfP$~`V$MzSo_u1_J&Wp!dDXq=?N>X;e#bb=NWmF zpbMQ=teygNY8O;e=tSd`3$g&F`WYs%r%UtYTdU_1GCZEai+u(sgKOVSk-v-zJO6M# zYBBZ>!ppPU!b|3UPP%z0&EOqza>VXv$+Pax4<^TmEd6Hay^e*1eveiGmUsCvU_8#$ zKZ>glA#NoJXsi!Te16e`q%hJXsIt}7@D@kSQ`J_O4XkZ!?RyWS;Kg;Kw;a) zyT`lukAf;bW`29bUEz}M@Kq2fc>NK2NZkvFs6(Cpn%e8MQDU~MN^wBW2|QfqYrarS zio6U_PfID_X0R!Z9ZOZJn(ksNY_OyFgm0TE**9SiELgbCBJe0P(mb@AQa*0BSc2*K zM@5`-{)jcLd0TaKdY%#GByXfj%+GyhjddB`*@jknT)#+!AUiKN*SnYnO8Ah$p{G2| zjoT6t@@$=4U1Gbwfp{-sqO#!f)-Rr#95=ubEE}`ET?VH@KT%HCGt+x5cI7ppAY24_ zSKj7qvx8O*Tuo!JYi1;rW~tl8t~q9!^}%H|PzX_`wKQ6jHMNG8WB`L^nyN4rYj2^avi}evXcgysroX>zPJ*0f`_icMo?}dN z^$ex=fOQ9%pQKc)Oib+_XD9D!lQVCE-h5~kyKAg|qoZ-Y^^_jI#fmcXQ`AI4&pvZ5Rfc zhpLZ`?r+n((gr%WC-)E#qr6R*AMcJl!f3Y~%H9iL#K@HO(Yw&EW$JXx4CY$3cxsc^ zv(x2P`jiB>klX6ktjTmG{N_iWb+>M@HjExuenf{`vG`KaN8FSsMMO{%?|x#h=$OqN_e>CZW#=?M{qM#So`fJHJjCWx>-NR zC!j2)3_5NG>2%zz618&mc~s5^H&#e`hF#FdTd~aGG4e45UQo)uYt8JvsH!7CD3`|= z8jpyd-pPn1Vu{2FKFKP)x%D<4>};>Lc+0Rt0_x z3)sDc80$LSMb5h1w2J|Apfaen=Z>L*BB=3=H}3lJ;|XJaRRzL{@&*-4{#>I9kWDx4 z{J@Fc$QQdtAc3p&haT*EGJfAQk?Zq9N|{p@n3;nsq5}f1S2BzsJ6Pz#a0|N-0}Xf+ za$?i*)`Z5YIJXek`O0)QmhXB&O5^E5sR?sXrgX(rSGaOX0 z^J~cn#&$M`V=GE;ON2UVtnc5aOxe1DeLHWCY(TBQ{;QcJqHH0(Z_YlfR?JFStXRpU zVAOjky?|fCVw zf|#bXL<3;sG$t_<1JM0RjUdh-8hK2$1sgjCwPh>$VvEK?MTN527-s?QQk8OBF3j^* z&l=K8_uJISwV8^oB3wbj=en&7yuOBZP)+!UzR8Om4*SD`>4K36w~jg~ztN8RLBWX; z!S?hHba}g9rD2)U{-^c6O$5qFl4U8sV8l%i9*eUD`RTYMH~u50b{e`Q>W__;)1bQQ z{6PyO8)WpFqb@-j_~&4Sbg5B5nMw8}*?9)%6|B)^DlQboeNRN=oJY+5dKF@UXUtTL zB2Qx)MtVA;15Ks3*_u?0aGS8Vg-s3VWpqVEANTl@-Hx?8W)lT=_f6cuNn$f_L5+cK z(?;{<>v1?%Ywl*rmQ7)_n4!%_F$pCvu`xwO8q1J=yfT!(Zdq$^TnpE-2s6t9tX*o4 z)%AVLB!-Fc3ezmLFXqRFMuyZ!Jlck(2g~Lt%3`Xo2&i^?gc;Uyt((;TowR@~hy-u! zR`jD1S%UgH1k@Lm9ju~uZ?!&1+;dIIc1|>&=SGTu8A_BgdJ)&b`w6#`Ue>&cMGBGF zd+iA#`T%pN6Pxe&Edbp|BqEucJ?T4Pv%YQlI^jK41Zw_qR^H|N{|{GJ9T3&}v;j$x zR#1=-5s*}3>F#c6P(rDtL23z+mX4L~?(R?;q?@I?k%pzdgZEzb_x-aXan8K+&O9^o z%$)ZOU}o_}A;OM6zZ+C+b~w-uxloA1POaKi7+KAK)~G}NYu_|%kq4|} zbl5ii*1)%h9AKtmu0YbD?Y`UIW9M`q;;&q*`-juI_wYjL1I}-!$B68MW#mnNBOq3F*@u1E-$W7G~3#xKIBK=TTN75RmfA-2G zry_Ou^>G?%H50cFQ!iHqraBm`Zyu8XK_Ah{-LnGl*v-7x%aZraplXzJd5sJ zpNC{|e9g;fhnz8abdf_hx;LFDi4PdaMs2*na24~uxg;>WRUGUv+*7p$P4J6e{?u=^ z!LKuIB_Q>sFvAulu5*^dwz;ive#r;2xaVH954Qwv&V`1G9X6aqwm5Qgq!_v8=k>)L2Y^ z$T}?eQFid|Ecdq;z=>$%US`foqpi$OcBDkpg4KDdjj(EVlNK^pOl6GPdJ)nOJIe&kL_auhOosaIbK{ zZv=a8K}3r4)v*%0rXPT}wuKd)n|uM_uZxgR#QMQOO3vnEABhIg~% zV6U?hPlwy@u6`Y8CO-C@oC04so)WU{dcDh`&G1ChCxx_uKAw62CdUg z9F+L;n04Y+je}*8Bk<+TX!Ubgo!qz5SDH}+`xTL0Th781uBX6i+azR0C_bRT>J- zS{0Q?+3Q!9*s$ZqMj`Nso!zCbS!ro}vMEn9Q$Hh9vpMOb=J0C>*Bgd)VgZh)YrQ@; zt=-a=T}VOT9kHIl$-0l7tv6TNVcmNJ&Bl-C^V{p}o5Z@G7C+c=G7jRdy3;i(eb|xT zNZhA^+F@s+gB7=eNVh85-uS0>Pr;=#8-%$BP=0a01~D&N@}H3DjRV3Ut&l0a0i#UEnzQbDGFPLHfU<4rhu7B`d zrFf_Ax9E2@TWU-jr?VF}lVlVkdFLt-yYHt&CjsWB+6FiJ19HapYibU`el6+xNTU(Q z5SBjeev4O$LvKijePa@Zdnv-+Oe^jT$dDGS@%XFdrxLT zm!o%J?rwR!+wShbVU}ksxK6Vd<_XLP&DuNrB-@`AzZgPv44vCY8wIC7U0;x%7Ci4i z+W+cQo8-A1eUd;HXHMHmkJG*n0haj%m>v>mpN&6{a$>40k>@{J8JTp}h|m-2LrTc# z1?2#cB3d52N9noQql~=PEiY33{ouz}f1vsy$TewyS|!NT(V&2#RZ9_0u%%BpkL=Da znrT~3H)3OOJ1Kr8b(#E{ZWCv(Scjq8KD-46Kix7oOg)EsJ@me(h1~EMS$XSR1k<>t z(Rxg{Zj3q>q|QxtTOsACnRPWiB^7ho&}|FpkJn&k?ZX!#@%Gb$$DZ(;SKl;{67lLt zXfgx;{3t)5soErooULx5sM?$=YN%&E2hRaB;7J1eYw z$T+MFKFnpK>1`fMk*304=k$=`^KW>B2QR@jqlR9?;wqL^Lz)LGCd`0csq zUrhlgY)8!9))gJOecP#j2$$POAp%*J`Wh>OaW_ff>qRVYv}`GtvA*d{OgD4U#5v`GH+;?GQn ztO~$Mn2y&gKV{YJLGcVRJTWsK0P`9u*a%;uuP;&7m%nT!@(F8=f7@(}87QAN7#(1}5CMtL7CxwcNs~>O9No4OWo(qH-w%Nyz@eoc(4mHKux0J*zdfgV_>k z`@WHfP19b3Zr{X0<#>^>J6rP3nLJryYC2AR!FJj857kwhuIDqER;_+**LDT&UB}hO ziBtNr7ws+HQ$6+-R|P+`;o?TM`eklU-m>L1w_Wo`UeBhO$^Gf|L+(y5{)p9;+;R_T z6`4bYgor98@r0SY%Vp4tV=C3^5hanFyDa2sag+M?QCn;fAIH9Spw*Q7v`IXpPlINe zv7qjjYfL6Fa9+KOX}&y&{%w$*)la#BUPt>W1J1-3iKn))@N0>YCn39Q!Xx}DGR)u~ zHs1)N_iEl^R6MJQkEF$)5*aHR!kOK6==9_K4HR4l#9MStZb-lU5yC|x> z9~|oa5(3SRP^($Qi20}$2h@rbsDcJs&=r#>WURN2TxNW6gH8jj=p-zQ&s_r9&;=2Y zN-;#~L&qlKD}`h+wbDbfwLL6HQ6sDDd*(4WLn>kmu>-T)N2@rsoZ*Qfx7v(xbH|0- z9@W-|*XpDm(y5&qf~57^9b77!wwfF)2NZ*kkm?kyjLdVYv%9iP)z5d!%X9WAqi#Qn zY3;L4t=3&txj8IRG8BfPO~I8cZc^)ZR69itP1NBYMTSb^l|Ge~c!3{t&rg4TzojQD zbJ3=D_k8tcUKge09svac;+?#>o(RYkgHMqB=3?@cZZ@8&kS5F*sw_Xld~!{Z(!dU- zS(j@pMO?^Yv2BAkgD4jHa7Xgn;jm$$RX@g}q-{71{LU7JRRQd#}Dt@+omOio^a8 zwp;kd%R{K@gESp=cVg9gPnPTUK9!^Shi5u%EV^f%lARvr25>w%7I(GVYj|e>*)46) z!+0k?^Nlr{6GkTD9!cUv?pboHz%sEn%E=ruc_r6w#oT;LoL`|O+8wJM!l!;v-Q5`_26M{yE+dXsj@GtIh|nMR-jZBGadgF z9YWLVscZxnz1@QuY>}+alT=z+@>;MGMzAV1HH7^LVtN&x=rK}Su-tq_+C5>KSi`*B zg&IkarW2{(++0yycpELSE95IjVHNAJy1EhSehohXXzE;Z!$7C9^?h!_mqzPR zFRP%3%b^L}9GkiuFbbdrj9%Y*sH#K#z`g|!F?96n)&o_V>!J-0;MH?yk!LFe8Y zvK5D#R~F$VSY5T)q7t*`rH4F=Z^u&(2LiJX!_L;8Eptk9`B#@U_Ef81Xo-I~cFp;M z1N;7P6?jkBEVa^$ay!gYzUfbg@s0~x;siL;DCWFdR}6atLKT>_Bik!s6E=ao<)hJh z&`o)8C(lW#c=5~AvoD8!m3X%!qau|Uw^@c$s`7c~qnk}e(I0XKNGKV5)`UjaeS7ld zLpib6R<&ap#Km;1yRidGLA>LFuc~XJlFt<=4%j#%(u4 z{PFpEMEjk2Hp)H&-?SZ~d%eSXpH%32xLl+)R75dMR1!K^^s@VEt-K)lXd6$Ev%DX| ziIca?WH)NF4SJ|kO0nbZHLiI`@{c$Dvln>iPsLs#kx(FbB_od>@Q=MCwVDV; zRi);(NHN4Ezi=sD7Ve)O`7p~Lmr-X+u*T4J7~&@XZk~bzOY?A&FWu0rIgy`+*r?Gy zVvOr`Pdd3B&il2`84b2p_o|Y(6OWR!O}lzTJsrJpOzH(39^~mRh*;J?>^a`GsL~D4 z$m-4N2D$bosq1}>#)3hum#V_%gOe5W9TzK@Wq%rSAZ{Jay7lOdHA+DA2niRH&aw28B3O0{d^Onco0Vk?|B!b`g^-EYlV{6w9*hdceOe_h|$hrr&dH&@yiKX=zvR zNap372)bx}3v;yJIMdJP)k^rraB&7#1#yNE8rQ-(0>Mb%ay}s!=UOL`!*%NZT%9;j zfWpikZVLaI4E>g!K+L@`QP)g8N(qbN(WB2AcQDH>E@a5kJ6HQqpQSR#LWB(4}JL-PRNOankpQJwv0&Thdt5_sb+&a z+Ek|GA<#eu+rOX!F76pD0+CU=+GIm?c;C5Yal5qn+304fBT|AFVnQk7O*;{In;Y;2 zY7;HK2y&-Q1ltYxL90O59y#&-dbZ_mR2}5{NyQhp6!Y8}S6qeN`F2Aq+tJxWB2F%u zG{yYgX}Sx^aU!Nf=aH^V;Vjo&p>(%mbR&gd!guA8Q_eLn-f8*>C4cWZ%OcqLAjW-+ zf!WkRAqcra@^5V(UB|{nqxf_xeJvU?s+YTu^7>A2`%q894f*GqiRP(${K=KiTGyN? z-7U%sPx4(ahVC5~G;hPhT9H#MWDRtVaq#_%sgvN)Hc4@@8O)I4mC7!aN{~@yskfxV_hW zR+N#aN|YO_8_77qiS;LT0#ZE$%u)1>ryvCq1}c>$`&&!zOYa*K1VcK6ByS&@AwnhE zeH~|-G9&jN?2p&MuJ@gZk;`2-`$eh_jcWQnkhy~+QyY%!-SjN#=cFS{{-_ZJaYOV!a!rI!)iF%%b6%8Oz${dl&1UA&Fg52ewZUZtdA==ve4n z95q>kbwUq8s8;i1G2D;N=&fp#4hxtqF`>rm!zz|4nq{4Ys`_0``td!7UzlAj+=FnC zUE~BW+z5YsJ1aAN_#;bs&F$i_Vl#L1Ubgh+^dnmJIJ5Gd=pFwmO~*=8+Lf!iaSFL; z{@{-GkC~8J^Rn*Y2pr_UJ-WmjfyB?TWf33Q7GIIZkrJ{V!cER5$mb9>p{WE?+~<=_ z5qqevL%X{5FnpPhJ);_X*7WDkyc6Uf7M@20Tr{zL61oUT7(I^D zC}MS|jk;l+m&#cjXuPIO-z7c~at6msySVY*=dzg^Ev7pdd*76Ye`Bl>Efon}KW`R5 z26UGB;>4M|iZ4voHPe|=J}yWQ>6fgk&r%$heZKo?me9GrSKCn_ePdWSk?SxTUJS|w z<8l=eaM&goh^TI3c2vK{Khg((JFMEq>+fwcUxj(}5DxeevD*}ulRS>&l$&{nH~3h$ zrcQV@cWgiLQq=jYXb4-S3ozYhTq3FkCa%MIKf!AE$&!p)Jx)I9Nsqv<9PPgd-@=My z@zllF+k;||4AJ4OE?PNT%j>aHi-B~FIB=LA;VhSy!y8_3r;-t$QV);%iGgLP+Dh`E zw-T&S;iH(&PVS+Hi^;1FGTXG?$AQVrBdJc>(20zNfWz7IOzfL|4#a5`(~;_d-A_>~ zv?ID3;&dYl@hDvbm`j5R8u&$i)Sv*E5_?7Mvr0^@mbB?Z?UO1|8?>>S? z1&!+2*q}eV6FH4QvHpN1BAAPC>&<~WWPrryjM%&|%|(AQvREI7=@eya*kQHZA2Y{; zqN0yeOkkn2Hb=lBL=Zd@u!9-?@e=M^kh2-8dx7!L(7yVD-D-k1UU+OD#(3?V`8eC{ z_DI&e*)oVt&P=eSuPkwrtwQ?YShvbGw!{2Oxjt(C|5X|xptNZu-w^;nQzktR$Ca|N zGA2duC$l3Nu;^Y^A1LsFJJs|^z1f^Nlz0a?)JP@Q_6;g?33i%xoPlB-Rf&7&(90-L^%c>ZsJR}lm*&ijx;vO zS5Vd@1zxU+l-3+M!QuVbk3XX8O%w<$BKN6hK^ocLq>1*Vn>VRRAzU?(? z_YO%aM>pBI$X-rQ7FlN*RY=A1y_%K&#~Nv%!NVe=G9>TqgAQhZl$rK<KvXjkZq{Zm~C->AYM<- zcoQMhG_5kCQm?QoI`4HFt+_%o;eeVi{5OEb;k{i} zWOZ-z6*NR@|NIFC^QGF?B0VxTY;~a%v?$`$HMc8KFy#YY^ICkLC|QQ|Ea?lJHs2{v zceg#&Ty~D0RAJ&~e?-AG8s4CQt#%@I16wGP;O7b}P*S`8yiX0dr1OZ}*?4qz2MC6e zSSq6Lt+UrX*ThRk{CV^%>*qAIxhM}+dYcpN>h_K?c8ks`r=WAtBsfC zAq;#AOKI}FOM)r%$g1j}T}2WtwthgeZOsZQEE~zfNI7C+a*q=2b+X2l-ZZ)0sGQD3 zVUaT?SR}{eDD4DBRVjUai}tso-rOZ_l;1dzwJ1}tkE7Gg<_OEtvaqyh{d)XK_s6c1n@eMxek%^=-9r3iR89JsC#GUpgR*vuxN_AE!YazbT3?G&uhXlO`c9CZ`-SApm{A~rqa!pVvMuoiZ@u) zLv1>#t>Xw@NC-QRWkQcRWY(Tj@b*b5Vi;7sO9YZ>XmO% z0#(m{;s}^pD9~9QWd+sUW+_W>EDv%uV+M^$motn9o+ZpD9vl` z$TdqKUDPJASJYDYIH`6Kv7)OM!nS~}XGMA_-$@}AvHIvx$Zgit7gfs25jDrUG>4V0 zjlyICeUQn7rX4=aBqZmXvxdd5@|NV2=wafit@4FQkF9{ygkSnfJxzQ z9XZyZ4NJRbN-(3QWb#gBUOqX+Wd z9!AhUS0)mFbOJT^8HukraQMjr<%5S0yabVu>IMk$wOzk3c}bz^I>VroVzlHt;O{ZM zQUcPEy&3oE(4oyx?mc=@j3=XRB`!RHHo8{vVvGKy|LFy||Mhb~H8+_r$sGJWk5shx z0;M9c@k}~LmzTXsOdaEO%nS#geN(ZvWdF))MI%<1ujZ}Kincv3-Jk27@QQLpAx6E+ z&&H|xSoMF)aHOS@AXJN=!f}PUSjEGrq-7a zsS*P>%gO!tDg0J6n}Bi_$;9Y)qd&Y9a$S9b{fjlgg?erkhZoqnmLzy*YZ7jODph9Q z%Nb|*7hgp8KD4##w%_Y?iiNnkxw-AWVEFVj-cN<>W-^OWcdBKO`nmnbWKS|C{KB1* zYDh3;q+H2qN=!Z1u)OK(7#pQI1S@kkOS5nHALr&&!`$oV2%gw9qWhyq{+%ifawCGU zE0d5PzcNDFDv)oy`siL)1l4@D>R->d+XhC+3Kl}!_kvtlKVm8UM3PDKO3(G{WHn8L zGymeGw^clgy%l5Tys@WhWOXD6BJ|M(sfSsGnXv-1C-UK0e>G;0t2Q5%FkG;KBxA{qM;BbrVlOn&NZDH6-GlGb*EP_eC!%Jq)gxJhSrKi@ z+t-4Vv5AacIhIF`4eS;dnWdDIlj87K|I{p7Chh`rxhf&sug>UclJqXe0kpZ5@HGkSdDk}-_4MLP9FWMbz zbrpx(>%OfyJU?aEOYX71D6^K#wnKXYAy6_$jPFTRniqU9M1H5D#D7$jT%E&{vBh7h zE{xPcd$)7g$O#+?995DI5-~n?!B~vQCh~@BFM2gY8oP~G7+7m-{H?wXHuS35c516q z+!kr8dZe%=SLBk<;&ut8>|R=UhknR=PEFEZ?M}DO?WfyM*Z01@$W=#$SVyuyz^@>S zg)PKkOjW$?{E+@f2j3J+Ko@%aZ*|<2LDYACBt<^!{UQZ1K!t1nbTr2Eo_?aFc4liZ zG?fdDl$R*I`?jA~YLLFLVU>25k(_<@vt!y7=V?_Tmh++EobOy0H#vD|jQpwQ?6+(` zpiXO(94)HCR31*3^^mLnU<|)D$H`At;;+pi%Wr<&W~em6vvm=-kS3}!txY^UV8taA zh0UQyy|T0JI>jr>C4F>(dp);mH!w|Kud{lWUwHN74?ezSi#X7YH*`5#GhJ33d3`}8 zg=<65&o42y*EmT;eRQHwW*V#c;f=%L0HK=4k34(1Cy+$;$5%Uyh^t3F3H3oMZ%4U> z3z+U%*F};ZjXLJ+Bm%)&*`3fsW_pQ?p_95KLcBhBEM`>V)jtmPkJWsp0r2;y)MqaU z)xt%`V-k&ViXI3#SLUhIJwlEUZt9S-(*RqN1;v}q(MaSoFVpQh8;iVpW(B7a2)k8J zn$i{b0ncG*X(Kh(VNoq?Z33cvr*%7w2Ey6CeMx4AXQ>3#2> z>L8Ody(kw7LoGSI7qXZ%$ER)igro0OavS)eWw`MT(bd#bPbKjF_zytd2m)LQsU(%` z4msigM+YZ!;3oObELq=2{UUqzUJ^sL5*wxNJU*{(0at|Rh60qdqX?moC?xxJlhNkH z=sNc8<6j)-cs2toQd+uSv^@q=x_5`)W`-gdc9A4pIRevU=lwGkjN!KFeq#zrtQV&Q zafIe0gzx}E#paxWR;n}LSM^5MLwJP)(;CZOYr;qMrZy;OesLnPo@5jbPrBV1bs9sA z6g0K9=Il#*f3WU5C+POX>0zD=mh)8ZugP6+316j=Ue=0fcQS5?vm=+`D(b%OP0MMB zAQ(U!h*GcTGm3JQ3aOymT94diJJcO|d??2c-ZjDB$K|IBRhh4hL>avQhxfpZ0&4b8 z)DPaDB(=14i;xK73@thy(&Rrbt%nL%3pryQzaY1_PHJ&8W4~J zGvM7;iGKZtqBPBfy3U5J{^C_G@jIDwo}Lp%Bk2Uf`u<8x>6$SrbYnQG3xsZj4>Clx zFovq*&0FcSo%nW=&w>7p$JY5Qw$HU+$VW#L+bip^7WnFMTX!PQY5)n1rZ>TJ$IQX?^?Kb8 zM~Ze$wmsKIl#56q-fxnktVvTWO;Y_LZ2h-G9mloA>$&>b;i5|l-XkX~tRl#!$KiOVAI5=>i-Pd9f2e*Dy zZma+q$WW(%Sb@Oxqvb3ooqzsbjs(=S_?Y&!4GKV9w+Nz7YNLjPVC$^;@rLCxH4dfq zuV6axS>6yo!`7$|EtDz`d&uRCMuYRO zxQa-iPPiNRi1h0A$q;m-Bj+gYrCuUf+e*1*CdRyuadS^&5VfOA`xuPOs=){a%8Fw^ zexe)px#h3i*y}N})3V|h`<6gNyq|6n0a&y+=(_mo0NfJlePYEbs|PcM@7poYho`?& z0}Gq?2(eJh8CN{@m@0E06-lwX!(!<&YygXqbg~|X>^w#uJ+SY(uZ9f>Ost{|pZo;sX>L)8#zb@w09d~YP8oXtOc&igmwDBl&8 zbY)#Wz%#vD6be;3c`op0ofOh)>2-v9_gFC{;v@N|gRi%JjBN|BIafqv!( zBwb~!6K5$!K6G){Otb)h%)4Y9RWxOTzLxLt55z?9unBM0?pulHcuU;oR1``^rHRz| zyVT=Hi0EYEQ(r&%B@}g=4DCyp3r!eW>xlw2o4IJ!GjU1I3Rp$I3=DQubiI8l!S3JGgDDT2BHlF_}D)9;IJ?Z!AG1cH+iIe4(UTEj$n^r@e$DOa1>mSYGa0INt zn6#&lwO0jmcuRr^CMX6!3&x*K5!Mgb-^x|#^xA6eM&K@2*KXPLeXH(qjn0uiyVyFz zZCc%@{FJQg;be~NJNaL_b{Fc%0mf=eL2F404c!SF0Sm17%;wOADduTrnOact4C>Er zft%cn<5-U9t=JAZ@H2j7im)Vv-A~6~%*{4L>r=9++k>;C7^_sb-c0Pm2;Aa!X z`i-lg^1w-4Ad`dh;!Um@sZ}8K03Vo7Y}^I-!<~;t>0Iy;#`TjmwGjXVY;%0ntR5>Yn%mA5T=z z8B{7?P)-9kKnJr<{pAAnN{BSiaQy`<>V!|-6iIESQ7Jb`W%e~m>LwiC%STpk*TLY7 z$?M=vOA#wz`9bxo&i_kKdZL%u_*)=&H6S-|XW(qjFdfFt>4S-%#`Cy*+@jsiq^(9| z#2OCs&>KFv@R(BkqW)xP@XPd2JX~MD2`||3fZ^lERP3Z0D~XZOs;`&|cpli{DVn#%2V&I@d0ZW#dWK13D;E~e9YJ@?6!4UHn+1P`T^nw#2q zY!Ak*uA9+Jp{A_@uE2caTHzR+jJv@GghY87ea&SF<9ql_%!t$U&d%vrzk0b_c#5~3 z9jCKo_umWzekk2j_3KYg-25i6#2d8XtD)ajHR4$nOgVLY?!ge0A?n z@A&PV3fNXgJI_GqHL}>b-h5WA=S^FqEj1(xSQWweHrx(T<^)J?!?+q*ctNUw3uFTU z4_S6HqyiHuqBVnwK#^p8j@v%^n;+{YT%_(MA7faoG{9e~{=tG$cocwi$_*!#<{IpAY8xvU%G!?$o*@@PxdwP1U6@1g1 z_>~uYQ))Fk(OgX0Y__vHJAL30;i2&~8=h2v?NhtSqkZMw!i~6kJq5o-bZ;JO)qFtw@cM(xK66x?=pSFErh{IhxrHZP9U!d6c7V}DOnNC&qL@33Ic zcQIUHZV|yqP6Dij`pJB^8hgCYVRXnA_#bKmQ`o*1yAz!qK@M)WTpePq{?*4Z+dCUf zBvNW?fQcytt^4S34y<%&S6YDyhP;la39Q!PjfyanqIpc+bls?z&lFXX0mVQZ+dlsb z3kXOe6e*`mxgj{=ZhZ}k9P!&*B1~h|S5!XxU z>4`Q4;)uTsQ@{i5;{hCmxz`6Sy~xoVF@z|3#^d+%-hD}+xPAd;{ynN_<9Y> zPn6%qI40sCe*(Y|CjvL159r`Lczo~=_vY1vuh{%Pk6)JdY_t=eymQ}p1mR05hxC_d zqJn19rRUlv%f6BU`{EA`Nh1ZR-`z)ckr6zD$r{^!mCGty7cOD8Lx9ewqc?e6#$1Cx zWOJRTJqLN2fJ-MUO0fTIamv75fL|YNuU156!&^S>c#K8CD9q*edK)={p-eR{bRwGV z0<7@T96z}RhC!RS=it!9hpc|5B{!hc^7a+iGb!Nn*qV(8vC=puPO!XBA^;aGz$3$Ci4h#eW%B0#$uEDY2TkG?urp{oBOJ30b2w7ep^tq2aTrM?lY3+}PszgTHGOKvk)@Z3>cmm_md+nq9Nw^PF>0-c!zaPZAFuxOxg9qDA%Rx&O`r zAURTPlUz=;V!!!nl&}dT=DBWsLqlupsc)0Ud|O+s(hKe@{qN!hDL4ULq9{=O*nZ?y zg=Xl2Lh#{V3?>BtCS@7J{{r_hJ|d$_{PyGSb=b2D-SfI>Nh2$&x(VvESn302)GkCQ z2jF${KYYr6OZ&D2InWO~0gDhJWS=Uf=>%;xd?eAU=1yb;Bu(@j-XvosRb{EXzDx;; z-B9De6FI2|$bXaBcdq93834y9T&A?U{YR$y$(17a^*@gWevX^qNysrvPD(1*1mU^L zmJL2;>rYsb4;0h8Wdh+%#runMVF?ob&mQ5%3+9P6mo|3P(IIy|A22m7cz1C1;Pwuj zxQpkazq#621h=s&V1vl!yqFNucfyBZK=l4sas3-o81YKl zYfnqOVn)-eo+C#|F!30eF|o#0_W<*@0PL~?2t~w=x0xDGVP5N|~cpX>LQ{1zM$cTjZy8lXg?n{KnZcS7%3J0}ONFa0DU@jrdH z#QnFz;BuZE-N!r-(ncN?a+dGHmJ(3lP(r}$;*pOXPqrmv^4-6>Os5~fWE7kojKmCe zI?o{G zA@V1eL>qMFrFgpR#+d*-2Z~HPNaps zQduUpS~`$u5!gc}Bv4WA;rV;@uQL-3b8l(6ptWF++<{5rW$5|^`S1An@Aami0lc$g zP5Prdc-KartIB-b_?LXwG${cKBw&iV!LwV)rQ^ARGw-CFsKL)5>dtlUj%(n*f6_PL z-X$10vjU_b#D-$&7V~#bYK@=$e!18>GFy!E^_6V>t*Pvgack&`PQ`ZjHzBqv@R6Xv z|0>O^>aM@E`|$sN|IQvhAhW%O+^+R4{s7@^4mSJkSA^{;)H=gh)z4~=-niec_Yx7} z?cHm4_~Ri!P>A-NzIr}&)0BT|_>l*BR3WJ<{Yl}kHI~Q=t{w>AdguvE9PYDLGIq#W z&~{KN#C2KwU)H|ernryymnwdJxgfL zw}v}Q{z<7=p`>4_GUl$^nAin+7@Ro}LcrX}wc2%qGtFz?vX+`KI&_CA|AGR7mp$^R ze(m5;JQ`WXbDtGPIU%C_oBgwR9=qi$s<2?g%6M{kHbL9s+U$@T> zUJ_1ysrcD5SH?At!o%AGBj@IIH(9#XFLQWFvQBmwHbm@|$>zGk48JIR@htGqVjxIn zBcb*U#mrt`aXC2^o2}`vpCmOHEDbH&m+jhll6-Re7%rrW_pMf?V5jp> zddomG*k`Ja)ZN^wrO$!5FwIJC-jlveT!h+}wj8v_d5T}i! zPev|RqT8EgGxQxh+@3pC@bbII&Ah|!Z29@e4>2$vMi!4_*K4Y$S50M1%CZezl!jE% z3>G$Wj=UpD*qZ>aa{SK*3}PW|8On|&?faH$Jc(@y=wrN#^FbCy(y)SoB3IKSWeKCH z)0XD+bale%n)Z=NpUH&cqQEh${CNu&t?&gBBzI){P~GjM%T@&gR;juHW~#-yf`>lM z7WH6*2ffogCDz&Zj)*?~R{JlQ_5`B&!&geLa+8tS1j{tYC&5dZMp!NgjTk&j{CJPQ zJp-QBk1uOdSSZ*#Yi*TaynkI}1;$?=@7JhJDL&~FLkr}nHV4%1VK+oBO+i*>1)5&{k~SVym~xG;K=ZvG*ct&c(NlFJmG8I^i|tzDKFTRH`qK!hg%;* zOJTY4;-zbR&+vEJ0&rMO>HM8M`^c*N)86jiEuPX%6HgeANGvk<_{u%%f1DMet5)PR zshyE0F7xPyE&A;WfZKlq3$4t;Nx6AX2h~$#3dQIa$QU~8I}QW^LIUMau9kYre9I;X zI*jm1m`1i(TGKH-2nv~JdwI@`C46fv0S*uaqGO#nke5#=KX3*7Lk~}p#3FZj?fm>) z8N3_rek%tq)n8p%PY99TXCCJ1&n3_;0!CtWZ73i7mcri8v+}0U!Ric>+6(S=!#g07vA7?4jMnd`-7F!*qxUS z#i*w7ZY;iD=RU7}SU6;r-=hC*Z>VxWv(;PPJz+pfFldNS58fo;+#^G5%Prhu=h;V% zd`?9X=4?yjh2{?|$QG#8!h+ab?qW>T^AyJ!<#_)hHU7{O*UEsdr0?-pKpTI1MOrcJ zGeGeC#rq;4B~mChJj)`eGBDymHxW8eW0D!$cZe@PF3~*w!nb0tA+CKh|MBaSED?$) zh{0zwR0Mh=C0l;IEJLH9T3P}o#eU&Gnk>z244axwUv=|4BGu@M3pRe+G*jHVk?x6(F z@2xipLfAd4>N?Zg4_LEgVsWYckz-y=$LNy0gEiWUz&J25dxraxNvOrgvnL}qh=Tl? z>o!`Yob5O{N#p&F9lzi*`kt%HSc@muufCA~Kk2n7fW(q8CEa!qxgkl`E=|biAVms$ z)@M`= zsgEbskgV7h2AzshzZ``VD#tf&nd?WD*89DqX1s{nDePgq@YX`F!B>o}h-y(N5{}2< zQNMmQO_i3z(GSw?b=p(;#*p4KoEMWRLXDl{S1*sBFZe%=PQdgN0i>_E%OMtI1ie9) zn1O6RSq>_(P3e(9HC5mR@T06?t97ZT@owDU)=+>7f8W#*=^Pp=(c5(RjTv2VfgNb` z6~~J55>)EqhCc!cy`@DSCDML){d%HLME2=NRy4qKp3mkC)Q8J!VEmJIZ6J9;6s`t# zy{EZ6%=Q+-(uwZ@FVRZkDeT!A(P=fFDv|ph@U21W3fTrcXBH)BsS$p7Cfor$P-*jo z@PV%o1v+AMWI06^Z)__3U0rvQ&PfOLEb#n`M&VAG)l}6ebeprNyYMKS@y*`RpTPUe zSO$sjMvr{S+C``q8V;7Pai6UH)Vz1XN_sm+0RuRtkPDzYQS6|zc_k{bj78~iH7GdT zAm0GCIi|Bf&8qsAYooWxnv1uq83jy3u9g#A0W|P&9MJo-q_P@y6yn%;XH0~lp*=C( zV0Njeb*TO{E%;ujB)bla68__XZ)=bPn}(dJ zkjz5$B+9bst2C13tK36OiY+cv8XV)gt6;J&(z!#A+fvpFr;pX0f9xRweXJJjJ4ms= zu#^ZZfEo?hM+zre)$cUWYk*mpdup9=MdSmk)lpy5G(Q}q#N-4U2 zKM`nOK{LW&Z<)KFNxfka2FWdXi3S5xn$PlIYwxS@4!ONy29<(vO?cl~7`LI;CIGhW zBHgm*JdILHPcIg8iwTq+U!m^D`?e(2Y(TU`p@r&&JrGd&O=r+yy z^>KytHm1(Td2fK<0eR5%cPTOh7Jxb*K&VZRbsekUm7Axl{PKsw!r)%>XK+*5FgJ`aE3VGGZ%y_F8jWaB2qY75sU8i8G zAywa%X*}a``(G>(&S7gt&tb`u$Me-3$vFcA9K?r)5ySHvZO~xZ~Brf?I zQNeHK8;kL2)Fzj9qsvtf(|Z}GN1DGT`uT;TwcGz6dv6(4)z2{u^=|ZDulM=i_lXbhmv{KZv4?|w_F8k!->fy~ zTDhly^KXRI^oc`Kb%+JKT}-dPG^VI~O11 z^!5yp3LU@#oRFEbwPw9K+1h?}uLkP@Y=Wp#eflY#3vj?>AE$ zsw<@hxu|3r?Fmi6(Bq7Jf^R0bwd{m$PyD0qrRNVmZ6%}rLrTTrM`J7cyGO+NW;$cV zPaON--&O^qLGXnv-~ym|;chg3k-4RG?JB^Kw}?ZNinXbXGl81Ag!_8sH*I62z|59h zcxBlEvY4oj&f)WWwV-sZBJbK`I?*0TVNUbIV{IiSo&CX>T+sqZtPX4gg_!qlU!{%e zHT9OqMUywiCXt$U&AwU#fN{c8$u*U^8u?j|ujHzjd5tB>NeVC(2ZOZCF$wZD8yQ-rFCY)Z0J81M3_U)X}< zCF!}(!>*h5Glj0Eu=CUYdRYyuU&#gz0}Pvd7dGZ$gP1^YFAo)|?AxA#9q?WAa>NIg zWf>=3ZU%!-xeZfIuFF;V*bh!)z;L|jj`18Hnr$P%K%zO#;I{Hdwrm}M_fc7i9K0h# z$i;GaEyFj1;C^Ew`{d~uq8yS;Sl}4wUYW*SNw0ZWN98{}Yi;7bPbx@^Ag&|?v@ zsZkNl(f2K3=C!LfIh>Z2jnP7K>V&-$#D*hzmS^+^+HjYiL&Eay4^x=OXEykbsA7yje)>3e1* zO$o4nqxs8ilYE#%B(2ZAKk)Wvtxzc*lS+7>rGej;Uz;7h#?Ico3~Y7e=W`2s-^8CH zx%|Yz)IQoq^x)h|sYS_#%fg&^AMoi}0TN_=-6X9R+J?n&dB=1I7HuZKn9g`ZayXBU z9rCt#azoA+I-8mwE4=Bz5cy|z1~pOOOO7cI;+DiazBd^C8FV*OVhN=WrcMOic1Kk= zEzEkQ*h>d%JVb-PmQGib6gy{I6kemb@zHki#(+*qn$Tcv=6S3L>@^p@j0TPw8;@bf z${4YQ+{l-iQB%qtnI0$iW~vhn8koLo!B!vc%zgRX zNe9ept%FXu_Vt~LPa`uGjX7F!JNuvcZ+DLFNCHcE5c|LrN$95jR>SPJsdu_4BA?pz8E`$BMi2_Wlm8B^ zHhCIIWCq1Sm#o16W?72j_+}!h&*^H?%wVu6E{AqLRFdOBIlYG?cv$oI|H`QE%U1o)%hVt~pRKN|v zIqDlJCbn1)|8$LS^&-$d210kD%xA2UgJ-3-Blnr5B&FE(<7&+0W9r=wKGPOA$;>@4 zwY5z7w4*PuaM(j_a^MsB>7;ps`U6`pd*|{#lid6UzRx9CKo@9mc@rC{ZI0WX87T4* zd3K^&&M2lj_I!)7Y|<;f(2G*Qi-^G~e{d-q8Rw6HtX_N(G64rcNfxfqcEG9xti~Kp zqJzTBK4l)UiAVz|-%apv^w#fUh~i37cl|mz^VvK#e+0iKWA)g?neuXtHQiG9G|qSP zZT4*8m3$6S#<>>bMzlr2Zbl6F#o!!L-vX23-p_yr2%9c}+Me$4Cy^xb zLB1|)YI1j43Jign09f+w(y7UL*MrH3T{vTzW5Hi}%NTA-H`ST9)arM0h~OI+$W~rr zGXu_XBY^1Tl_ojBqeHm3{K^IY0zSGnu}XD;mM{3}3r2QNi~AMG3uyN_$JhX=E>{eJ z^1N`@L;JCav_kl>Fd_!Osezg2_bJ{$9VU?5>)$w*$=i9}@a>5wN-`SVNbWTT?+WH{>`kA^qAt8TMzQ6LUm+4N|K9 z7P~E;W{oehU)47XR-2tjKE~&NQ=K!GJiGW}L^b2ra5(J&AOSIL!X$Uku!ktxKPwd= za`GJ+?af9>JyJ)gJvC~xMj?L$>WLjW4i(iR22}) z2;OQLnoEv+x>l-hi|j<3I<@vd>4Z6!I3vGqU)Y^@MD>lx3M>21?tC(#G|hyz7^?2} zQXNXH?AQz@^`FVLR}HN575t%NzN4Y+9pGqz^uJQT4`WkPl4|Li6o=Wg3tk;D;+yZe zYDS&HmNV14VK|Dqy}R$bmI*u#QPJ$J2WXnntj@t1n>-8%%kE z;5qD!?V{Lj)z0oO*!@`wDC}d{)t=59boui2wu#`oTVjNqCu64$vdpn{F?X0Z9A@KS zd$*91*2p1~Ow!@{ehm)NDD7QX`|xw)>uS>6vX*0+H@!>I#7Ia{po2SH7eWGu^?ohk zA>e89Q*9ZQF_#G}E8K~?FgxLy%}EMfS-R5EAjm^8v$+Cq2L6a&O*)L+ZLU8n=-FLm ziikz0@srVV2@w`V!db*;tYuDLFkdw>8Gn%%U0Fv)tRCpdqzUfzc4T_R;nrleo)#AZ zAw#qT&dHO29s*lL4I!=A83eZ3C%t!&A7IzJplSJ7Y^Ndadk|LlVXrpH#(f@8NHsxh z79>1AN2I>>_kY+{W|&-%h*47mSvMsP{|@Zvp2q&k>o@o&=%WisAV(V;N63h1zd^~Njjq<>V2kcWRLP^zCcC>7ajD@;S`t$A0|G_| ziYu*MkvprX#w$t;AdDW28CF)@#>%8Sy6Y9STXecfOS->0g{Ub2^AS@K?hH|a^?hr4 z+L}~X3*q=G!_Ep(k|xE|T<`7obOt|>dkpjrX!|{cBTVJ-?~S!M81&8{k}IXJn)DsJ z@(z6sD3t&KAU^?&ZC?TYJt_m5Ge^f0*~`bu2}j#QBJ9k>0;S(?i2~FRyOtz9K7UG) z1ibTu8i-)v9Dj>cGjtW*Ja6t%HU!1Vg$s8vBXq1#7uU&QTsJ6%OB-Dq5WqtE;iBU)!dNMOxVL&; z3^nM}104?#9LJYta%J;y%2*|?FjeQ1H~W9>Ii|NFs*n*fw%KHT`jp>(o*v?-`a$2) zV);pF5COhHrMD&aqxOC|0h>g0P1UTNFSrXT=$|__L2RW72dN+oC#%Y$b91AF`6~8G z`jJ}#BrW7v$A<1so>c#p?MG#V8W z{>^Ivhj=4Jsuq&=-td~TrPJ#Cfp)1_GmU2>wgqBQ1{KGNAPEbQ0Eq{e<{wfoJXy-@ zJsAgP@$Bqj!#3Rg?4te5Ar$Fhbhu(lM z3|QW!TG=)-Z)u>=JsmER@z~aR!B;nY8p%#l9e@AWk(iix^0EX14Re2Q7=A2&mZEKU z&z}~zRI%GB(oJ#HWkU8KGFZXQ7@u_Q+w@5e=}arD)DGjlKi2eJX#xeVK+Tb9+e>Gk zyLn5VrD260fx65#j!7qh(WO-S4x~Z5iIQXRZ#||}s%bjX-k*K$<}X!hL)>>7U#Xt% zJgY{$?aULpt?Wo97|L}P?C$izmmq|d{=U)6(Ny&Ut1A<8UYX^-P7M9>Hz3f< zlQ?Z-UF@62rQVeBHRdKS`v{j3&X(oV5h?B%G0K{9>4h7W`pX;$6~m`JA5U0GZp{Td z>;*+#lzx-LIaPCy;pIN9X(9f03D7k_o~0A%YXCRpLO#oUTob4R8)_Yp#witdCnpYMa_zZQ))RO$PPlhe4QMp1!Ud)9crli(8Jx zMl)|3BZ)@JT)tjYOygm}vA}OXJ!8~cx!v(P4IoZ#*floWuNser##MlFtg#QNi=!z! zv~8izg~c56uIX(OcFC%DWrtb;VgXaCj@@+TP5-E6Ig0nKemYu%QN6AUIyO(YKDxv{ zm$~1YI6`pwwiaE)C$Up=4nG(K^0AbJtNgU}rTT7g&V)>(N^^W^>f)&j?6(84uuFsE zw;MD#H#bk3(l_xAgm__vX5&awO^!`FLbEUI9#7O0zgPDfxjrG(5wx) zV2H=oCkOH=FY4!!TK$lJ^+4}R03;%fLdXmNBE8d+`)9!5yo4taGiL7|KVzgckx3r% z{iLGwNM~PZk}RIM+0!k_eQMH(6SSVa)Y*rGSkx#^mfHpY9L=-#W4yB<-=>!(?s8^e zPRV^+4ftc7(?Jo{M-&wv!^%5LOJ6GTL-h8Mv~!hG*ZM+7*Ur^4k^k^PTwpaOc|%v1 zFmb7B;}L5nmA9p8&+nM%%G!UPd4O7yWF<$sBV2Qxelb2D$tsbqmntoVF=FFPZTUuI z1OnlhaEP)w;Kz{4H%71;i8USGA4GdUnz$Hb&PH?fq{!HywO(#Q&)6K#ljEE{pwm zdGjn0=cDCj6X!d23$uCS1ag=ooH2`y^CY2Izc7~LzMBg6`Uh8VNbhDuCe_|-l%olO z!gC^Y4#j3-#v2YKR=OH>lqb|qONKA~r#{(PGF?|Ui#c^X-I>H0eyYo3g+qG~(3Rw< z9rY$Q&UbjWw@1(ux>dM&`SgkYQSDaoLA+JmmCQytP_?6>-MvrOL_YH&%YH}4`I0m_ zwCfY^c8w=(L&gMW3OzZ>uVydiHppdWCKDhbgmKbmP!g{N_YlCX&{&>E=YwC_F^SKp z6%B6|lw+E}JRYsMT;j`nVqHUHwqBVfwS zd!peBfmSoD(V(4dbz)c6W5K9wTp1(k>*U_+Y(D~FcrqZH24Vu{+Z>4ek*?G;g&(bn z>t2(`zq9U89fdL$rYr6+D#Nlijy@nLN|3{O#IfmWap+rKpL@ zx-6GOrJ~-1JvU@iPp9{)1|HJ@HFq>;;90D~t?Bv;(S%ss$Jz!r-M)*((mn!^P2Sl9Up(4h~k0S^#IT28>T#!M>)EaNmZdWf@Xu^ba#^>apE;H@6lEb{Pciy zDKP<|aceIYWd_NR3+#(kV1hzaZ^4NB0=>8pdcGH#o>2(p!|l-AHMA>t8!LIMRYjYG)HRZ29LO1n|uxyG4t?<>3au%|$ zb=RRy+LK4+%>e_cv?X5Q563(2FKDP;)s*fLPhB+hh8w{Pg`O`Nz)MNa*czXUrSrd8 z8W-2@LD3{!d~N{S3kPDqDeqKXcKoPvq-DoKs|zm)FZft|Vg zA>1+_uH5U$F!3S&K%vmVMFcL9l>qaZQm>|iAcxJp=YEfO&YSaP7?%1}A+7vq0xgZ> zM?Ow*H=I@dLLi`HpF&oen6*_u3cfGL>>X|ML{r5_zC$m@#6ZiDh|?^+(Tl;SgO2H* za}lMLNssRvoNo@}Q1D-UPSBTZSfmQ;Y=O?x<0-n0aIlO%7^3Q|C#K}OE(rRjUX{0< zWFkrl6?6M0nt?AzTUdOBK}h+uUCdh{mpkXRt70nm!Ip=YbskiCp-~#jcX2X2QF9$= zr@8zEsJ(HF7x>bArb_{2|HS%ZWXY#kDKy{mlt&Lz!CcjxC%g;0AgIaS8eA^(@#Wfy zX1Angk-DDjV%I;fA6!kE=;(6N4%tzS-6z066pC9@fYybUx--g=3K{iq6COUOAn!rd z*<}|zPU{BI*FhW#f+fbOwiP#Bw_mLEw<{23{LL5Age7#P7qFRH!UJyR9%}7#gmeVp zT9Us`@pPDymo~}3h2wE}w8cC`>NX*nYGimvIGj5{z9v>DTP{@x7SZ>} z*V&VLEO1X+1~*$1EW5r*O3o^~`U;amjfv_n(h`(&PJPuk;l5N-MTQo*e3)-4}% z`Yb%;bJs_q>FcE>9Es>;W9bD6r0#NRjSsW3R61MYtEP^J$^4Px{^3ufhTO(i?-xJJ z0Ocx^^sJBeEod#C++bLR#AQbki2MjrQ88sz@w!F;=CG9l5aa!zhy#ncoMuyLp30xx?luVwz9rf+f?DQGx!$AursT z64SR#>v2X(wKbi!HL)?7nsBzH1TfhXG1W?Kj{*e$=ITfSu8tr!VkJo$8ZD^xh~^=( zQwsk&vU?4tTePHDqWwy26OJc6{y^gvW@UbC(Ww1aKj`4bfQ+}sZGB-{&ml(K`>FoQ zNd8Vx-5mZrqWH;fWM9@BcC}@HinPn-BgU=0y_t3)S=vVj4$+b)lyy}oAO#v9*$uXJ zXF)f`*92rR)e5WiVzbksLQ^~x#x-W9B|HXlmX?=6AMj+Z>F~KJVMaIjX%tg$3>DU5 z4=9e^2yNT{JSNp+fT?Dof6#43|FX30K8)c=kuc*c3QGA9h>vVhPRUtqFV#~0yTVRv z6dWa0|M&ctCZP;kE2<5;nFpJmKGe@|3w0eA`s>Zky$dh4WvC|?iIIEqE=#4|%=sn?N0^ucGSA%bXCWes*JD}19<|}|UcO)S3qUpfDUV3!k2=EezO8~^tUdRhzf24)Dmse9l?fX| z;99Nd11^4vJXSEeWKKw}=<1-{JX)iX^14nEo4Uyx?29A=%7A{r&gpKR3|}!egHaMG z{isY&Wzd$*0(xm;yWAsH2UZY9^tRHooa<;A(zgYeu4UIbVHdRT?EO{ZL4(;SyqMiG zX&-i>Y9Q}Oo4#2d^7W_%+zRq3uHd_TC#{Hur_Ykl{NyEJ489>P zSA%u%8R%^9=mbxOeln^}6u-n}HF`?7k!h9PB$N9*D^mJ=O=}icqdmsSzW1q@QG?Z; z5975E(Mq-pyfO{HkAp8UZIkZs^ziOCG5EhlD3ppRB|h<&vEx;$lWoxR`Q}uiUA92? z9B}+%7&Pv#Os7;Lmn^xFzh(JzVvBV%Qo9uFWMPcc%*apcJ} z9AzJdc8s!3W{2o6aY-hMND|5z;?~Tuy#r3)gelW__<}8U31^_|O6uqbbK@J*5BUtQ zbjQQ_0+pazYFx)tVxGU&^$XksTxm(chw+%U6lnt`bn+?UhMR&xrwLkRXe6p(t0q`R zQDhzZJ2#SF>nex2_gie=$IHxWTubC13cFuZ;{?bm(jG)#t@JG^yLIi9>hi3V^hH{F z)X&&o5wws3gN&sZh@Ebj^W1HBIju{Scc%MiG>U(xAt zpn-Ye=NsJ;X0NNL)W-Tw0!G|$FYpoxVi<6_EV067a!7kFOpPumyy>Ud}!~}()_rAWJ|k9iAGia;;*eKN(=**Lp#CvIaB@tb}8OeNdY```@t&89eie{kEw>@1r>9 z^^QDQXLC|XXSTPbr;nrVdQhumg#Ik0EZGznwYagzX|Ncw!D3LYE9;~-Gf{}DE9R1W z@-;gw@ioRj->bUM&T;v$D{>5z3`!mW^+$n8~P1KrX* zFNyhZtZ7taLCqFdjQ_9b>*m<%iwG4RgBelFKN};$De=SJ-~P?QQdcqM)YN zkRr0u1yNS@{$YWh--4LRoE-`SaB~OWey>#8X0;fDy39%*aG7v@NWJ&WrzqBeQ_Z=v zci-IB`ox=6s-KPRo*uEKGj?*9gXsWRJ2ec5PER6{!Yeq!~L7 zVsU;&<)d`JOom5;$05S(a43G`OY657uK9__qlqFKpD7b%jb@ZV+C&6AIclS4(b17M zz`W}bw%D!DfMPH^iXC2maarOtyL{k~n4;xooUIGBY?VBZ!hjEu5IAksI=x1s+Fh`y zFnaHRdA6oM=IA7?1d8u-nnsXhT-lhfLP=QV|8^Sddr-$Ka^BW!=eEI%jHa?->0JN%Sv=zNYk4G3v*4Oz-hsS}7}|{%>aDZX zsq8yFlt*tT@E01HGd@vibn~xCvS<>RpRd)2UPiGD78!*OK9(NSXXnGjqX&0&W!B}M z(jM}wo$t~dWx~)< zt7MaOYt7|Th>GsHyHB3;b5m)D=>TJVp&_P7>lx(^Dh8MtRzv*Gox=jF9J&Lq(a$XU zLW>(;4W#VOM&WQk)@IFnGv83rE=xgW+gRH~4Wmp8L|K;)E0vBv`-ASO6Dn1I z{-B}`qA8`*?!L1E=wQblP4$DJ-o##C0y0#8yVZ)7sOd#^ppj?1OO1;{$q1_eTU2re zWh0gLV5S$gKxADy@f}x_MjHq@`8>tLjP~j>Rv7kjVGWuy*o@H4B}duHAt`w~Wf3#p z_ZY_Ul07c;M83d|F_AHsME)rJJ@k}aBrW>zSmLYm~z-=)j(-qUa!E0Tl&(1%96;vh12T$GHOq) zy0}&&u`1|>+H1S*I+A3B-eKRJ`IG&CFms8tHf0i-kz^>*d{Ho9yK^Pm#S?fg?aH4^ zU@HIZMa{D#icNnUC>>-KsVA=clTvo26|^jfiMs;hF@_OI(Ta$fx1Y};ID*Fh&3=OVl+U%E@l`_YE-47Or&a$dS;H9^b?i*V<`xv262 z71@RQ%13FV&i(A4ohpPpKeF_Y6l=3@YPgJ2nO-$A@UgYDLhS?h_7B0-6mOx>{i@BO!Ld(RiA zAE)-j8+$nAYuU{mQPlv0fTeh?d`Xt_tCN(yFO_Us>x&+`3A}3$>aTb-teG^$8VzQl z{Y9PwL>7l;=EUK!=9@`(*NEq2M!R z`m%D{mNTHXWtev$x7=?ct3!!9avt=!cVgKsYa;mC`HRxL#-@7~0$#)CVt2aPPErTp zIB(2w7O#0^8@Bf-)NIq;{!IvrHG!Q>pS3BIh+Z0Z4t96wUiABqW-I+fsoS-}#f@s^v}kr&UzVs|Tl((xORK z=HlVe!Dgj6hke9}sUh{q3>E`eNFa2hCn-Ad!=v-!hYLpumuz|H=fI&u%d)rRC}p|S zJ~T6Rz8azuON=;5&sp_r@sb`pICCzx4j2t4OqB+H<6#w$Fqx+BsOdL-TIFl=CNba{ zZ?q2%7bWz(=Xy;k)OQECui!>|(o2nwGM$tW6}DciB=D;l0fQMXt8B1fEz08&Az-MZ zn&uSc=JKMDO|mpZEtehb(c{gdsz=M&j+=+E!n8MXUOJB+Vg13RPh@E)+eWP6Yy&*O z#@Hv8lxf#?lT30e+Z)D(hbbzltD;?M8a9XKJJ}!)>Nft16GY6FaQ}m+4l_-kD>$+* zn;!VnzxS#2-=@%)fTc|=?bzDA=dH?o@8bz}nO##0hSSx6H_z>{>rbHoI?pW$%say; zG4Bcq_}y3C6ljSOM3sz|i_v7l-%yJVA8v^_SOBF?VdMD3xI9Coi*0X8%A;B1N+*#| z@kNV!%6hEmW@t@nhCBLPq9F|jM26Iru$MLQu8ZvhOs?u)c%LX5klY6!B={*@Q zonRnDrn40O<4_wl(6{rW*ue=;T$jrx%V)o-*PNPn35bbj*A++WqEAJL7WBQEM|wx3 zh&(6bPDkRN>*C63rmDWS##efN2OjqRP(X{iK&5lSs7Rsr4roP$rB$V8PulsEzjqusIYLhzKVpg-4KGsm5nI0QfEAe* zGZhgdli*I`dKF(+hSuI>`ZBO-6bXu*(X}UaN~y8%B7e*!Y;8sMyf~rLYIw072LV$_ z<<6?&U+I`RV~?4x?3+={yhS3r)CQFcW<77C4!_E898YBOw<_uDLea)QXk0E{EzF0j zF^WJ>WdL3BI(GX9Hd`xxeup>?lJ|1S-*p*wi4`&#bmYmKzZfHOws_{Tiof$~%u8}x zyy7p39s(9{fJK$uGBQo%yVl|9Ft=GuN5D+O0D;%MnTrAIS1mY_|{W8&+i34R_e zkcI^B*fQH4D#vTP5{M$AZo9uEyh!|Q(6D3YC|$ek_6`eR2tdr!4)}R~$9E^+o<5hO za_9Uak=)E#?vRwtw;&Y{^<*G}{c|m4WN9^Hj>p`+wYTAVXRZ)@wDE*OzY0-Hh;gE2 zMO|*tyz#!=RHs_o4tza!sY?g!(Z!r#gO5t{JS46=YH;o}ClaZ!oP360{KBzBI7bZ3 z7Rw5G%UyAt^Rz~^M(OlW>v&F_$0s&<)Me#9B2)HSJ~&beS`@llq4CC<6W(MW3w9n}IYs)( zVP)+s&N6Rztj}bY6-qsRewOy$J>u!Fq^)Z7@gxs4Z z^u=o`%Q86(yql553|8T1Evs0ij;Sop&D?f7?d!=0^Zw1T4Sx2wZOVVCg*{-NGwE$f zrGPwa2V#UEM%#?OC>m(;&X&rM;XSkWXhRR+*meyUt;ESk@Qtw9PBga4T_=S}h=Iyg zM3^~#&_27$C_RI1wFm*o%tHxIFpjzk1s&ZK_jOMg-Y^{Oi{?VD_)U-Q?G6=5zR9Sl zG3%r^UG`2DHVf}pxpEGIxtcoKv-xuQY?v<6|44cO<2uB|R$&!PqKloEWQ zz7yGSCg^xfrKOl^EyJsjBi_}Q6bt!`I&QXl=g+%@g`qseoNvlA$C?ICm6~j9iel21&q)G9;)x8p(^OOjj-~m#U#>?HygOW=res03RZl zIXM(U?2eu3=S$pEP43^uym)sFIwJWCdu|K(BM~h~hSUMo5-_Qwl|2#eoXN}S=2S7b zAZfFCbO%}5%bVw+pOJ4?EBv1x5!Z786p$G6F;85bi}}bbd?m7jw}v2`iP0GftZ(MV@|X1ykY%8 zF$$=}a{Jq9Fmxx=^Ubugc<8CwrwG@OK<2QN$J7UZW=L-3Lq71fyk4Fhm`^3yim1hQ z*4PYQFUc&~Vr{@PtreNCnKEi-VZ@cfW_)jf|AsFnY_iywA8|L>BO5mnzCH$CZN>za zR_p~zD8Eaq+*6FLP=n0o@h1nt&VoqHWR%a-q~LX`aBUh4JC%Rc_GSucXQ&V3(WBzkd>-b=sg=THhLkX{YGp=p9neat)M4V{ktLHKjbYV5% zb+et}d4V*>rlba~!@17no9G2gJPp5!>&kA7UB?oX!@H22mP;$MD+M!NqHmL5r0|5U z_|GVnq}~U%b*57)fLel~)&({mc?+eAv!_t&j+4VpG@+W8;@24F1YQVWfX9EG;tAjO z{8Q6_2{F6HTe+4%0_x8Kt^<{Qdh_^V3dY)+#sn^SVlvYERIba>gmIxY6XrKGL|%BK z?vKkN<@~hHIFEQjZ`^>Hiyj)Lme|H_Y_a1t&EUOy$0Bqj<%A*nbzA65E_ky0@^ux{ zw$0`Ye8N?gIWo0{K?CUw&@|c7c)$B1nS65aIOTtW`6$45T57~}z%C%{|A)6&vR>siUJ2A^8L`b<)$)qRp; zq@?s>OC>(^28BsW7JQ^6;VEtQd0Kp6Q1QZ}kul1t7ESd5{D2A*mIuG~dFnj{e=1h} z03ZqVtuLa#Y>PMV2M(Z3I$~*;`&%O#$E<1DN5rZ&^bbz2jmTMI&~e?}=lq919t=v5 zzdR?KX>D2|_NeG5OVwZn>Mx(d)+14I8AER@ok}`v5eJ1Km+pu zbQ$hA`9@2_r|G3~#QnZ7%JYVTgb&Ei+r2&)@gkz6Vf>?j@&~jr^CW!tUneZOKj??n zB=}XKam3m`ZPRK4)u$R}_H#0uG=*ZW0TtQB@Z*~eL9in9kec(>>0~QI4vb$~B)|ed z>C*#hCPoBUg}X&KxP(OBiKey=WDR6!(nTPT&yQR~BMnk!(l1$PKv?SYZWov4kejIt zISc(@m4jlQKZ`W-WQr4FO5hn7|gRl?|$){ zNFrhZ>>nJ6;=^-E2qa&c-c)1a;lRW^0_}6t^#(skd^1jET`aAJ8eA4$TZQ7uK?V$P zJ(Eo{WzaY;z3x=f{WKk1N35ln*jFs;y6Ga%&pMbqy3zkA=RHsZkihsi)pQ3u3N(GK z3AI#&jT}MT!)1Vls;269<9q#;S5^@FURY;N(9~j zEH;EMY94VX9CIoznovKl<_oZfIIk9>c>Wq50C$Pq;(I6S;rr)+7lkGpVmQ=|XxPt) zhB^t`qI6L#7Xv<@*;PQ_c`||nnX@PxYmSYqdsebPMh=UAFpy6-V<_Zz<4H*_6h!NMH^1!OxCViNz!K7F0#_$J@Q}+tV{5#pWr}PLKdYN z5;MCwA`yx=ToR9JtD-ftR!HgBVV&L%GZzL4j7S{#DFV*A?{To2TbZ*8aV;6%^ij;;_p-OTOY*ejdgHFff>L zq_-E#`~G>u{O^a~)PRNmEfO%liSSz${8k0OQSciDzftfT1;0`78wI~n@EZlcQSciD zzftfT1;0`78wLMgp`Z}gwGR4hMf`uHFnA2v^9p)QQnxK*-{^jRu+p(~ z9w)GNx-O@}%yB_=INv@3d2P@ea`=Bwuoq+z7THKWrZ!LF``-saN-8<6q-i0Govhh! ze+mZ==Gm@K3(@wLCvVS8Ut{HvpH}Ym#m4ID%7)8A6Hj7-BJB75l!<~@;R2c8o?0wH zJ$vp%UP-HlF5Ro80OLReWR0&I8yY++vdHd9{adL3ufo>7;F2Ax?T2H)8*-XJ_=?yV z=VaR&n!Elprx||l$gy?kQ-!xI|5~Q>QXtEv-0g49&1)5#AlnY1P>8{>Q-rF5dJ_nPc z5)>G_xp;YbHTrL7_WoSrfAvr!O5znDXF(!Mdugl};~i_SYs^KUR`)RWR`@_#dem@% zI?E?sR8#MWEL}bFi2gk*|0UxKF2)w;|9Vy5Cl5uvVO;EOk>vF*{3r)k;+KI9oEEeO zAfp;D%FkMex!1hBE)e>+O!?Oj>lGqwuENICzhbOvK;@mbW;>(t3mhOy-jMkr2>M`B zGy>WLcG1NWSG0G8|N8pB{TBEM^5toZZaA~)IzRG_xrBPr6H7#lCU#d-x+CT%0F1y$ zutNA8M;W4vtBZO^X++nz|M@?BLQepe&D7Ln4bp;89=M8YGo+vX84 z+phDB%tuPU{$GDM2zCR-^Fxncz|K0QU!d&n^{MptmA#;2>;?-7y2tqWSE?BFg#RzA z1@>zVyMg3cCgTNWaa|9rZ%|}sle%5*9dHMHv!-Ra&a>kkC=}}I;xch}R);n4`A^{f zR}Zc_Bya&I%*j+^g=iVINJFo-3Q}HuJ8H-ZEHD&z_ z_@wr=r0D-73BIkt5C~+6vC&F;{|TJE_kcip%l6I=(aO1JfS}OA1JXV%@6)-yN>m=M z&yW_?&lJ!DnKo;i#$-IlV12XiqRtrDI)sF-qOxF!G`}kl$4R{CG^l!k>^!2c1X9SF=r#?jde^h95Mq{h>dolqY`b&+)%yBE`!qL;cZcIghLwF7Y! zDp7B@@Qkd?3{dWdR8T4BPNZNxwI9;_U*iVs47)MmF-$$3`?zX^`b91yO_$l`ak^5( zv!n*};|xA+$V<0d`CPAU?F2OO&eoej=I!G|osFNo_^%!;O9Y^YJGss>JD})96oO+3 zTED;o9^!`R{LQRE2VB9v`?r?l8SFxTq^YmjydnIKM6cC@ojdq)?`}Pke*;E64}|NB zg;FtYZryMVl&!6;5RI+}zbHoV8p386*CT>M&x}W_#eCGQ&MoPOlcQiEh6?g)QtoedDTBGR;{R?9Kb8kfzh^eh>tTpI z&3JxK@aK~Cf)UL1WP&$MhK3}9If@Okyj3?!wOr7kR$nHgb_Gm=>1{ZFXaE^K6OC7- zgM+eX&qn4WAZc6w2^4yL@aQ!~%>{osLVDx~!-2JkXie7gbqg#4YZZb?*{|bfmlq8J z7i|ox7hbvr-%|Px*aQLOz9?^gP0KxeXoB#W;y-TBoixJYlFf8oh2dT%<}V9#y#JxD z?vc|yMs*H2hXIXtb>ooe{XD5lwK0;X7~l6*_5dtH%%N;*Q(9UY9-Bc${U4z>B)tfQ zil7l3{K9!XIC$o`oDRJ>hA3|6kp_iP<7W&~PqnHDZ6qSPgwmB;G1TLRuX%ZEKBRr( zFKNFxR~2Y{-|fHoAQ77f?94DD)OgKX|61_Y@I5<~cgS0Q0uY!*@Tq{{Q-*7A)Y6=UDYV#7Oe5zY$OZ z&&oKCk}U$(atX)o1y<$`SF|VJK-@&^k;OGz-z6_x7<%)?&dO9spTrN-w2HI7(waVv zl|z}jR?>fn0$A5NPB*@wx?gzn&&X-zf~Vw1oIc0xnNVTuL;(gQ^lVAhDJ&wV!s%Kms#Ofm zFRo|X1iSzI-Cy;f1rA;@r_a+|8c-}xU7n$*K}NNYOo6irxV)McI1HumAO5go{fZ{OALzu`3uY%mHCzE!QOeQE~C8vgM9$%baYLStN0 z5-H0Y22Ivyu544J?5H#k{_S^f2{t4`lDsae{l50uFMoQ4Db7Df$$u|`!;gR?8vL9p z{$v;fO1X-&FZ=R z`-ERP_m3a^zz_jB&Ahnrkok+Kt+oi(*4CnQlBo?RA2j7q#F@TB6oTN+Z=0tuL53FC~(h$$X==a~>DtfMm z2^V;eEHfbd@F<_D7n0-&Oz~FY_>R|~0RM*vJ#_G8+PZ1ja^x?V7_tFhdE?6cFn6ua z$e!%co2wLGRd&!Fqc`~J`jS5EVjni{n2$1-sQn{p{!(FO+OTyyTKsS7CSo;md1Ae2 z`cPLm;z&a&l;Df}lzcz`)IMrK0%208s*&%cf7n)VJUk+cZJv1)T+pqXNql(OCtaP$ zq=1N?Y{Vr#+xH0f-4C<%Z*X@hl2&4Nvx0W>nIZymd=alR7_F9Jf>7i)co6rKn2bE& zzON6}gT9+wfg5;dsA|@lR22|zCp;idm18@dRb_KE-N}CT57he?-T`Kz*_&zeL2KJD zRI$wR#fP;ttStDBmLfqtTe!epY9!b|H|X#r#A?UbahM~=_Lmp_c|*V@nhd)01UxJ= z&;E(2Tz65O7Kg?R_v8Z$d>TykucNs!I7!dxLS254^B4I2%coiu;q8mPfjeJb{UQNB z_qwMa0XVI5_TOxi1o>KOtUtB$3|38WmAL-Pul!%xZPi7v_wwf7Z9IoGT>l7k{{Bu{ zOsgIe`oq?pogKRrzUsEPb~i5v2Z!nZiW+cW2n071$>#ifwEr)A1P8$S5pgJ^*BuTi zq%@C0UWB)Mrs}eBSw55__$T81?e#@Xa5CY^ZTt%wKWQwGR@b_SgdWpU{r3Md>|g%pjQ}D#A^4w` zt5DMr&4gsMFM@V9@Hq_R+0c6N5L88sWHpX%hVb(figLmzV}yEZg5P`N>s zOY!-1CKl5J_Ft3z#el!9^XG4C(#Z4A#uFp*&iD2fvi^fGJVddaZB0MuL+<><<7H-f zzSDAZ*8`NK>>Xe!Kva(h9oqJV43<@c+~)tAB`+b^H+eCPOm(U% zBLKRxi~?*abZVhlpS9NpD2_#Zu%(0OLgNP7pWFg}1nkC#`-2LLK$3W=2f!;QPU8Ec z{^R509su$^Rj>a;OsW?HJC@FIzP&IzOR!wWg|oXRh70tyhP&^MZ5K{mdnQpdNX`bk zCtH*)@n}C;-M%?aK?18IROqA_&$+Q!xZj$uvJF^_)0rvJFiNNS!9(_|%yjraK4m0E>A-fKjcSu_T zv00SD+cK9^ZBi?HMw(&ol$NR0(Z*+o--4`U`%Aj!Tmmj;Qp^vb1B@l`Kl0(XVgg<#ZsxAjCJ0h%#X=(M=6=>AXo>`&2a zEkoGUFfHj1RSfpiYM327oRKc{^pt)?AN5tlNm}+%fnORDSPbC+F4;s~Y@VOXLBz(G z*J@r&UqmDp4F(pUB*JS|r+}%qx#46cv#|vI;t+LR=0G-Fjz(y^aJ!b9$$Grar7J4R z$v%3T&Lg>KsjI0+*rr1hGrgTENZLsH*dF7ED;ydju=~xTeU`ti% zh+~_iEiNB1C)AcU(Wr;FOJ;qzKGR%wE>W($XQ5h7Kmo$%P&Vp8?p`9WydJLB!gzn9 zg)|mlLe$uDd~;$)IGH2bvu46@TpTQX>~F&WSvOU@x#Af-MA%aro`8B?pxEoS>}c)m z%wI#hICkAEg9OHNuK+8+8Dv|@uO4fD<$frA++sK{k~4E`eBke6sd*j}rw)%uFFzBc z{Po&9_k{mA?sR#uOM`l!{xCW!z-P zpDTCJIjomul;tc`T6F7m((%3?+!Dh&1V) zh)C}(^rCc-gdRdDN{b|f03it^guG#&d;aI%eco^RlH~cVXRbNt7;~&OXrz&mVZts` z36_$kJ*Y9daPDP$x#LiN{r-#da{6{(ohDbwgNo+;ag`SWFXh%_=Sj}EejB4w!-yy` z&#=O~%pn0#sb%9lLd&1);9vLNE?s@i_n9_jyU>F5Fr^u??u50=VQY49Zzj)YnKQP7 zE~Ug)KJ{+O3uR0bKmU9u0?~e0)ekPTh||YUN7#~m@TiV0r*n-V-`?7__6-k>`u}>K zQgrPgqJ@?lJMue%3_V=zs63f9dw;)ejt=~2fNB18;c>(F_Tn5b)sw)TKMqErjWxCn zk6`E~;<>Ms)vtBs;=r$HUrw8;jIP47b0~a_VG&I5wX=WCV#Z}H$$w{se~0q1@R6$n z3eDZwjl8P|K{jdGt3N=HOV+%9j6BNoL>dmKWxl>_|KyQwIWhI3cO4s4VC1T?KoT}w4Ns0>P`KzCfIBEy~>6- zZa%`qGNqNaF6rb!d9R$mK*zV@ZF7MY1Od@S1km0nrj7@}zTavW`5KWFG*7>l$#K0F z8tbXAe8wzh_K9D9;mLI;NWm-wp`$NVze#Lh4kbe)1e{{m%gCd~Y16oPK!MX<{S z#Br1Vu5{+@>abye7E>4XHN+P{l;)O`J+w58#uku&x3t-s3AUDZ%h(rJDv7pAJ!8*p z5Xx~HC9qIReZ`t-PdsUAbXmxHf z{}FlPbz3>wkahg!d}-3Te&mO#_gQ9TB8EGa#~-JVr-TV9?2IDej>@W+0fMYqh}VDr zU9{`IKZG}Q<9#ZVKA_?JlK+cK`**~uFP;`auVS)kmnxY=hxYl=c4fDJtv`oD^vYf{ z7I|VP9mpV-kOek)#rIr=AyqD-%h1AM3O%%NTN2YLinX6sI2oB*b3#;Njj_V~Y*gfE z60w>zkI&VWpUY@iR2?~dxvaG1Jqwk>nTE#d?KE?azsZY)B*TU128G+*H&A=@5B2M9 zx@MurF~Jc)d)sCP5v`co>+Zf&OnZ6)c<7ZyqIYP9&z^49$q+-mzZ%uGc6P;2ZD{)` zu9f=wphiZ`wXRae-M8E@^er{S*0n(G^JjRsJZ>!OLA{mCt76r&2m}JixBrNN@b{9% z2bh?bnH7zr#mAAaugfPl@Uh6u;O>eu1-}(q4dnbSZNDgcC}ap=^JO`ZFw>l9E+53=GsKEk8#M`n?3whbJiSgAH8UnZE28Um^N8$L|NQ*?L|#`x zn+K=P%s?@ie`)iF20-cz(X}C`Ov^=y+^7>F^~W#5X?_;IOXo=Txz||2JooNDo!#I4 z8|S_1v+~6AEEJ5n=@KNmj&PNcG9J_w@{!{2CHfv(Z1_wa20V#oOd0 zF1&$m6Kht>vy1a<^)sUaKziu;H^GNq#=qOecmJ<2ega>9;t%u_^+Kxw?+#wn>0 zP>wf>4>JobwS81MT$rqaGw37*BtZB18CTD&1+|wF+s;UgJdfMH=vFA8InP=oFk5|! zfhS`WP2EOOc^a5pqObUH!`@dJ+dFgFUOyU(~v+9?> z)|hr!4+YN(Pk3VEEPJn@S;Ny|wN;rzDZpJ%_gm-w50+wZt`(Px31&$I^G9mpjjw`N ztp};{2c3iagS7W2Enc~uaD!lqgUqq+u!J0!mVxBPjzhr-?>wDt!t+RcfaQml6dew03MJ;h#KmqbtQ%ShGUCem!ppP~Cb8q8L_LsR(c;7?izU@`R`d zJPfnt%Jh6}=bipm{FRF9sA5ziutM+dF7DLYp>*zn@4$Y&blp!liSjDdKPy=P$OEj< zpU$fAZS4S$eeHlahK)^uGlmxz$=}NHNr*D1wZ)&30$Ck@e3}y*UCL%tE zmp-l=oQ#G?`7Q9p{ckq=O8nx6oE*&yeEJ`apQ4>~o@GbN#_ zA9jOpSFB(iB~wT|HkiVAGgk*l)!;9F+32vMovdkyVMZ>{8Sr}zm{layHwwA zI}ImjhUM!vuTd|RS01Mu63Z#7+0AZr{SNWT_!9_VNId=A98s+dxBZ>IK>8b1nhHHf z{Ti&<_8tr@?(Ykv4T~8yGFq%%$=ceDJzkPGPh{lW`{f^)wyrq zd)TqBK0-gWh3DIq99Ed`7c~y$KCWx7uhI(HYrGp^FoPXrQejAB2IldyzJa zSggmf39qcnO?HbOUO5QPpR+ULl>5{`d?9dhR546%T)@RD{@gzxp=Yx?jCF3Iz`h&K zu;mvE##~3x_qkf}Yo|#al3;HZVV^p@x10&>1Sxs${NajiE+;UDW zIwbtG@jDY2su*#8hFGbpufVggi`^5Q&SV*iwfyVOVWjBNMgAwz6lhzpj&`Fbs~6Liwf)8(oN{rw6EgKW7J#wpG!1I& zQOG15bW+iblJ~`A1##l(5 zs%4^g&N24YIpD4@?_6B zz1M18WWHXu4AWK9E5XZg2HX02FuFA+KF+HD;YRQnzK$2ld9u&H$bL;4D-mN^We3TE zZnthu$w8Si#0=v)@a0v`H7(5`kg9fzo7?#DgIAWBx*3pAo3fUUN}`*Qb#B>HjD>Sm zuC`Qn8@#47{pP$@lJUHxjle|Uy^$u~jQK-h^|%R`Lp37)R;bmda=GO;6lSl!V{|ht zD>`uPa{Bd-9G8s#aRFFlJ$}lO(DqMy*b}NgKO&DlBW5%VP-bM*VKv}t3w7PjzW|z1ZbbijNQ*O$1BH0b4nBeE-HOfCdD&-u zqJeuz8usv!d;&;9do>`?E*qZc^+7uS#`r~5&Dk*Vls|Jw8q3$I`RL6hBC%ORMG(?% zYa(IyOV6mXqO&Q1HbQlW=n(h+tlo~eqi=al1WWKKGe>b0rM$4iKZI7`N*De^bQ~2T z`)enFEW&6u7{Sq$t_6N1Z`OG}6+|eu+n@W$l1DJL4m!dm=JDzzC14KiR&%G7{<$+b zSR7ah92rsBd+fa04e4F*F%6r5%XU*W0n^Q6wNjR*2gwW`M1C9Kr+BMKT%FloS$rZc zPeku1V_XfNEp$dHRsuF;^Cq`7hz+PhMKx1|W>|Na%L2B4&HH;WXP~Wb86)t{XT@3o zxoFMXjLVW-UrjS8n3L?q+^(TMaKd!pEVxj}uHVJcdz+-wo(2EpnDdiMaEnjuv9nc( z%!MC!=8G)G^)!u8>i~HRh0LYbn(2n%jb|4NrLXS3n&TYGve5crNRT6K@-2euX# zjJPECZq2Wx2Z?r;$)Z?-L9{%ID?i~ky61*>U4F~|8c8vls&K&7Vn2Px!V#yBR|hXI z2ArLGk+1aa?9tk8wW*V2`jKID!4hbL;~YV~ZluCvyA$aR|IqNP6ce0`)x)z}1Q?|a zDhB43q#?i%sVYgA7M)Bvc!89+Sa_fWj>sAXGvPeq+ZmF#L9l!Ftu`A+iRn~eTqC%6 zxw7G!ee8`QTv_Mu>3OF==XPQh#hZx?4mu$)Fi?MI^6TxO_pQNg+x&ZRKM3ur_1t8Ib6^YecQld;HmN7QKejZ^N6 zk~d*Ptut-iullRt^Bx01cylC&h(c&pVIMt%aukhD>^J| z#x{9-TKm{hiGO>7OfvFjyI6BMdNrLygK|&k%^pfJH%$TPuTC!{Yr;?OAykExV>A8(Y_P#!@*|VsRu*UOqp;2Yv1UKo&JAUJw^owxWK>^ta_=E}{Y=T#0dxi_SFbi#0Xdxm9a3n7 zt9V`WKrQub7C*@8)Y-hnu%h~BXTrcurBV-ILI-8|Rjyb23#%auUliZ7%vg>cMF3-i z<`k1eo1yOapDeS9PgsYw|5NBy?Y~pgswbX|cRC;tSyBO`hbRM!m?39&rq8f{s zCaZ><%7dS?X&kM~ms9R23)JXevtZ$~z=H-+wb|?dwMmkGn6==d_nA=Joz|kVMjOBJ z^fU`(yd-V9b}cF}*G+A^%Chg8$MqBYO1p*K7y%zv0UL7{oh|$GJ0|I=Ih*v*BZU+2FMC(ew5*^L&m>=XGDtbz zucA(&**q``YY9gTQu($qel zdge692>aNx+#wrXH<|_X@Zfl^@%LEk|Iu4rpHBO-e1Z9`|7_+U zD=tp8`URDhv{)bI$}a`#Btf7qQCT9(&J17y3-7O8^csCLWg$b02fJQ!pLo<(a@>^B zZ}f*&xQ2SCdj^_@igs4r85S`kDwW}xr`W#}XPva7b>DRLoqd^Q(ciyp?0SP*0*?w# z*kcEEVBy(mjq%a5rY;MvJ7Fpnt@=oaMhWjZEo7x(A7!&TA%0L-@^GqdXiCvfp+JY4 z(K&?o^R5%SNNW2Q{)jwXNKr=*cSkCBc9H`SX~1JaseF+(CoRC{CoIxn997zN$3kEf z(;TCIO>3p`VGA~`mf6c{)Ms-M7w;+#y!nbTSJ7HZ0l0b;al#EcW_bDS;ssAR_$OL; zL(=Ebja6%+)&R3Qo0N5{)R{{~*3SmtixFYNTdqJT+=4KJXmiw+{Ga1za>}xnYb_g<|B0oT z*#;D{6&uab{iZ%*9ufTL(oDSpKOfD%b&{;7Yw@49i}m3vQdvGrWv8k^+g#TD+Kb+VY{bE1$Ug=x8SX%b^gI{ddbVwK5c1-fpBj zZIztZL21SpL4q459vs@4hG8gRztp|Rj59>W*zAFy(|aX7$GDw<;gh?8>!V@W4_J#> zYYF%d=gVJ@J_pXKj5g)2n(FH%k2+vgU%ak9# z^1NNW7K?GFg%BPHYJDDcxiv}!ET%Q!TuFVbqO2he^&Vcy(5!=GLzrIYdH}zwDsRd$??g^&~ zrYI%7KAD_vs?;Rfh{2)V?4bZ?8oc&Zzu!Y>0)(+e{e}@MDZM-LaI+}1M1*>IobxkT z(ZT*6WD{<+UMuTwY4v|91yqv{ZH>vElLyMWx)PbIuI^w)j)eZe{nx)}+Iq*Q(ZUQ%OE2rt51}od&U++wKnjugHA@%2ht2qj)(vaX7(o z6)hTv6p-j+Mcn}eq#p+6|5W?g1`FNV?5(P~>GB9W;q_~@ovl2rVQEq;ab1no8Ay@y z@whDmBWM(WXPFl^0U5^!Ih5GM%9$ZrFa&1X&VDIOur zJVD1e=!&jN!CR!S^abX#^7?OPooz-q?>Zs%rD6pFixhwEOt1)`VYgRXJSQ_c^jJ2` z+CW$&dN&EUgsUAgXEKZmttsEbe#$Z+G;{<6O@8+*I>ybehl;!p%HDQpO_ME6M*z4tu*YaI*~C1=OmJ#Tb-inKBAo> zN?q;8XivFUkBYn>2{Ob6==PUj=b5GAcEZs3h$x%TS@B5!kZs9p-jsm{WJKlhiTi3* zoHeR5ZljQv>DBK$klF{zueTQZXstrnu3OAW#Ic-R@h6nscaP4nI8an-D>#Q?#aycfu>5zt2wl4;!_r933|7-w=0R#D~|P+Uqsn z?@ZP;Ld+t#(Csj$E%LISE!mw0a{-A@IOVs1VJ?ATjHQF0m7etuiy|*bA=5Sf0p>57 z+xsp!XA8I8cqJoS_{S}tHMAvQaw){Gtj}UsQAg)##^HP^*HXy6&xAP!Q5dFFL{-YF zW`TXRSIN;Rv2#mpUVs};luI#X^l7%nc<9vq^&s=?H)ik%xV4f<#EdlpT(U`0v`*Wb z%;ooyYX>S5t1Zzv0iK|d!1<{g{)gBFR%Qw5n}-f#cn%-wFv`i&v9Gflz%E^4M>H>1_AX>wJ5Rems7TV+9?hd$u%qlV9GhiwlI zb#U@mu_99;y`FRIMT4yAh`>Y`I>4wZ%vmw4+-sOJr<#fo!Q*}=x^hnFDE5Ort;rm2 zuaC4ADwm(3rk~(wSE32QOZ0ZJpp|{!sC^NxmLVCT#l@@Nh-ft2H^mP`-|u7V9AE+d z70;`9DdgwQqDzsoW#5-PVY4N4z}rSFZqaZ7LYPhqUTvpS7Elv*)8Re4#uG;0LLw)O znD1$@$Ue~)+eX!|E+i7n-S#GndV`*$*2_}ZLJ}>w2L$j=8xf^?46g1g&=@zST&669 zc;GDNXh$H$w~i=h5{DMcfx=icC_l#TDD9QUGGEQ78E*m*=LKLEi0i5UIwJI+OiN+B zyy_w$8}x-?TE*{bEmQPUs=uwbd5{)EI2w+-GytMvn(PqPsk2e9l%!jyB&j&wjm9F* zmTBpESIbXabqvSOx=;H_cowKiSyyi2)6h!yu>1TOM>@b6j&RSbW%Tuj5g%$dB1M8x zRU&Y+P<3rcE#>kdEqRsflqv|UBD{?Vo$R)V>i@evlpj_JNPvPVJ z7P46MJcq*9R~siz3N~tXIq&dmR7&3B28&eiZ#`W6d%d5$wZ1VtS?KoP=S4uHqv+ZD zPA5$QJ)61S8m82p`MRw@|yyl0@hxv+vOXLv@W^zU?-5WFv>g=7_}T0(8xW;#@rB zDli9|M05>NTPw8{T#U%cdEa=R&=F;Kug93*dEq5olCE7}iv(A->jpqfg>mb*1Lr8wp|eJ0zTvL!pIBki*sggmEC4dyB=1OXGHRm zG++CY*V+*YVyUZOs|4^Avu^!qwC? zj-V5KLDVyE;he~}7^`?~?m;H7`iXR0u-W#F^U1ytms$PsMVDu)fr&i}7`wcTgA?$q z=HCqNRcuQ2f(QzfutG&2``V_8yHX7;vPtc<3P?Khz2~JG(UNiwpC+CLHp_2avl#T7 z?lnNf_#Q*P49#xjawF7N@TG2z!sdO}h_QMEba{RP1(}<^qD^e31 z^}SA4z}Z6EyN~e#sHkScU)w(v7qOKOEDA!>HPS4YWe%$29 z`eE187K3jY;G&%^eK$6<5%Pir2CcTzIE)-okA+|8d(DYTI-O>NbYOtjyvl1g?|rW_N*qz5l7{hZ7c>{u!YNl0 z>>lhRu4s|?u?uQtQvSJBsh7f>Zk8R$1O<+lwC=Ms9Zaw=-79;pFR7J7HY6v3qK$CR zabuCh(xK1mAq$Fg`it&go`|sG`?+_pT%%Rr#N1j_R_Z=M(>))p(XQ-PyFwg%_qRrz z=xPcxo&kKk(X1s;V|VR9HZA5;6_2b~c|Lvj0eG|~m#x+c!QIMUBlWl%J~g=c2t@cQ zvG!;O=KwNeDgSI3#4${Zn9arECb4&oIbB)6Ko)@6wS>Qs*88rNpxIWonjTlNUoc+% zq>j*_R0YD>m6mxbS3e$Jr&Crx7?^DS$aNSlgvJZu;oB6KLcA31cBrqdYPJBu{gA9l zcN9%>!alu!ug{)Gh&*V2zU&KQBbmQdhei;)32Gx(TcFg?^McSOEon5@CCZAP_`nKQVYMNVxeyG(R%=OD!bWk`q8FK#8REbxvXVyLc z=p}AUT+~Es8r~j`LtCiNg3nDxDoyNP(IP5v#?Q)&-zHu8q+~UKnY1BlVb+v>)pyHJ zEO_!GV~j7vkSf2s(u)ITE{3bQCn&}eN_!P$D7Tlk=z6LyWyQ~!->R5vJQ;@N_u7}4 zXN$IGvn~t5DJKFCwrCo3{WjLn_V*hiKubou-!Ts?j=QjS_;4rPW|+8Ev-b!r&K(xV zkxciCDQo|4pmb$mzph=UgTVSl*Uzg@{3Gj^oWBwnfm+6Bvs)>)Kc{Z`UR$-iACGA? z&LQ9n1rV`pBHDGm0#sbL^YPQSwmx>XfXyXPUy`1Fnn-I@Enp-|9mcuXXWP ziFCMBtdxzi>F8ivrdp+v!la~ytXf-p9fW+Cx(H2!Y z@9>WnQKf?OX0}-7qGFmhGEMgNU0O^W!lDth13Ls|pk?Js!**WgD11alf?|@Ol*!zY z=AT+gobu8q15Mww-0L$)g#O7sbcu`F*Eanep|-f?@ce{ZE8(vug7~;v@orH$JA#qnQCZ2*$wj4-Y;W37eG67D(Q%l=(tu) zQ!h-h>!w2Q__Z6+5>*T5)X<7kZMbYZ?p<%Cb@V>Ti!TKVo>v++aiLAq zB-Tp1ZbcbwPUw(O(Z4L51;Rs~#5rrt+&7qBk9da-!PUG-@Oo5W&AVj4P_vhwd~It7 z9lBjp8ovr!;t>Qh-=PHWl;I2ks37t)7ktvfHLdU?6m=rpGs}aM2JS20q~x;TnkeE^ z+M#9moj>B<(0#P7a@yra-StfkO$GRlcT&d(@{j3Z47tOHmSj{^#x=p!i|c11=8*6x z)vBvX6I$MfyeAsQo;}K~Cq2^REqkmlsH)0 z5{=NT5^nwWLq{JZXGpXqbI{RI$=gJPW~|-Hm%}9 z*vO4QdPeAuZJ|MBV9pBNr!d+=vaFtXGT5S05zD(m@0?Oq8wO`P?elyqp^-FMhiWP` z_7XoKS`3Za7kc`!Autv*iy9uDXz)+yZ(g8^3Ouc>x;R7tRnR^tyG{_^$+;b$QpN6< ztZT^V1w?f13*)9GwJMA3bqs$#xtexIKt3!S!228mbb`n;QyWL5YLrbQ? zvdZDw9n>!Hi|iK$R+zziErX`yy}Y}7tHJXw*(W;OECx2-U>;6u45y1atoljlWjES5 zPW86-lu|~Ppax7{aAfZ{Ri%Foio}7p0zUY+>n48pE3%-6nL8a%T3nP&q4V)dDCzBo ztsCxn4x4uHdN=55w#)g_lVlYb>OnDFkT-I%4j|t~rd!%M=co)TqF#=gFYWq5n#F#x zQ4`ApIeo~Lz=rUU|4RX?^Pb)b?W}aMF7&j4Kd5R6nmV_yjE2Jec>c6$tT`pf=Vw1v z(D|w8G98?hOkgNDtR};9uS;M;MnZBAa<#1xskvG7$+mhj7~``$c>r3_=QPen5Jc+A z>q`*u;ZG{wbM?A8`1b6;W{Yv}ABytYDigWGO#y1!)8irnTMF2N2Zd$5Y>?0rHpDR( zIN*0OWa2pPWguAtSC-~1iOhxEi5Ebmu~p(OUMP(5Xb+ew0-!HiN8c$4Us>J=6RX{F zYQydiJ{{+I>CDYKi4I6cctdoQ-`psHYgH}xR;VJ{Rvj$ z=PsN_`_cf~KD#=nkvke(WbrU1Gt#pjU&Es|CYiN{!bn}KQ^U;2Gn)Gx3Q{PB!_!!8a}?u;a<|*F zxdw9#2rvHyM3oGSNEmg)uP8`_6{iTn8zgrA)Tqxg28%f|VoOE!w7V0gp33-*(vF() ztIh17DkO{BxGGcNzUM5VXJbT!m|1SN+PWRzG&wj)=DtS}_QAYE*pC>Jy5r9HiGxFF zUw4$Vg1_gAJclGzY79O=GH?(cbZ;RUn;e^E0=<_p)_iSUXbSwKNYj=v{A-eMtB)) z8=9jhK3(B(_ zW&hNW)3$BPS)_(3-wG3oY|$g2!n$w41nb#rBf(j<#zpAiYpa5sjBK&{ zz#gNDiKq3iF>?qXcoMKPdy?WsipCTR9tFHlvVT$CHydCkM}3yE6(ot_Fl04_S0T0! z1W64^$Dt>Ig8e`HH3L&i%;IxwQ=E^z*X*`)azE6#b#L6Fu>JTYPvB)iZci&Ob+Z89 z@_6PzJVBZuI_F&6pEVfg_2FB$wxmK=y5*MQt7v@h;Rop9Wm}n6ZaM;jU+?8qXek)f!xDH&=jO0ncC!-;mxj}on(^MuF>ogYjDyt5juG2 zB&qH-U7nWdHSWx7^)k^M_#>!Fqdc!a-TK!b%UHMscObLF-{)KZSK|Moc0wUgQp>F+ z_mQ>9d(2!9SwTFYx-@PeMSrJE@5EwJqAtur^mrNgx@-NrriWLKlDwi|7H)JHkXsu| z9gQeVw|rM`!pzZ~3iZTh6l*Anh#j+ZilACqwi`Hd=fyU@-z4B0Rw#XT}?jV6E? z-R$??r2t}{#5w?g>GI%bR`m9`DfiG}Y*{7WHAlYwFnyB-unuXfBtvvje=*5m_;qxj zR%n=YKgrn=R3~QhvNW!-eI`rvn@yQ#RC6sputLPLN3$J!ea*Gc+Vo!8(?7SI#azP1CwdMnD$0HK=xia*?yS~c(w=M{d$aYq)*Egj?U){(xuLc{FAkC^~fB@p-%#^mA z1Gp4Uu)|)({l1vF7wskmnoeL?&5$4shD9Sgk6Kh`3+&>T4cE8qrxH^ND z4$nL+eu&xkBjqd$oaVCsJV|ToScVzys*;AKUb&{G2H(+@jS&R364U-P0v*r^8*Ni3 z!#y`(mTZnFxm>cS$0JJ~@*UXae;d<22OLNnJr?o6Ly?f*{u&TPv4Y`kX zJC`J`izd2ZtOP%>GA7owI?N#|f2t2=*+#-G(t;LKl^~4-6H`7XRjHtF*2mx{E4ggtjS zToj*yLzNDLllh#uRTT4oXIX@iuXy1WZyO?(KTKI#7Uy2%3Cam5BhDO2->t+y)B4At zeBUYsMX^Dlw54jNw#u4XZd4j!f13XGCya;jri z81k2CdMkJL)syX9xwC|?z4nQ0tLA)t4?D9>PTJMNtOYvtFRXxCpZ$zuvkAx?K+U8dbNAG9ZfiEP z7m)i@&KGt=n>RO!l8Cvc)Ne_HE{n)P8 z=xA>QIe6P!UvVSMF#(8P$&zc|&hEkmrIM6G#l~va=9Lby%j)%!Q?|YH$DXiC*%0Xs zG#rZj4t>$xDtTX&8l#dnpHKKOi(efZdVPeEE}VZ!-}WQ%wkvztRBOS-Qd{DZz90`7 zPNSsFBKTVKPzJIwM;cZ|(c+PbQ#Jvw8!o)Gf~L7;zugT`lX*a? z9||>TUdeRAby9bmIHmokiXN(nXg3%gP-K5C1BRr7%uVTP!mk>0ZIl%Q-+Pn%ZGv>{ zcW1IH7KQ_(B1~)8Vx@XK(vTl`3p+fhoA(F9419+fgB|;;r&L|+z_sVDWfZX}1OjFC zDMD^fjn=9>Nh?%?iB@L*HBC}l+P$)#W0IES?crOG8TUwi4e-tw#1&eGg@cpCFAI`u zw|!$zB(W5XfK&SV2*;9U)CE5_LZWd-q&5PDb@qm(eze6vrT zIEv=ld8MV1hgC7JYh&?A!}IoWWhpoP2a3;xyxflGekg5M-%FfZdylc}TBvY&bX_?a zh_%nq&0q`+mf_Dy;*Z(_G9>Ms6f^U{!&*Cv)ES!7Y4)RIP@(MR1liXBW#Fc8Mx&xP zQJ9(t4oeM!O!9JO|J`?W?O<&)xzFh}JmseKV zU38~rL3u?D@eFxh>*|%|TUG3;XXB86#HKIhvdgrZH7X*fUxv1_`^^pqPF&H+pgZk2 zxoFjnWaR+cHLSRIbp`sr6jcn>$Fkei+HUmY0cPEOovPyv^8YoYP&}_AR!%Ni-iF8w z?Z@y&2E#?pMt6V#_qoxNzy;I>=DwhGZx#U8FtGb&9@U>m$t**}4+ zXGF^gNcP6MI7cjbY4c{Cxck7Hk}g}gK^YIJv8(S9ZP19qXY1N9sm>w`W| zl2*@-<6r}DZYmlFaw-B`WoMo#Wol)1k)e+| ziskE!1BzJ6%|UXg_^ToFlQ%Gw-XWe)&fP2uqN^l@qrOWoqYOX6kZ4pmd;w!rbB_XfYUgPJ7_KO3Td$0&whoE=V5omGlB29Bw>4cY7#*|#t!Xe=JdCzGy;P#EbNZq1CH>P5!uO5B z*U$enzSQz|^xCOXKI7AQ!t#@JdCEe?8x{@1347T$$E)|0KR>f~wwtz=VE+|2B*i-t zlZ0Z?DOkP(Squ1JsPZDBCo8}b@6jM%&OhdOnHF2+G^Kgr_0r&@K5UkczW#%(dx_p} zZ_&RVU-K!B3NJrjMy_r~b6%YIG0OV*d1w}w{QMsw(WZ2z8>5l2Q1Ry_EZJ@H21}7U zJzpP7fBpZl>A56y9M3eQ&;qK`eRTCHs3E#mj^9mW)erZH{1~5#a4Zlz>H@#JvBkkpiLDw?k%PH$A5Y!C~}JLSa{r} z>c;DZ+}j1ooYUS0J&}e9dDHF7 zl8X^?*WB|L?#teR{(2~@5;U_Ky8P#bk%?qmZsiz(8DzrG>ich_$p2Vn#a{mCAF?v$ zs8c6Vtn^7HRdPx{?frEnhiGv7m^tZ->zyA*u5YfN-eAfTW{d2y#vx`^8wcql7Avt! zS707(i@nzyeB80ldbJ!^llFf}l2?B96r>zyQpmad!bvx@+k}(rlZZI(HrBU;V^awQ zU7Y>c;ySQ~{jmF$jp4wi?s$re*L*IWF;ki9*HT$|_9Pz=b#Pq~W$(d)NE}383?HC& zES9@0kIMa}YF8T`jNo(~JoxfsZk3abcMoQmZ3hY|^4H1H-KcMhNkctRM(@UaXvchb zZw~k>areZd$z$RwKet|YM@Moiyu73EBZU2N_Bbuc!dBR3bGA#z$(gV7kqZGz?xaG%)XBGfx~MncBX>g3*<$AF>(2=9 z0Fp7D2iYpFjR8{93u+ZM>zCiVFBeWvb2rarPOL=`)f^<6$~;uGc0yfC%TsFPhP7=8 z=}tQG9D;pJPk3aYzfSw`vc+7nc?FMjVFT(B?XdgTh#6ad8;2`T)3!Fu-GVC=Waycm z+HDB$QoGAec4B=YfB7v2rm=HI2Dr(GG`>Sf_*^1Bd?%b(5cfSg>7Y2~O^vDILXgns z@{F`{)xqr*=XGSS+_OS&cd8s^A07A)pj&Kjfa5(vYZ%0UnF6~V~QSG)fm zv$#{`l_qotqt+$)Yn5Ch9EwB2ceK_OMF0Bj|0{Sh(z>)a*fx zu4<=z6zWs+QlzYo!zEyp>}b-MreMO0MY*adlH;$XA$O?n2!nM4Pn9KZ-@4^*1v{Tg^d;#Kx$R{ zY;4uChaTAi$36rH1=X?0=(*j$m|~z2f^0BV3?qFOwQwBdcs=mRX!Xz>3W zarP)4|FP1yZrU&3N=V-sxrmgelJXzfNiVp8Rb>B+>mvHQI9^7(pH=UQdV?u3{_24I z%kSqst7p^Z)OG2Fu%C?&(XBJi_gG;U9u$jh6?on-73`2n#3#eYr_Hl&2xlQELH?l+ zBMvWePTDkJ{vcXbj(^#wN$bnw@4H()S&&R%I&x4nJ;4F7=ySK44{!=)OKTW%*G_D~ z$%R@#_Q$mwr-k}_)qbrSuKFg&%W5&D$%L5P81BpaG~j%4ftl zuFqm81U5}Um%kQq@H}@H-YTP-yWY}JHAo+_iWVUDm!ryz%Jk;$IIE=;B}tM>tPA>= zG}p8cT7hqgParGl8`ojOoudJh0_w85dk$`nRV&k1I|_%EqgZkjJ2M8#l%;>y73l%? zEQ5!lbyLSdzh6$g@6T$^_AGfEGcz1WvnkI!S{U&f+(S|VL) z*X#Nu3|TE|20z=8__9IW0sPF^rz%H9C;wCjnAS4dJ0r)eQmV>}b$z+;^H&C#%KIIy z2mN+==C}JpS@Pt{lSS`C*9Fzo`a@@^L*+j{nn(ch%6QLyFrqAG zw+uAwoHEYy^4GcZohUHNCWN`3+8C3LKKn9c{><-UW%y;a(0QMm^+KNx0e6es+S;@h zf(9MQX_!UEMXU=4wJH1x%Q*7PVdSKVu$iC|EiosRNFK?0E!Q+u^%!iEOdMI#?7MZR zBhY#n8!3Br{UD4n%U*AtNC?8>YT69SJMN8$=Ge$qlySiwh*Fc4orgi0&z?M2v6t-u zC%MS(g#Wk29iLkGb>3SA+mRMv)>-!2_3di~pm9}q^NYvN?uNgSW!9Bh3*&6Af)3mt ztMMAr8;L+Fdu_e$jHE5yCw#t|b#dQGe^^za$~tW%brCautt3}T;@Ry6T)9O#Kp}cS z<;~7;#gKQs9}2{k%F6lvf-O1eDZ)1W`+_(4$n*D)aCm)LTnbSeP;}vH!;9GZ@8e2B zSSnb#W-q&;mzuU0`r%De#a}&6%!@Zn3dB0^U5kF!eAax)Cr*z$bz zj@_$=*ZA#Q#ZsI#^0_b2HEK7We9+fC(WmdWqcP^K_38EHk#RG@HE}&CfeD1wX(_3+ z7N)nUUEP>l)gHW|1*2GZwN#<9LKl)zhtyXvUX=Nr&SAZ0cYt*RW$=8koGgeab4_o( zHPK7yYh#1+6Hca3108j4LbvVZGF`P!o>tpmZBwDLzth-0PJH^ZBy~>4*i*GkY->P2 zukaG@Kl29PvEza#IGAoIMeSU1^gHGu{z76XA&he*A3RO3bsT%(rRja;&YOXwuoLF9 zXYCB4Q3m}&HBXy%1QqOmEaCBNEPec1FOQ=PvU1T9Sk2TRIwM)rHN6#^&1Fd5ynWF)3o%+U*VCTZ%HQS zp5!*uSjs++`XG~7wBb$39Cyf5HBTm!Jh@8c1KUEW0hL*h$L`AB4po3hi7(;8vYly5 zCgy^+aoW>Av`rza*Pohv6LY$&y$C{qK0Tcc;4Ou!arT7pmI_XfI6EzFzZd;Gt^ALf^Zzt>?=BDYUEC3Qa>2-$58792sTQBm zZxdVhvzIQbKXd+vwI*4;T<+SylRHP5=Etd8(p?J0F5vKB)8?<`70nj|*B+04h_g8D zkbk)(KyoU3EdS#9+~D&-Ef8E*{ac=|<#oCNCy`CGp89@JtJ8R$U0I*}&~+wWm{8x& zFZh9a0gErRK)N|W%s|esFeeuG7tj?dIl%0@HINO>KKz+9MF!R~rzwZKIaw<*8Qa`0~ z_0mLmhc)Q;mz~{P>n*SLLAM4N_XC`==pO%!nJzZjw;G)Jgii4tf#>_ z2cdGIDHPW&d7*1HKb~1+-H?_Syb*m~_p$V;x97}}#G#@@;600O`}|v1zO=Yu>|qfz z-&lMv9sjSc?~VGMQ@6O_8zXxWeHNZvi@7WdlP;6V4!cK*V21z8_Fj0LF}l=xprfpI zG7!0kD@Gb`gj|0 z8Cg58EFCjYI9K=w{JJO`|jTF{p0(ZUcFY$F{?&ZjhgGv%f-7Sp%;^X z4t?Nl+npVo>CjyA@UFdVnIzsx31reH5yMF(yx=iU$}rxXP-OGPiH8CJ2Yk_=V~jaM zfUs+h4-6*<^Xi#$Z=wvt+O`LC&(_M0b=74C{aD?nbNn`oq zZu{*F6Jq^5icySX1>C*UGHH}zTJN!3ryKs10F-ux%2h0qC02L7N1Ve4G~h3x$Si|tB3TQ?27K$adfz_*+t zVNS`(k6r6QG%c8#eV@C8F6aIs>>e#GH`sOS^G;?WOB1^G_i`rc-It6lz7`4R z6mdk_?FLqQYWkhN8JcMrc=SL2M2P@X_!ahCfNlAHW!dkB(Ovs7wN27vSCn9!e5bM`aq`` z9J4+8O9mAI!&cZ3-BOCLnyBbPGMye^4N+V7a7PjY8ZHy@ird)GTkjWa`RO_bd)6rk zc--Mbb%Xb>RKEc9XqkPYZLDlaQxBd$2obPA-mMOC8;B|$wYQ}+E3$fn5{i8RcY5ZKGg!wkt=&{$fc!8ExPlue+yCI z=29rj8ml(e(EI&5Kn+#lE`uA-b1}iUVE@d57NZod&lQ6%-nF zw`j;|=@SDI^L_R_HXWT&2ELK6qG+kD4%GoCBP8@Va_UXT48Ul9P_ow*l z3yfMr&C_SgMYm92(%6?UVz6p^QSF=LzjS7NGOSyEx7pU>6Q@~qdLRYq=yVv?OuazGBparWF(|BCGL!e8 z-ZIIGvh?t5O%YMLblMBN|?U6G9yw52eu3AhJ`>ay5!uS~m6%U#nUExkT ze=L7gh4(}G& z>sgp6yrXwp950cf4d&_oz#`}D@y4T<1CyylR(L?*!dFt5;d*xMJs9vn!~N)m6==8r zk&(l!6RDQj%RMNWhf>;g>Q8A#N$@!-<(<<4+ zAb@uc>6)H~NFv+1F+gXdm^l?NIR`j34nNp^#@{K2c$IVT$&80EVEz^&x)ROCh2ax1 zaNac@a+<`tj`i)s9*rwC^PC@+ypqr=?MbAc7COm~dlwp)PRat6W^i1NM3bAzi>5uW*JB?o0$X0{Mzoo-29*m^L*? zqE0rnsr%_RwD>dT7$q+z66i|tC+6#L#a^UxaAR~f-8HFQa=-ts_kl{BM_Jr~DQAIm z>vMH-wA+auGv=mrwT79n{<7_JZ8p4pyg#U71oz#8;-I#3_a9q@Pi!NhvoEg{0g=oZ zOaxA|!q0y;DT=dho51ys#D7vr*FDm@7{Yuk1}>m0}7qF7Pg_M+YV z<2!H_lIV$xw-Uj1Yvi*9ate}YYy2(pUlG~-t3QZWY5mx#M6L$+p%cQ=fU{9Q)CDtk zV-H|oL5fC^9*!bZh2yC96+G%@GY-u#P~%cFAKRsy(9unZ(3>kAR5@T#Ea}I?jW*Qh zkQ2VwAWqfz4ps@uTiETHzShj=hdPuxpfu!!$_IDdi#Akid9c(6jMx_+#bmV@Qas}j zV@uEVxqJR7(H6T-Y{&u%0QgM_o(}*dKL6sgROB~WJh7VD>&`+8nlreuCI)Jp!A~m5 zB^4Gc_9_Y1TYty{P4uWPgA1SYil=_vf!*Srbl>K0^@_1X%z_4bM!fojNkb31_NJTS zRogr}6$jXQK7D4sC2tECpLzbdM4}~yx`sQCw8H0uN}4n?xfW#T=RZ(-1&xGp+9s~W zekeUZBR&`xA&g|gLhx#yio%amKuMwMLs5>wI8|Ygyq^(5b$& zmnJeiu-#=YXRjbQ0KNC~T3sAoVA`hisZ=V3XF-n(zQ>y)!_Pj7tKI*0>{8xV}M z&gUC~D{QW@W!5Y|FK7Lno-WlF&bLDC-JfGqkz2ei(D!JNxts!o>C(BH@F&rk2`{!W z(UY=eI@6G`8b7mA@;Q`}WjW(nP>k$e9du&kpGjy{I3KGHohwGfph?u%6nge%s(2Au z$~8clq0tqN-6qOA!|<#2I=oiPIL>|d&&)000wx#~ID;dE(xo+9$S5UykXq&$pJ`Xd zetmJ|C3~;<8e-#k|G8j>Ezi?rm;T#hRc1N*IYq6u9Ue9g70Zgcx-km0N@M9eycr51 zBR-o8`xtMrm<7dVJi|=23Ya$uXbQUZio9o{Z*A$6r}(;)Snrgq>xy^x5>)98jrCSE zeaG*&i>s}^TfHRd0$I8+D)SY~CM<-1$;ihY@R`(Hm+&33jUls8{W-41P6Ph4{9+HUZ(j^dy-C|=AL#)e%QqQUD!n{_7mX@2?iTiVkDc+@ir zBxS^tcI5K(0>Yv+NY!Fg1!Z-vyfLc-ZqfYW70Y~5;AcLxHZK^a7LZaU&$nh@2YL9} z!gp7Si=57xW$hWeT4kBS`+P@h_ty<-!l3DkNrb->m|bS5@iW=_&V#4evYCsb`!0yX zWGlZwP^WRrfo@bXZzRy+#ahD-Er@L&GZXC;-Yb98>xWGm;`C#RRR!Q32_*`nlllY{NU{UnyH;BgIPA9uD?F;%th0{2gBBbu8okWPxw`C=hZfr%J z;1(*bKUwb#+WL7ca;e{EzTb`T=mN(7LTC)B4U7~gz1fgs#EfJ<2AAbu-&t&GUtNEw zU-dx4wHZ2v)Gf?0puBcV*UC=G9Mect2bbEGs7<*|^wDd57GR;|p&N>ql=Tp6RD#u0 z?ZUg`wXCB%Z7=HTaS5Yh>8oo)v^vy%s~dv~8rH;b-ZaF}?z_?vf6;*c6Nq@pH@G^Q zkI@>JbX32a=JSP*q`INxE^=-OvM2vZS7wxdDfC5PtV_-qgEt-I#Cz|!g~q~Rc1r8e~6J7pi_~Xyj|J_Fg16dtu3YG z+`INJ$LY)zw5{{1Z#>lhMoz`;iWzf&;zoH>&k>|*1xjQ9t z&y$*Hp*o)r6buEyB% z2essA+MNhP$X*4=VN4{qjQadEa#M0S)fl96ZxlaLYLZ`LxNJJz^R+!o7Y$VWsX)I{#OZ+c8Q`*&nbzhN3 z(ZIoOS2{l?a#ytJzhmqF4)*XgAK-C*QIokJE{&qY{OM@;zk`ZkL2=1LHx0|o1)K97 z7FsiX&?0a{e3$yL9Me1Rit)7=W!^ScL!T?)h0Nt`Wjl)UDrQ~ptJxgbrPk^#r%Lz~ z^6g44ZBqXs&xF)j4G!+NQfIZTc3>vrG+Z09u7T{YN>X7!Ggi>rG^OfWk{L-k9urr^ zM6>NhQ9j+PQ0EiNVd5L9%$Pi5}JYkF*MmuX!x|E zIF@}8Otk5C2aMJ2_K>*(@rXTF{gqw*i;W=3({Ji5hkZc+6UkKp+2SpW_6#wHf~J{F zE(iMS7xzg>IuU#~``UEScV9_|zsRNvN#q@vWWe^r^?!Oq6Y|j2?L(mv!f4yV+B2H{ zqo09;bAqO*`5kq7^Mxfzsh*?iE+R`KDsGNTRu~b31@Yue#mJ4KrzX_c#O>$dzz-H^ zX>Ps4!I0FAX#J`o#vv<{xyB2FHkV_PG0IZFwggE?ny5i?Tb`&h^p}@l(=pQ5uSBDO zA}n;3h0(RMGEB+LL1k4Z+iFk9L_u!rA=^8k=svXSJSYsa5&0O=x4#&qJheGZ2 zXqwLVH6yXUN1E3S#E9b`^Io0er50OoOczM>`@DX*Q}uqkDF&{>9L?Kt!#PvgB2&Pl z2d2Yka4CCfIL#SpYg(4LGcqV9{BnVPjGuqj#xHl`V);woK$CtKEg!Td*bVe4Wq7bp zSPqM|>~Un?p~%{%PV37sDc>KI{-2DBR=_}+ltJ*14M+{48|)#Y6{E_t8*A;5%Dm-7 z;9E%ir-T}Ho8)I0Vj3`)8V^1644-VMSfCteOr_T=Cdzc_r+NIl%DF{qpF$y1L3+vW z^vTkP6o4$6`~^QNWrX2`25IH-O1&+Nhe84tn@c6hF3w2Tk91nK_qL77L0)0=A3t$Q-8DdFmm_y> zoh3H7Ai1LUGYHr}a)k1S5$#ey&6Zo;>E4fWX`3RaDL3Sdf#t{Ea5}E~Tu5&elv>pb z22wP%WFp(}n}B*|v#=53P8u}LD;{m+H|~@f_=g@#v77yi&EVa)?Y1L^@Ko#d6YVJ8 zBaK!@Ejdg2HDAvNll}Kj{>IvWaU5`sUUg=Z}Gz7$LE zLQ=Brn4yyR3CnGVT{8+PfjrbOf!X(}8##T8CDmsv<2yleuzHVI^V5BjiD9_2Nz)2~ zYHlTn)DuA2!}TZE!Cbbz83h{G4rU6T?1zK}o&e3e7do6r=- z&vi80tv)k&di%Id2qLI2Fu_JsXm5#aCbJv!xNg{w_)t!)nBP=6rD$^9sbGVqu%Dm+Q`4)#AmPJsdf=~?*JGCJ*3 z%gP8Qf@cYyWywxX$B5~d9|Xz^xNNY_NY0LLD&Q6VwyGiHase-}qpU{`&D~x0)jK7X z9pPoXQ@L(2M#17RcN8O_!#24ZB$T&?^=zGx@vt-s{8Ur_Gq_Ge$J;k8dU)N>WnuYC zFmM2{m%nboAU0qf!dY&-o)Ho;tPF8d*{nQkzIjjAyzzV}QptRUzf#>xV zlGZ{1076)33maHA@)KCj*pa6OwUfd8*{*DY0!KfTLRR0caAbEz0T+2@22}>l{zHx% zU=}#*sL<}759=rO(?{DT4b(8hcr(C4gfA;x3zH7`s-xD7CiYrnvo?|t{`B&an%915 zJKlOpLSYXHF7*}jwleW1kum>|PE}%E8-&``4|&g4ch`chAssRKw)0UPvJy`Vr28$l zfh{@XR&n7|8*}HHaXNW^lwiR$SgdE313Qkrw=MsYWbQHthz+XWhY^jxd>Bt*U`PLB zM|T*Zh-R+cns7e%?O**ug+%{R4=eDngi|KT$S|oM3-l5X+xSj$m?$ON|M)8|$l})1 zLb>FAh-q%{pF5&R`3UT><&>SMFJ=2eMDYq_WX$Z_$zS*ysHNlM+$DID*XLbbGQJd1C`}p_-e=K2AdO15kESY zvmzryYg6XX^f*UE{;VaW9|3MeWLLR5=TsSpxgCnR0Ld*+_tIs+OibHOIfUJs!67rq z=qotKKJPVJyzlhuLMtvR5?JD^s`Irr>_El8^2vYSujN!(EPL6JY>sf&(+W98M9mq= zFUpx$&|WZQ#|>Og;Ah381%bN3!Mnopl*2xJG&82J|B2RFy%5;W)b!0p1RS|O?^<8a z&R6PdGVCE@IY5fDk1%W-zggf72tEa`ttI-zDH6RVunbc>zsW8e*YE7WPm1tj$PAQ8 z6OoJ>vK#TJ2YuWQ;5W9Lpf36LxayNO@m&0fR4-|sW0bjF{B4H(8W$?R`}acqXApD3 z$;yZeF+oj@UkuG*=m-6HxOtiir~kjiz*A0ObK^A;pb-~Ht@~aw!c9Ketr(M~=LOO# zaspRP#F)ddIm3chx`F^kKAIIEA^i< zFcH4fSQq_7^*discfnocSK-rnzwTY6r7m~ZP<|G zI`yCP0R4wsf4M0l(kvi&8J4`#V-?xJ!9_6q=vUcm^=$b6EbS}k7zl(>bZQ!C`3tbE6h0I|Z%1!XO865&^=Q!lj6DI!=nKvhG&-OE zkc_wRyEZWILfb0@>e3|A3O#Nj;Y^yLME``#ukhx4A7@uu+ZB(WRo0&wrgJ>tg+!8v zQysHI>c&RIsEPk_S6QE+ZJEgwNsK0{n2Ra{Aaz1Zf1RMWtMFwmGU+DzMH|fNCfAB9 zRZ(*DyC1!+zO2Hj$`OcJceiN|79SFX8~4^!uqhK@=8P<*#Uz!-CP>VQ`tvN?WBr!* z->)k217ESlJa@v~>(71V-dpQXG&gvi03b8*iLZe!4X4o#Q607`wa!4>q;wh71N}zRsp(yxE z)-GAYgn;x7c51}X7)%+{?C%SMV#|#|^cr|)SqZAt=WnlE++p@Coj5;QR+na3pDE*3S^c0QpuR~j%ii>O zX;tJmjG;V~95g(Hp>NZ~NLlU4v70M+m~Q2RN|KGMBFQ-ugg?l?+Y&^SyO^uxlpDOC*+phs5bf`2_eI4A06I``D#{&))x=cr3tT#BKYPkBC#54#LN;y{g?dc>UqNW8n$;(NCGW!)AvTqtZIP zten=Qbofle)6B%>WMIVjD3k7T70`qE4OP&<24CyP$#SZU#f52hDr&uUMEiTGqXQj1 z9T-o?Pj=m`9s!PHVvPYO9Ckg2szleCK6|5@CNTfU>--h`*ik1mf=2!F*o(d?1@ZbQ zZ}L8nXrJD;?J8PVG~)E{_g=RS)Hmoa8rM;E`$3wJLH_&r%M(rZXlfbJaI;Z!z8~gv zi8h5};X>pbkzXJQdE1Sv0U6kJq2M9=e%_MgiCB|6TiN#Dt*kE&|8Hk{+mB#@tO||>op5CHDEpz5H+Qcuts!b@ocW(m|pX* ze@D(1F}C~5krIZXJzo;{?r9ubd0+zadp+> zD}L{x0?u!gp5f+SzMCGg8rCVPiRu-U|8wz=&`j zjZ9u{*1fR0t8g_X7p>Kb&xZNS(W2r7_zmoke=H%`5pj9G1v{m+KsBrD71eesfgmG; zg_W0pz~=zW8sdcU1~h1ebnX)ha50Q>-Te|G|kQ{q4#)b{R$y+x4#>?|6@o9o>~z^ z*qO}V-7ZCZ{OK7nOZf@?ZGQujSVQw}sza~P3Or(f9_j?HGXVf;ky>(@t!`g5M`VZ6 zM%)Ig)K?h~{6zdEM-Q5VW(X}KRAspVeTGwt^Y(vr z#DG!)N6jGKUsbuu_%71mu%ZF)Ma~&>*Hh&RIk3)@4)@7$*@=sC^sOddJtf_EQ5-19 zp%bs|XciPg0TBRMe?2VeY1#G-xa)kox65}>`2 zUE16F?221MUi4ZI(fdOrVSaiNB%7)#aw6F zG}xqgBGRNMw-9e1k=aEg!GagOJdDm=%G18Es62eDBHk5N+1|1`m4O12iQsNcI<)_5 zNP{6mB|qtqL1-dRVr0y7;DP z(N4?LJ^sDeOFE@PbmQ+(g4A%-X8qWjsre_y1_23R0sYW_fmD|TGz+7DEyu>?c`nQM z7{W@!!wUxah){Oc2H&|4&=3k8fqHv?b)aK3(HKEHUaaD>!%XB0zrywQPy5*R2>VY` zoI#b5ZYyc{0l-|{bKk{>u^}aP8>M*+5YR^`s=&XV zb$dKUfvO)Z&TU))arUgBVbyiGh)0g71CW~y!iB%mz4ChN?*_UOLfU0TMxk#s+|C#L zi4N(4+$H&cRBya|niuM*Pw}57KbPK~R@6i`fg{cP$XNQ3O>gdwq4jKPDLJnU=`V_& z*5BIL@KHgGagtq;J&ycDF3%?Gu@dRBK1dke`i>JZbUmcWFn(A*s(ba_tXUjgvWa6L zc1+Ql<~7;4dN)bRKavgzQPUXQQvTs?A>)f#FKX1DKR6q<4@B+2GU|gn+=0}#He@y` zJ2R>AxqVL<=2X*{LdUiGcwMlT$UmL?VTCp))tKOhGA6%tAF}|^mNe=O9_-5s@#f{- z3ZN(X!Ke}(zqzxp@Mt%b1E*^hzrOjK7dEmHe-Pm&LV`Wbe*4bL>^8N zC=-S;Y_o%?8nfdq&$wLlrpqvn_xkIx8QgqNC0ABpQ|gLbwo+dGxDWBCQ6Img_Gu9m zxB`h~u7r5zdIOp0a}QsAfi+4raRK&Aj|?OHy4o34wwN=m+z$}dY&{64;kKzd3aph| zW@De4bY63CJ;-aC94giErp}@f8Rb!+wUg!DIvhETE(JPuFek15!(NbHd7OZ?(;Iwd z+52SlY@>FvpKNP&Z$ssEAkR+#Gec~Cb}?NU^CjzBh!%=#`W+#J~OIyI%A_ zes=H8mZSu(R>)^ux2GNa!_#6Nren`^ax8MYYs#{^95do)t?WJE7(USB zgJj&7+2VIKz9Y=1jLU{HZ6w}QfNogAqis?Y{E^O<( ze_}KHPhIza`S@LmfQ6m6Y_@2u9VL|B8d8 z(3XuWbAne_*`JgCn??m^Zt)d-0$ z{;TyO^=*{gc0>tRE`~$3V0)R!TMmY=c?GJ`kiGCFa2E*sEjGHwMp)Uo2tSA^2yNVhPbR>4OhdA7o{$a8K z(T?hxo(gmL(}J?slGz+0$SCQM4Ga|D*xMT&9(i9sPw~=uy^`XITi~p6pl!f(s2Kz< z?;Fc!^x5?56dUm@KkY90v1pIbR2JgvSo%9Gu#3sw$DJYijC9Qfg)p=?cHW@`m_ciH z!29-O?GN|7?*A4SFA19vVUpsW=^)g+Cstl{VuE{ePx!h~*DKP;&DPDv(4x%9Nk(6N zJc&_XciUl%*(N9b`@6V{`v`SE3CBa1>+G~$;-g|UE*l!$f8ktUGSu16^%K?O4OH53 zPnJI|W&wH&5D49(EAU1`PI|9Y(YL@Skat$l2ZynPZ>)0HnO-!FQ$=r(=o`7RD)l?Z z4nJVuXYAs#TVq4^UH~>~6_@s?B+Vjh4lUrz@h5^q@D2^6u_6TRDZEt`WNO3;^A=-5 zc%AnIC$0w;^%rKq?s3#wMopsC5w$_ap>-8_U<3n6qP?*j!gxK0!X@Q)w0Co6%#v8I zFOnavY8eNV@5cJyNSomZ!tYT+ZAS&2FBPJKF!>c9g|_*_lr_p>m8W`Q=)RKLO5Z71 z8g7rKff^YZza^_C&Z~t?FXuP>rn_S24oO$$J>tIY1mYP8=b8Bbr1MS;4f4Chs7M> z{f8$dKyYvsi@n%KOBdqnHpq~duvoA1YcJEnN|!F1c(d8mBvY3MhIT{JFEZ#}VHz&D zfC@B))_k0ZM;ihMWl;>8&VRkl+6q|d$gY#>{NWVD8`paULM83NkU90)@ z8qmBiS*q+3j|bxsja<4(@Vp>Kcz2qeMta%>-2xB@&Ysn#zs9R zgMd;4$6|*Rkxe=atRAD-yYV!9{0xC8LvP*!wr*_r%OHp%XfZfsIT@^~eR{Uq$`O`q zW8DKNY}R3(u1t_?a5X)ms*-iwy15N+U*GA(y7m~8Z0itRTr{|RycXBhKFA>=$iHZa z(0hL@2D%j>)q++-q2-s4R~Kh>wC|e89@8b}e70uOY=zR_myGjkf^?(Dah_7UHv&l9VG`X9%REk(RB&ybx{#1`4Z5UybK^P?O{zCQe^?BfxIZ$ z*NX*FZLFApAp%#9^kK16L;Jr)Kj~7FJeUr9a}3eJ{=o`42)Uf?Bn^=b7|3Aa*OA$BUQz1|)pV z{AJ3PsxsH2an|&PHKV3^b)q@V2Daf%tAp~buO~wcOEC5>Y8_B)(K%-t#OPzWWEg@lXC$p;)Wgh6eRF<-~5+GKb0a@ z3~CHJ@}^IsLqW|#6(`|Y#z7`6d=lfXY;uGlx{ux1zIDhUMP6P5;$}nh*OkEZd zmk~58PNV=W1ov`PqB%>Kt>LnDlIBoINn9QJ+Eh(EK+g)BNY*lmc2-bW(;F*y7Se`f zQKhST?}a~_QE2Wmcl4el!^&WLx+g5oSQavvuPL`EixraWQE)|C*Ap|>po2G`c&-?EDu(mr``i;tz}F>}8tfTRl10&xc@v7Z~nhYB|o1`1^2Ah zHq5YJ&9^Pp(qlnfF1o-e<$0s#g0amXBiP$<(avYAxoBKcC)Joq@AV@V2-1{Fjru02 zp5t|QbKK(;n9e_;(FOk79Efw803dh=7=kS&16Z}x>9tA!Fhe|kdrlwICWcLTavO8( z4YKNlcdpU7qDKhxD)lTxN(&}dy@|51?q~{W4~r4oIjcWk;ABms`zYQqMu*|6^&t&G z?&6oFFbsZ0qm8aq;XUAYvpMa1#diT}7^gaBGnS|+XD_`0=WY~le*r;gi|$kLz*4!r zQOMgt;GuA4zNf&5RI)RJXxe?Wig1{!eDE9p;xkR|MmO>5 z3-P;Px0z*()H&|XySI?JU$&+i9*CYOh=-_%=M!@AMb%04O0*N5K0!aTuHVilw0qbZ zY#U<2puB!Lo%u$j;~zHt>Ird@^oS7)5(z0OAs!_p15#0fikjaz81c>c+q{!czHh$F zbNnOVEYP5CCYM_8Z#9X*`1%YD)ZWxWbwW5$B)UR-ZM2m-TB})FFgnq}SDWwww$k2& zg73L362ThF1-Yj@5;A;S)uMdVZ|8sP=a*#D8%w?e{rr?ghr`ll2PY%>AyvR{ zDp48*E)@yyb>S2!FWQY=OR^LA-&GpVkl(xDB``c2&>p609x+gHh0a!BY&tL&La?6^cL+<{{3CgE$5yL=3w zXRe@{G0J+#*)_Gy|3to;1eEfl+^qFph{qNX`A2kq1>-3!U{nzXpV9twe^t87BZ>HA z^27%8IBt$6hb>r8NcN8)y4XX5n&p1iOmO8Lu%BN%P@~Q(m1v;OyWAbTSl#sqf4b{{XDYXcu^&V%i112Et~trp0gb za5=vRNX9+Ysk#j?ns=X8oQ&Kd$M=5e3rC!)?E%MIPxneTeVlM%d-xeK0hhZHfXO<2%c0}yMSE*{fIuU>xon_1 zSkV-$*=S(8=3d6WM~R!h&jWRp#Nl`zmXxanV!w)=#o^k=KsF=^gmlsL9XBJtt8wk( zo}{1OdS)YbUZP?KA{tTp@p&zhYkq`;-Pp~c(W&XFdilpw_H_SqS^IX^eN&OvnmAT9 z&jnR6yG%|xGx{KU-e#utfx*kK+mlv1<*%*2OzbEpUM;52VY}xl&F4+`JH5U&EWr73oVY?Qv-eo6HeyExgnZtFDWikB(JK#{YSe98}7gHtSpgBai5TJZKJWgQC zG4|xPia35fen#*QEJ6&JP98l<)pvaj{qau9$v#ZEVbGVcw)2dJl$$y0D-riK5p^^n z-{>L}+C=iT#2~BV_YU*v2}(zTT!8#h!^lcdV;n%;(qlr~R{4BuBx~<{i^XmolmGHM z(0`6K56R23UpoX}4r)diqF@}q-<(M;joqndO1)<=!t+@K_OjqY%fYm**OareyRepD z7#?;=x_Dl;K|1nxW$r88hePacgTk1dIWp@D7-HYFy?|nK?_2qa=$=(o-_|~6BkcV= zV3L;a63Rk_+D^+S|00LJJa+I4tsf#uppiFo%|M*q>W>c4zBA>pU-#*=&x<1?5WK&~ z9GZ3e!0jvKVUi{5aS_?7+3KYiW~wq^CqEOq{;1Tn*GRJ6!Q;7(nDjAFRC8kjl9 z?M7q9%YN=45P0Dx+~s{DAdnpi_7V5_2t&*>Q*s#2yH%$dyx%=hPi^B$vP^a=b>$yn zyxoeXV|0P|-%ibsd+F9q@s6D^$?y@+%;WQcNzuOE=01?ev1(%_CDrf>Gb28U|cpRD!dWCo#!kOYSw>=v*R$AuM#xN zXzOrK=NoK@S-sfhF5bfy;D6NWGk^oI$#1OSnp|KT3c^DrT&bJ|4kQa-g1$Z!d9r*$ z!z74PG6)k5?tmMOm(_Ff=cSNhAsd@OgMGuohWpELPMBrA;%5Aj(%Y5~k zS&(YmT55m-A7&U`BO#&*=S0dyaeCWqJn!Cj_$IrqxtyyUa{i29SCan?Z|v~g3WRA& z*4~TXFrF-L;1Pr@MN45VnFlHYsj*7T>vs=}jhMJwkYKRPI}S0&9GW@N zcHsjG3pw^;q?_eSGl@>%=KM$9K-ay5_vPm?#6Xg1E-P(g`8_1LkOc`-^xsDM>pVG+ z=zWY|JTM%#D_wdDWp|GQjM-X-eubl0;{#t8?V2QjR2+`K4V1oryx7TZu(@XklqA8bWtMf6h0e0D zqttxzI3x#=p<+N?u@j7!(lMW?`VO)KS z1v$b~y8q|o6g^Uf4AHiWT>LGrT_cSESD`-0eG@{=x++NUHw2{;$8z~ zV(o3aRGO9S4-eLqRz>z?uZ6Gg??=gW(+{wrwiB9pT6PoF&)jnz>tfl7?BX#RvJj@QGiJ20GCeOUfl7Ps?SoT1>OU$HGZbB*irPL&}(D}xpk5(OD4L@oZbz=lZA0Th#6R#bzC*>YSoA41_?WO=YLiJ@+^k@rM#!k|{) zvHf=3dnR2_xZe@kM!T(+iS-=u$w9q}5t_0^TZOunhmAEM-FSsE%X){872ZvUvK$xJ zCU}mFpiYSj=*(KFY#1JPtD$#!f7(FC9Lt-S%hMe5kiVW;E86%V0=In?FqX6ke?jUs zr=7y`jIy#}+&U96c_IE<7;;<<>oXNSBSs~KLx;oRSAOwT1)w%xGZ()9J0=)sqvlgo zXX~nABhrNtF%)X|8)1R7Y#=SicQ@0zZB}Dg$`#ot$ND;1*HmA|btuzA=Gs6S z6Q-iJpQs)`mo-+Rl5rQTe0LrgfAN7K1hXnc)r5yAPZcssH_tybV8TB2z^JODjCSgN zs%b)MviVT9N~~JYCs;MB2%JM#HDy}v{)|IT%np)y07E128`5N**M99905xU|B^Di| z38x#^@)|pO?rLR#RBp=lXF{0ZzxtbHwyJ63Uq_U9XWopOJqC5XhhA)P8%G+A2+vMG zX9>1aTQ50b4eL&913>5{b20H3<(-Tlvg6-!KF+WL^qv-wmU^T$lD58%C_ObHMRFHv zCn;=73qnKGwQYEpljL`2YF)?FIY-Sr&$X^Yl3>^>IHywdYTZOdYmPDlSHHI1IjGib=6`( zmO@ymM0i|Bi)xl9f{xZ-2U zDEn&tdCUhBJ-f zkbws#QO+8NBKY#i5Y_C4!q`#ZvFZDp8UpBXnwf=TX^oRSv4LJE@;xT$Mj}S$Ws3n3 zYG-i~Sz1NMBE!1)y07!CGy(T78#&}NJ@IXl~yVLY{F17WepZi>kC=yU6XL+?NYEE8CYc$L2kCnmx z!^!o5ohlTo>Lu1-QtkO_YtmRE{!p1@J|Ln%@UaD%QJ0j&`!E z1Iva*jqOmvKRMNd?z@JYv$MEiX0QwO){2<$r85t}5lJ>{jz8sz-q|((2o%79H!t7Q z@pb3J9JVk&NE* zQJ_k7AGR)KaKI%%k$-5P9+?Ih*GKuEPm8IA%tg#O;HTZh-aK~FBJ^uxM@$>vMn}k< zv$HIZbYxf@G$u1QtZQHU9U0sGrliqgSsYN#BvdkbYr>J(jjuXJLzf#^PRhuXV%?bp zJ~7eO1o!=q)bGFRkfjY%<5ByN=wg#q~H(8W0C^4Nskg6{blN(CLn8(!&e+ZUfaj|>mUYI^i>%L;_s#AJ>CL*LjaH)zU8jjpy4^($k^xuo)V;iB=bmvBkm*~} z72~ZymMirBAOm57UQxt%;Bc5){BW5=SP2ORBLm_A5!BTM$6TeWYkHC?ym`Z~JD>lj zz3+^QYFXNrVU(ccq~x3tK^$NJL5YfzGm^8+Fd|8EQjjc?L10kH8DYpla?Uv^IcLe~ z+n#$bc<<|b&pB&-KVSaPYwg}ORZmrQ_wKH$#zmp%9OD_D!5<6@s8B5Lu_~GHgcd2T zy$JA3C>bVte&1=-ke>qTlfXO1EPEDW%PG7TSrHPc3si=?iZ++Y7w%=%+8DXOl-iCX zyV#hluyeghUGT;&g;G8>lW(1c_tf|Y=^(I%{xiL*+z;WSV#v8tIky72l9k3jIj@)Y zk4?s`I7NltRt{U?hF5sbPF`y*l%!n|yCw0Pto17(ZcOtEgY9XF6VV)4PdpcR5LEG| zQKwOtdF*CzOWSM?!?!y>R#Cl(1-=9xE|;2uo5wYPoSjwHU`X2(fou4hQh zsNnX(dHC;}694>?9%exD_(n8-d`81rocb{%5ru!KhB{BlCv)zT%#ghI1@@o-?jj(_ zpS;3A^$5jdm~_xM1(Y$kTgp3XW0gvHtzh>POh=ubGU*MbIU0PdcdB>0G+4e>x_X0B z?ON3-Y6UuZy|E~Gu}vaL!ctm)rV|6Og|sK5N8Y_VVcf3=&Sx4)k?9HfPbE0UqN9t( zK7CA)@yTu&Ml_wbla@>nN2r^<;>w9*r{cF;mLLNQV`-eJ9>zJh+P=5501lkVnyd1| z+ItNPj^JEqJOhvRA6Khdq`t-U6a%rxwTTgMf0WiDtIW$^#XE#3Ii9LO?r+g{lYs`g z)2HEY)$}w4SFbl^C(eHfZrIz8Img?WY$%Rgo4EC`{^`tpkWa!$&vQm>x`qxycYfdUnC6fvW*NmC!UT3Cb9Tm+=R?RQBfy;23pi}8rbY|at z-n8LC>V=UF$Gk8seVGA7Lr%+XH=3y=l%LSv%I4Zx_%QRksJjlrJd8*S&iY1f@>?|a z+h~iiADU6xe7+E^L zccx~0n43H68|*&1meqmsosG|w$G|qSR7lWyZXo1FHZxs@xr4zhDOYop#O|vcbIVA? zmTN3WHCK4?e0=a@T8_q$r^=B}uTZ|z(LiQu8`PGxhwhkEvX+PxGznIrCTsOA4t|MX zL}c+6+lG&nupSob+_mi_T@h-~z)SoRw}StIN&nQ+(w*tYsN&|ER>KUVG^pteE5DYa zkez&vQ?fsfHuZ~FGw(1Of?Jy>M-2mJ4_EOOn9L&L_rvB;alSD8)E$~Fiba{Z#fa1| zk+&ZwM*K{!(vR}k9yZ)}2>M17UOaSf*+w}z6*I-E78|>OV%baaBUa82IjW%2X6Sh^ zeH>Bp%Mz+9%>!6noUVmmN9MEEQ%onmm)ucxbLW2e0KE=puxsMUvx+|gIRAYo`MVH^ zyzb`8XD;Ye9|-iATl-y!g=c!@9Uj>LUtNKKFd&p?2AtKIZ;!MVKf%c><pjGO@16Wi@Nt@j`QWlDo#M6@xmUL*-mzhU@MQdRTp|Ua>I4b%gS}0a z!t@NXINyR!ZT8QT=xPp{Wvj`gqIBP-#k|2m;|hg`Qc`QpT5R3*%>5Wkx`)Yb4DZ(~ z3jN>UY!_99-KHU=N-vpRdo9YKLh1aeLrOHZIiL$-yCuI@U(9;s2fMLih+7AgIs4D! z-)CUHs=`4Yzc%(PK?$)L&uZ4}UhyG;Ay!XR$Ou0+-W-4=59m2YRnbD}=x({7TWTE1ql7%0{m|Qo3Crk$S=vJsh z%>oXJhnh&Go5ms7F`TYv?^fmg&4A0VPn+eOciv~SeMk3{!eZsV{Q)QM&FLUFxj7*4JX6!TpWdDsCAiqN6#^=Q$zxZ2q;TBi9!B5eaUp_2Vlf zMy_M;1iKoOYBiC11S*<$uwF8@G?-?|P6n7{6Bc2Yg+-(tCL3{Cl|m8H-84|Rd_w*Y z;HMvBX~eT$-H%`wq2rU&L^G4 zO|{OX7!5HR>_QeFA`M7@<6c#eR-tGF!U}JBg*{c~qyVDw5blKH7xn6RqXo z?3J?6QyN%O){%9!SrZNFmid17;rZIL^N#7oSGfm@2;xzBvUR#Wzs+(skkE>@@YB3| z>DT2w?b_a-qSkGhDoY3B*;ma5Bv;DTBxt~f%`1**xv021x$N(Z#Um*9l6Hkt8hHLq4Q7H_K49w}8QQ>ixR)Qr2(%T{D?CF;LX zf-*uOr40D^AT+QFBnqvkO`<5s+HQr@XrV`7*>=#%od_BNVs-pFhB5bPzqR_&BOCG{ zQ+dW#`*kvuxSKo*{`y3ZgmlyO_1FE>BWI~Y+n&OGp5bmj^xt8rK;sD`*p52l`u;#tzy#kPMZx1w+E=j9<{E;wjY%x+5HK_s3Lbyo$^p)ZwJ686q0M+;CSRv z>Y$X?)n~b?uBXaZv9k$JJ4hd|xMz~F#+^F#))y>hj&AebJA3S_I=T7UTZ|QLz_(4| z*43HRt98<3;{W z!{4U?#omo&hkG?$nbO4ZP&NQXDA$m zY}>{23s2E@vw+wsI3hw5Gq*__s}ERw`jIHKk{6Voz4*Z_epBU4 zFT(gmwP^JA-Wmr-^#htC@ozIk3k~~bD}tYRc{8Pnw0?7(`Nsjel<|(KUuWKxO+C&a zx4x1Ev-v>sr(Vxm%lX_A+YCTJW}qT|-r+>ngz1g~G23=7LF~feBa5&zXCDOqA~o8a zb-6=y^W9}6X4p_~V=wE&zB}nn;BXcWXOB$dZ_8Dj13ZDZ@<&oTFaR7?x;rr-=%kEN zo0A0HWBqWE=_qWfYQS)}K4AVMdIAt&9X98ps(JrEktwt=%L&h(pIK{PNQv*GnC zM1Gi52<$wsv|FCs5Os{811!En{Er7q`aqt%0ux#s!^er7_b;qRVu_6V5Z17Ib2hY0 z??T03!TE#UjeuUCL*W0_00~-c7u|tZbrGWm+Si4H3GDYGlOkZ{x1T4r{&28-mS( zEX;lhWTRiUvmNu`y;9ZmSaP!cF`rPAr?<-EI(IVg&=Um862U78{X)8ap_S*9IbQ}DmP zb+4%|7LO)e#)1IN**tCgjGK$MbS33}+S6#;iAN`v+-P%MMazMXZ>V8pyWGiU8c7M` zA5DC-DYV-5<^5F-9@MEdv0KGz)1lFI8-dt4ASr^RUKve%xvG%!=D`RS9=c+?u)P;H zBW9K)-h<_`ja}0gM!G%jK$Qts!Ui+_rxB7a!3%k5J{{t@_2+64?e^%3@9VN!y_*yA z*%ekK&Mfv+(-ojM#WD!*6xa|nlOOs%^F#-+Ri~o$eDY^ErMRWiWUOx%4JT5d7=Y0t z{pF4t;`rsOO?UTSWah4)w=R@Z_;^95Ov@p8C!bGH!t=g4G?n(8ot`^g6uHL7i#0{# zsrD}C3=W@V(A9p}XN66gn#>sAy!}T7l>hj)^US~+ew81b^rN5WOtVM`9)|cCzz~PJ zHEHG?_X<-n(QyVSGO_tyY|x4Ys@*U?CTns6l@Z%9$~qK~@d0ui7G$kmn>@`;h}wJeXk0Q z{w+2dKd3b%JuZ*ItB}>raaM^#K0tHsOaPAU=bu|tzHO1s8^^3ebJh0-u&dx@Ep;`|rFepLh`u5Gff;3Os2ow=iI%T-uT7?X8OnD= zHY%GUcBCu`w&m7bMmKgNy8kN{75*YQnkAlHpyiL1(bkG(4vFY9WgUGsAWvI~j>U zrbDB+8jQDMIh4}Pm7wlTZE^x<#NM7Mou&Ay=?uFs)f+Dvw{`oEUXUZjqbS~2$uG-W zZxo+19N(^9s~+v1Su-PBcHp_We9-5TbM&dhRy2N$QLPB4`L~-+$6!7PZ+Cw8Vce|s+>35Xx z_4Wl@Rd(x-TLrwVW1R;G)PjpGH(;m7DKh>XXJS+E_+y2Yad`ZsOttqIk_a+Oo*2j_ zU&<}EpJpEMY8~B(g)mqa4IDnHrcg<~g^09e!sF|=IBrzbTg4L0%z8MGIgmhhcS9}3 zQZ*PA(97QZ6=JAnjk~K&vS@j%2b@E1rHbm5SOjEmi$493>4?RulP#<>i=%gEoGfC* z`l~<>hOg!!qmm9hjEv(^8haNN@46fmPFwTCs-O9@FK8{{uF$S?S|RnwIXQGH_w$!- z3GZ4KsN1)q@}&*K)k$tdElcvTk`sb`aH2FQf@J5R@NXvAdqi*b-O@}$ELF|$?#C_; z_h%F#d4s}4NCK5Kfud^E4b6@56ALjM=$c9TLF(oIQWy3YX*0l5|-A+4FCmT#u?P%7nYIR|c`MQEzuEjz+#T99n z)gWNf>wSAYSA4=pwD6i8ZJacAo({iCOJ!o?HMeeq{YVT^xiwr0U)^Q><>~erR&_lDyk*a=Ne_OetZOW zwu!Vi99Af?XYJ%dXiP2O7A}Y+hC8@ zP9?`5DC{~u{hk&Vyf53Ol8*3fh>6yY^3BAtcW@yCcw^^+)xxojkWqE9fyqYa+;Y{~ z-I9G%MG0@1>|DUedB-~a7MAk|kKxV;469&Y%xe0>fER2sKR-qOxdC|D93aR$qzf__ z_KJN|c*l4CwU)3u!B~2jKt10RF(W@n*NJqUDUuqv+(8g+K$3#)rsdX|$~VznN@=F` zsF=3Qdi>W9X&-`?$K+Jbv(nr%%8Eu@X`IZ1w>%7p!6$#Xja2ta9Tv{nX~OeF?+1!NBo29~6P%(Rqak zEwguCGEf&&arkpnmX6wxWj3Uevz{$<@Yv~Iv2L7d4ufT&)&>@?ZKOwnxuWKo%kiG- zsN@ThyP2FKhL5gWD)+CS5&DUsmQ0=b40?^4zbA}FX%a~}=%kjUY;Pq!7F7*t&kPTN z{f{jP0Iu&3YUcTE0~y<>_3KyyYj!kf<9upq8%ee2>gHoa8%@dffPIWpMkK^E$7u<} zQE!P-x@&RN+t5Alup{9xs2rbFf;Q9LP33dbLjtT1QCI8~b(LNR#CnYO2L($=DXG8p z)Hso$VOZu2u;yYy78JEE#~w$PDHM&MazuLa)3p>;w}DV1k`d~bB8r=($H~&S$?Wa4 zW1epw+e|f!1nLJ&7qPx#v|6Dq3u)t`M+%pPm#U7x)5I{EpeL4m@-zT^8-Fmm2qmso z)1?j(lylx*ey|k(`UZ%#YObN2eibV&!~)b&Sz3sK8D6YTVsT`0Hu=X(-jj*g!gLXX%7Gw#i*8We>z zib-n;FcSa~_Q38|)oK;1qW%$+P(7?N#k!|w`CbN$a`cf(RU`}2>yF~;eFvG&+ApmS zy}uEio^xjfPdni!(!do@dhnllh`E535Mli-FQS&mnCh+G*!j|$` zcSohhs9-3+Yf)cCT4I{1kGOID3OF7jo@lEzs9KNHr@;sxrg3wj7b9lenB2qXCHkorhiBvP4qC#lq26B1P0nw4mjM_sy1Lrg( zhe>%?=!G**&op;Ohc&!_z`om!e!?iMwZ{nR;72gO=oE?n}Fmjb{`pD}*vTu)T39`sZwX zJOsGxZjh|D#ULxoK0F1^t+huvnygW~;2)+z$;CwE6@MTDtnnNi&dWMi@i(ZgsGU?C zj~`ixsA=a5)_-0UY|-Qp?euSCII#dl-k&ZLpvveUz-Y106@9Gvknwj8qwvn7@tzv_ z*kf3rtfD67tJuUKbP-W=xpSRHkbLIM^Dr=HkR4;eRMjIP^6pBVXNb6&<^>$aE9`VH z{5@JtGVGnx4so?vrOREh2djq?XwFgv+YhT#4}d!Yh?(?FWbC?Swt%n|rjxs#7+W(* z1I9jbL<-b;Cc6@d^`w55W6s32NsEc4`UXKv2`RDD_c5&#A-pSeynV~O3q2?NxOQw= zT6h$`8O-AhhwPLV5c6J!+_qA7wY_jm?St_JY`g0lodRiw@#Il!kIuxAT;KwJ%U7|M z@j?B{`mh3(bH`&d`EZSDRKWvFGR_u@Xe?W4U6cin|l{~B|nsqV6m;XSm;ow zU~B0t1)3m^qynqR!IM4|jXmvRYPhlbAkeLSuqMk0LOe#>O$uUHwgfIHI6vozqZu`H zsIz~`-SYy3tkMN#&?IamIdx0t+u5EM?JCaOREGBeVBSQFs8nf^+3nyKVK7`F@tt>% zIx(G5h*G)>(t~wNnEo&kdLUPQ));T5AyvQ-E^Mi=Lpi;T_q8Rk&9!h^4 zc9>%>$AE%R)J%8?mzGP~5A{!uGE`=0mpjsjs4ldiNiwk%gR8 zSjZfyH+*WO5qION$ZEgl%&)us?jN;p{$r=ikQTWWw%hq|Hl!fkVaIZ$q|2Qbcjad9 zRf<>!eWN1s9rN6nfh4d&lrS`@7+(5q4npk{K7Ew&b9QL!a$>mu5&*maMTF|HAYae6I9PEP4MMq$i zn`DP=K1r^;CvME|B7A9fXR798ICI_Ablu(JBF_+7>v;3Pwm3bqpkm@cNafbKX6m!~ zhgw(mCrL4%cg{N!G1_TeSGqwYB&}cWDKpn~^hjrfR)H^`T4mpj7=4|||2>0CDpuN) ztS|S()kF)65Qh8mqS|`mXC>JUE4H;WlwflDytOb}6LiJIMk^r7RUv7qOe>Q(i`H(U zi~U47)$B{Q%I*ZId-0JJccy~-I4-s7*BdeT=CK|T+9NL(^qX9n+a9Ql)6b^1;Up~I zBonmmV>gY@FM<}EQ0=URwIuN0xTlKyBv)ia<*=36kvbrhoA^ks42CpPWmUV59$>Pp zzvr#t*ld00D#3r4&>#3Ap&)Yfk$8rquk#w8vspjlCMw?+|kv(y1wS6*0w^l?L_i66w7lSOQSrS_kI^?v3>poq#5Ng1UWv_w=2jt zu}Bam1(rnYVT1AozW!${D;yL19<5}DmI&IB95#*p8nb7w#SDO0xb89lx~LX=j&D1) zaztoACB(p*Nv-|z#v5#@ubgxHGCP`c+w$pwvYrYoXm)}9F~?(82h6?Bs;R*Gqssj2 zQSamj`Z#_j6MURM!ksBGf7W@N1uE&MOEe6+X)KIE1i4_}Yrm?h6AbPxYsf4XebV1? z9E|eBJ%o1WlU4`d`=GFb{nGHg>i?+r`0rMQTmy>b@pjJpHEuJRXCqxX3YDR=;0(xPkR%Hw5J+wBH<2*(74Ez8rp zf)%HQ!SoEsvQ)Tb+cv)OsV4Q~*Pb<4pbQE^Dtj6`8+r}psP#f>#4|H83TfG5;sCIfg_^T@kJ&jVp-qL1pBB0&9vUn(gJ{6cmgTk zqEaLE%Nky>%hAO-mH5SsGxikkuI$-9a-RP1^d|FjC=AW9Gn$0UyRZ6+jbR$E z*?$D09v7sbh7(s@iu>$yM;mWB16%e{V3h<6sL{^2+BxHq+b*v}VKvH5k-fanJiGo5 zX5l-Jf$Kt+KCzUt->2xg+C`&p0MU>I&U6%Yh0aMS$MpRon)ENLrIhFNINk}1uwpdM~m>LUg z=mT@Ff{E+q`P#&U>X-)p75zsoLX+4E7%gh}P)w?NCWVKOk>FG>CaQP1yvj)AXE zM?-_=635eidy`On6kd!DsskOuywYjmgP8ElfwqM7bZEtSxD?)hJ2%8HKb_M zh>0BV-XQqg1Uy;|6+6kX@{d~?&hDs|G_`gg`}nc^+i}On$dr5ZuoZ{a^ z|E1r>3;$~9w}S?vSr*w=0Y@v`tzSU6K;!ofk6OB>e?j)&7aZ|q^By%EPn|HX|8 zz#qrDIkDZUjG6qtH-BmFpiB7c)Jn({1Z)vg)W7IRS=ZHvEb8Tho_ z+G0}*2&m^TDr59qgz$AmytNYlM&HHGAM)xK&i_Oo-*o{?t9;}93pHEfl2VHWf|uhw zt0S9>kiu7}UhKb4kX9C8k2F#L@{K;yw2boFq-U=)7a_JWaT0&sr)W1d0Pc+NLVgiq zSP2OqgZj9QzB~x1i%mH>nhs0&m;e4ZmHxAj({)zL<)jYS6Ji?oo~ zo0s?zkpYU`p%rK~Ny}Ik=J)+O+8qMT<*IAT{^C8JQCB~{Fu&DBT`g>wY33#|)!m}T zfJ6U_ z5v+@cfpWBEI6C#8@0-c3Ld$SEcx^B*N5Ma3QTD!GotNi${A1Eb&b9_y zl8z6Tn))q`OJ-N)#F~;OG{*=p`gwYj0=6e=1ft<(u_l@DS#GDSz0}l&%Kn#zCl}b3 zaun2pe~FPYnfchB(al*>9FkBo9y{5czpmPHLIr_B4vR6m3mbS*1L}br=!gHyIorFrCSMrfu)r1 zCi{r=rE-Oce@@{>g|c{D0tr&_E_uEkYd3#-3pK3E&m=+&P)L_$Dj69`H`Fp4sR7#S>W4N_9tf}qd> z3bbjVh1Wt`+IRh9antg8hjZ^ad#`iO`+bs?@K>|;IeYQ$IcM*+K}1AEL_|bHL_|bH zL_|bHBro;_76E&DCnAzZ_W>2)UhhOi@@7k56;>dh0Gs=!BeisTC(wtT4Rn3_-~E8I z9CH!Lm7&05?5{^P76F4jlM=~`v-oUrRAZ87QX+ZK0lY%(0-5cYmq;!gLG9wG#-V3S46JjUQe+I2Ya5^;*6Uc8JxE(W93yy+Ya?C6}s zIP~2_a%^wl9&ArX%)cBT1GfXagdd_vdRqWjvF<}?}?$eDhrNtxV6?FZYuuXr7dwfL!39E-8n#3HF|ME$oy7dqyZ)Gw%=`_-OF zNqJmG{huiF$o~eT&#|WMXf;-0?MOvZ=?}b3pSpi#^R26!YZ64MaiJz6i23UHD`z9Lbb zL3_nf!CHQblvW4U+NRwCsbF2zMM`NR>58KQ{4ILcA`uKFyZ*LCSbieXlH19qI4W3& z^)YeN53U1Sz5FWH4(JZ-91`gR&LO+vsK#{4b=2}DtUJbOSm8*CeTQ(3p#MraHYE)K z-lz5$k8sRO0_sP59|T*C8NgO?h7`=1)F0pDj(JHSyJ5$Ug)WY2EWy676mcC?2k5YWhbe10mlJT@F|K~@h)%$FgoD4 zv(oIW^$M^I*uX16Y0n7kwd6ef3!+{u$M%x!7UmM9Hjd;kFfMV*QChSe*0ZV`{{mtjEiTFHH)OQurSU|TWl6F zx`7+;Ym|L)f*gCtbeBwJ9h(7@soyoQOwoh=Wt@=7l}n}{_LHfdeD{u+r_^u*uoC}? z>k+envFR^iQ^%*s@plpOlRCD-f0BB~k5gX5B&MdwZzc8@UL16h{+O{M#}eR+sjp&V zdu5R42^%Y32hIbAXK?+RxjTb=7g4U2%!RcxwvS^u{*yRFJP8~_ zyl*3$lHK#&q)0hS0;93t<_qwj)M?{mtmt>i@&YCFvo`Xr7jq)#E(r|8PI;Y;|3q&! z-T^KLMrD3gk{JXnY?5QE5%blwv}gkGDEa@g{v2=ucGQBWX2-R)6AwonfwblG*mCTT z@GpjHe1i4<-9O;CTQ!U9$Js;vkIa@&Z3tY7{gm-4j3vNTz!rhW+N$w{8*>p3JC(Hz z1kS|{v5Zw1tAT5=1%P?5#41=RsOKz5U18L(5- z5wz!g;uJ>(>j@wQwgK6_;W|5V?(OKrb}z;%kP6mSMhYy+vCqw7Ci&V|Ue8yETO1YO z&L|_3U^+;@Oro@*G;%sP2Yc&4*XHE$|N(pK+T0bZKdY4RFcLl!1QNdnhNWtAp z@*BPyTe6XN1F^oq-UU(t4hcL~32c9o-|PpSa%|Nk{0pNRj|Cp9MDS0N-RwiX5^TW$ z;0^o>W9`@{$Lz4E1|O{me11q`roK5i|?+^DVf$Sofx`#C9d- z**4_&r&aGbHPAJX2x|4Ky~{PD=7s?;;y>||#>eRBghK0mZ1i5f=`_?2>j~yP>06EY zz}5k-heYrV*4)Uc&bc(wNj43rz2bH9zmH#P=!Z3Xdj&YqIhT6+VGq3ClRRy_25gz} zwUItW0+PwuSZf7reik<_*WUc!G;XC2+xYig0Uok+aaJzjAW#rPmAIkFjWKmL<9 zR=f%vl*yHoQX7Ib;a-aWr1glqvBO+NK4Tbg0r}bR@kCM{E*!PsAd`3&6>!V@jzF0KNr0kN<=#YRtzDblteMOD}mg3hOqa z=ZoxO1@K#7_ZIGh%1#0$JMC&j)4L?m z4F}G{R;zkXl)fI_*vVo#fj0-nV#l17eK|-GqX#?7{jj=sPl6ebHIxf|K&B#_n2j~o z8(iyVN;U>zZGdLsf5R2+#Ufxb+O(CaO|T9p?H-$;$X~3)HlJyqd4eW@}fwdp)tNSk$ zjpG4eBl@rl@V^O)e8&@Hpd*Wa#KBC|T|=51gB?)XwxTW+8{nOQ6hi;B|3Fa-v- zNbWt4_3=ujD}zKlOvU=e?&6(@wraK;YXH>kJM>d+`=<3HGsA+Hfegub>D2o2keyreTMSY7JIK zcEB2%EyMrjD%y*cSog*}ACOdd8vs89-p2oiE0V=RY|Fx?HSVVb(jV)-J_G*=pr{?Q zuuX329`EcRtQ)(w;QGgEY&-P$hIdR{j0LX8I%{}Ol)e@pVhuF5ZF-l)-ALdf>=mN- z#3_5s1FG<1l117TWG1H3l@if+?uc+$)rI-DHKcJ58X<|9n&Q)6!N^C3O8sG!` zCtOkDU2F-^=+-Z{6xDiIyVAM%Pr$s!4B#ZRZ>LY|!n%`B!+%2NB_2g5OGym}1JB|= zLA_!Muy5+CD5ZBCeX9l0LTpunNMh>&H{(BfL&rnt;agHT8h9W7iJWz;!VbIli_?o=H{%M+LbOB6Vy{ao%oS8t58`H1uWsCx5ee0ySV0M>pYL1odJGIxth54I{tH zQqBlCZjr=}#lIMO#|vn;pXA6h_!q_6p)ciAYcdiIc<>coGK`?LKeuC(MBT@zzu{ejupU|cta7%-u8rh<$Gtuafk%-7-0%TZG5B@jwJiym=bja zA^~*(uaIql92a#2B7vPvcE#}qb~2VoX$&K~gG0X`Id_pLt|NVMbYn~EMM`HZwk$nO zf!r89YmtZ^rv2jR!M>}Al-2~=FOWwZ@)e2hW!fu{2_DIc#C9gF7snizOhp_F1s2m< zft>D=sfd$nNM0O^fWba#iul-?VBFR$0z6z9O1p;XU`pFl$* zGeaUuB7A+UlJj%*5>xV%QuQiw3qT4OY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb z09I0xZL1XF8=&BvUzDm~re~mMpk&9TprBw=l#*r@P?Wt5Z@Sn2DRmzV36 z8|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{ zw8U0P31kr*K-^i9nTD__uNdkrpa=CqGWv#k2KsQbfm&@qqE`MznW;dVLFU^T+JIG} zh(YbK(Fa+MKRTvx2FYd*p|Azy8r&ny_XiPHVwMw{9)s@A=yKisR({B?9Rl9eIu=EZ^31X&w2l7{B`NoIU4UjP6aS zbm6FytlPd}{gJK-)juOOj(pf5z$dh#C4%#D)Qxy=bysoETlYk~_spNbx#J1@-%|?W z_beyg>YaX1EQqU{hCOx{#yy&4bq|FJHLD>nHqTP_Og#ZeoS8RV|%d5fpw~X zCVZEXV?Me4^3tG_i&qvWG5_{wojfTc!z0&a=Y8jfxD$LX`}+kix@l%ky=dKaDrS+W z$_hV^kCI>hEs@bot+rYj<cjvI+bvJcGRrJ)`lfB_TOyGvHl>M!1gVvlaq77>;COqM7Eqy zW7?xTm&NX|`hnN}6Q=USG+ql!j}M#mEs~3Wos#nQH8K(`<_?*E_8IV5aelGR6Ons+ zMfZc|=Z-(0MCCrE#mziaKKp68KA*WQ->a0Qc+O?tt&1574fr4OL>DL@j|^bH7tiZA zMLX}nK`BwU6M=H)-iS{MZcUxntjxczmoxr@-~73?`Kj{iS8x1i3VO9Zc;&0+(?YH4 z3zO$)9${jd*S=1&Lh^+1;j15=CACGOHgZ0_zhC8+?S#CXKa+$yf7YEB_*6RWyt-Vy zZpPZ<>)CZbRV1f>tg8EvFe_GMk$ZOJPkV>=C%UVJzw9|~&wXmT%J%Ms%oPE}H{0W8 zPd<>Eaq!jQ={lM^r+1r8VA*_J_(yl8>{I)@%+&OC3%NyA`?0!v+;-&|}|5?v7$e6NV*DJ2W6=#B+ru5$`^0*h$ByL>5 za;ENq{sHp_W(WHhDl+Die@3_}t(Ve1cTxD41#f-!r-a$_JB6QnceJ0a*0oCcsCwr0 zX6w6Gj&T2+)ym~$lU{KWQ6wX+oD9`R28^YfoUepZZ2&g9&0P zuYa#{zh~C9!Ne_=f7i|ZTwxrkAELLE1zh_ub7FIh(3;M#BDSq}_s`y3#j|d=v!HoU zecs0pTc$@#{@oS8wUc+@)_>nUR7@4JxA|ZDZL-n6OL{`&X8*OWer}Wg1Wf$pc2?Hz z?{_A{NefywKY!C#nJykD<-%0%vCyh;u{+bF+viR{{U`5#E7wWYXF>SHyA$4Cli2Kg zd7giH{M8qef_`l@QCcLP!(P)Msd?wDqo_gMDFd^&>*a2Hd#!b9JDyw1kUuHrOtY;@ z{xL~~`VD_}8Hlg`ta@NyqvwW&`#;^EKPhza_J{5Nk_CQVIn8bK^xpK+1?wHFH#C0z z`e^ZNy%SH}KeHW@xY4&j;a^qD>wD{2?!M)UY(7x*LzLOwP`gIu{j2p-xeurDbWRm7 e{O2so_m5ePzkZqE1nqKA{o(2A=d#Wzp$PzxiR>@{ literal 0 HcmV?d00001 diff --git a/app/assets/images/icons/sd_logo.png b/app/assets/images/icons/sd_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..178fca52f4ea00c397588f78a0de7c396b683bd4 GIT binary patch literal 4324 zcmds5i$5D#8=gd{<tf|+V*{K3ZBYOUkphHv zA>a-q_ye%_HUJz0O8^j}LjI0Yp?^go(o~_3w#+V&9(?xzNaE`64zJk-Xruud)JLYA>TuYfkr478$09~Ji!;K zt*WM~hB6jIB9VrKD}MS8W?%kU9lSC^T_X}h^f4F`iKI%>P{k9jVovGl>0#8=G3x3n zAVMW9BADnEt`Zz}^aIIXJZ8ROJ_P>|qCY+uxy$S2jlV%OLZNmOecV2@lj!gFcc$R5 zKf48X5VIS>oKjW8eB=hJ8t!`aZ3+IqVCG$Z<5Pz3k^krWGmjx=H~IfG=0i{4d%><6 ziy2}*4x6#qq08fY0AMf6%IvgLIE2Hpxh^wssA)x`YGn^X1ZE?o{+Xy+DH>saT=N7h zU0eK7#^(l}Jp~RjmDg(@h|9Wu^;zjpV)1Ax-&wm8D3p1SdA#LqDO0nfcat9FtJs_V zia4k?kef@_9z3MwnYv`C5ylo+zB1U%iC}J#X$TxS?EWMcfWY8bPkm^pJeNl?M=*L^ zHJGKjSjUZw_9RIx^)Xb%*W=)Po5d>(Vl=d#&-XOe5hT^_EHsC)xc${pFPz^IEupZp zmOb}PF?<~hR&!o|(Q)BRZ~NFor6@+uOAYQsdO<-&^2!9It=lH#&ZqJwax-6ulg}9+WNf?DSFGX{{6puR zJ0B$^)@(Ac5&VwqMaH>dwUO!D#Ik)3ds6^PdY`VNFl0YC)QP8hK8GHLK+PwuWnk8z zUdNiO(?qXeHnZ zpG{6(`J%?Xex3f@cA9x;>A4JYE>Ry}v=auJ2D)Gx0O(T?hKt<}AQTb{Zs zlTpwc*N|*4eI!eF%3cA=LmY}A+UJmu8N_pCDn=vfg1by;7W~5^Vsp6KbL7)DvK|>E zw7D(TX{_F0Zlf?aYMxf@!Cc~*UubUICfGhCPaACDp0byhq;k__A7(nmkkAf`6Ulu6 zPvqp23dOAg2!n7mPvsX0-x{-qZ@|I5^0Fc^#xw?Nk9HJ-90e8n{&deVs2HeFzO@AG zTTr1dMQpM~(V#-{>RNC$@QyloNHYjU1@9N>wqEQfNz(9|-88wieIl|)OLoilvAp25 zl3?X4-e-EbCY&WBnzmG}!9-Bw)oKgX!{kCGxx)G!pVV1*%zEC8g5dQSzZuhQYj@v$ zm^l;@UqX8*I4Dh zsU|&(U;A3*l3-5zfb z;hHsZA+~F@juc# z=#%`aqJvN`aX8wGd2bQ^ofsVL5S*k4uL5GFHX~>ELW;ph-PAuClF3e5uMKo(-8Jt} zU*0J3>kI5yoolTsXq)^#-Y2RoA~Iv^-P&4u;nIKunHF(*b!YT&FX8%8EQ{tq!3f1^F>^-(M=RBy80B~c<#zg~j=rsS zD%aqTHElUZX3i%oAHfj&C=1Tgm6sZ=32q96?plgt|GbGId3O%11`aQk;P?eS9QiGo zB?&cZ5fNW}OuZk^`x?rJ0H}-<((Z(&az+w%NLVzz z-1eIdMLR_Zw9vHCyI3^cBL;hXE6xS+Z-WgcRV?feSmwF8yR)|6t;t0W>={$tUD$nG z_t0>yG;y~MLtOO?so+7hgplxtAQ_!>N7iY?+LfJu%_3*}=l(jQmz&&Q0<5SqeJ1!c z1??P>$Q-OcFhRth($su?bBS%l`onL1J2zKhfW_kQnD;}SQ{}U|7)9hz?nzoNW~iQ0 zUmG(18xfP&wU&CsVA_wFYxIB|veFA`QS7d@kX7l{kz*WH;VE@zBW>h#duMjwT+Di8%r;Ksz7LO8@ITu`TCI_UAN( zMtV+I^RCs>s(gsVUXSPD_HN-DbA4r{r5!2j+}iOH=fX;Z?cBE3(08l8q_t@qS5Cp$ zg_4VO^rQNA!Hl@T>SCmWB2Lgy&_hMINu=w}6n9@LhDA~{-x+H#TzNxJud=l2Bc9F8 zY?>6O+X;Cz0mG2*; zEe#Efo>7x36%RLB*XZa%f2sRDGNV=a?m)(i#&vd+?4I+?ufWH2f3~9}1O|n_ML?9k VMNZyhn(Y2FurfbyR%YsX>%Sfy-jo0U literal 0 HcmV?d00001 diff --git a/app/assets/images/pontis_white.svg b/app/assets/images/pontis_white.svg new file mode 100644 index 0000000..9a636de --- /dev/null +++ b/app/assets/images/pontis_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/skdigital_biela.svg b/app/assets/images/skdigital_biela.svg new file mode 100644 index 0000000..a93f86c --- /dev/null +++ b/app/assets/images/skdigital_biela.svg @@ -0,0 +1,31 @@ + + + + skdigital_farba copy + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/us_embassy.png b/app/assets/images/us_embassy.png new file mode 100644 index 0000000000000000000000000000000000000000..d06b56f5c46f6d8befc2654747fce06dd0bfe59c GIT binary patch literal 123900 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 E8>hl7*8l(j literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 82b5d3c..38c8be4 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -6,7 +6,15 @@ @import 'projects'; @import 'static'; - +@import 'schedule'; +@import 'statuses'; +@import 'navbar'; +@import 'footer'; + +h1, h2, h3, h4, h5, h6 { + font-family: $headings-font-family; + font-weight: $font-weight-bold; +} .btn-outline-primary:hover { border-color: $link-hover-color; @@ -14,48 +22,13 @@ background-color: transparent; } -.navbar { - @include media-breakpoint-up(md) { - .navbar-brand img { - width: 240px; - } - } -} - -.container { - max-width: 1024px; +.table-title-item, .table-price { + font-family: $headings-font-family; } -footer { - background-color: $gray-dark; - color: $white; - - a { - color: $white; - &:hover { - color: $white; - } - } - - .fa-inverse { - color: $gray-dark; - } - - .icons { - a:hover { - text-decoration: none; - } - } +#filterForm.collapsing { + transition: height 0.5s ease-out; - .newsletter { - .legal { - font-size: $small-font-size; - - a { - text-decoration: underline; - } - } - } } h1 .badge-alpha { diff --git a/app/assets/stylesheets/footer.scss b/app/assets/stylesheets/footer.scss new file mode 100644 index 0000000..ce3155b --- /dev/null +++ b/app/assets/stylesheets/footer.scss @@ -0,0 +1,85 @@ +footer { + background-color: $light_black; + color: $white; + + a { + color: $white; + &:hover { + color: $white; + } + } + + .fa-inverse { + color: $gray-dark; + } + + .icons { + a:hover { + text-decoration: none; + } + } + + .newsletter { + .legal { + font-size: $small-font-size; + + a { + text-decoration: underline; + } + } + } +} + +.footer-border { + border-left: 1px solid; + padding-left: 3rem; +} + +.footer-sk { + padding-right: 3rem; +} + +.pontis-logo { + width: 125px; +} + +.us-logo, .acf-logo { + width: 200px; +} +@media (max-width: 1200px) { + .pontis-logo { + width: 100px; + } + .us-logo, .acf-logo { + width: 175px; + } +} +@media (max-width: 992px) { + .pontis-logo { + width: 75px; + } + .us-logo, .acf-logo { + width: 150px; + } +} +@media (max-width: 768px) { + .responsive-img { + width: 125px; + } + .footer-border { + border-top: 1px solid; + border-left: none; + padding-left: 2rem; + padding-right: 2rem; + padding-top: 3rem; + } + .footer-sk { + padding-right: 0; + padding-bottom: 3rem; + } +} +@media (max-width: 576px) { + .responsive-img { + width: 100px; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/google-fonts.scss b/app/assets/stylesheets/google-fonts.scss index 44c13bf..749a37f 100644 --- a/app/assets/stylesheets/google-fonts.scss +++ b/app/assets/stylesheets/google-fonts.scss @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); + /* cyrillic-ext */ @font-face { font-family: 'Merriweather'; diff --git a/app/assets/stylesheets/navbar.scss b/app/assets/stylesheets/navbar.scss new file mode 100644 index 0000000..d047c9e --- /dev/null +++ b/app/assets/stylesheets/navbar.scss @@ -0,0 +1,29 @@ +.navbar { + @include media-breakpoint-up(md) { + .navbar-brand img { + width: 100%; + } + } +} + +.navbar-light .navbar-nav .nav-link { + font-weight: $font-weight-semi-bold; + transition: 0.2s ease-in-out; + color: $black; +} + +.navbar-light .navbar-nav .nav-link:focus { + color: $black; +} + +.navbar-light .navbar-nav .nav-link:hover { + color: $blue; +} + +.navbar-light .navbar-nav .nav-link:active { + color: $blue; +} + +.navbar-light .navbar-nav .nav-item.active .nav-link { + color: $blue; +} \ No newline at end of file diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss index 52293af..cbfbf11 100644 --- a/app/assets/stylesheets/projects.scss +++ b/app/assets/stylesheets/projects.scss @@ -109,13 +109,12 @@ img.emoji { } .accordion:after { - font-family: 'Merriweather'; - font-size: $h3-font-size; - content: "-"; + color: $blue; + font-size: medium; + text-decoration: underline; + content: "Schovať"; } .accordion.collapsed:after { - font-family: 'Merriweather'; - font-size: $h3-font-size; - content: "+"; + content: "Zobraziť"; } diff --git a/app/assets/stylesheets/schedule.scss b/app/assets/stylesheets/schedule.scss new file mode 100644 index 0000000..dc40fe4 --- /dev/null +++ b/app/assets/stylesheets/schedule.scss @@ -0,0 +1,100 @@ +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 100%; + word-wrap: break-word; +} + +.card-body { + flex: 1 1 auto; + padding: 1.25rem; +} +.vertical-timeline { + width: 100%; + position: relative; + padding: 1.5rem 0 1rem; +} + +.vertical-timeline::before { + content: ''; + position: absolute; + top: 0; + left: 67px; + height: 100%; + width: 4px; + background: #e9ecef; +} + +.vertical-timeline-element { + position: relative; + margin: 0 0 1rem; +} + +.vertical-timeline--animate .vertical-timeline-element-icon.bounce-in { + visibility: visible; + animation: cd-bounce-1 .8s; +} +.vertical-timeline-element-icon { + position: absolute; + top: 0; + left: 60px; +} + +.vertical-timeline-element-icon .badge-dot-xl { + box-shadow: 0 0 0 5px #fff; +} + +.badge-dot-xl { + width: 18px; + height: 18px; + position: relative; +} +.badge:empty { + display: none; +} + + +.badge-dot-xl::before { + content: ''; + width: 10px; + height: 10px; + border-radius: .25rem; + position: absolute; + left: 50%; + top: 50%; + margin: -5px 0 0 -5px; + background: #fff; +} + +.vertical-timeline-element-content { + position: relative; + margin-left: 90px; + font-size: .8rem; +} + +.vertical-timeline-element-content .timeline-title { + font-size: .8rem; + text-transform: uppercase; + margin: 0 0 .5rem; + padding: 2px 0 0; + font-weight: bold; +} + +.vertical-timeline-element-content .vertical-timeline-element-date { + display: block; + position: absolute; + left: -100px; + top: 0; + padding-right: 10px; + text-align: right; + color: #000000; + font-size: .7619rem; + white-space: nowrap; +} + +.vertical-timeline-element-content:after { + content: ""; + display: table; + clear: both; +} \ No newline at end of file diff --git a/app/assets/stylesheets/statuses.scss b/app/assets/stylesheets/statuses.scss new file mode 100644 index 0000000..6ead29f --- /dev/null +++ b/app/assets/stylesheets/statuses.scss @@ -0,0 +1,83 @@ +.state-badge { + border: 2px solid; + border-radius: 10px; + padding: 5px 15px; +} + +.status-1 { + background-color: rgba(255, 0, 0, 0.25); + color: #FF0000; + border-color: #FF0000; +} + +.status-2 { + background-color: rgba(255, 127, 127, 0.25); + color: #FF7F7F; + border-color: #FF7F7F; +} + +.status-3 { + background-color: rgba(255, 153, 153, 0.25); + color: #FF9999; + border-color: #FF9999; +} + +.status-4 { + background-color: rgba(255, 178, 178, 0.25); + color: #FFB2B2; + border-color: #FFB2B2; +} + +.status-5 { + background-color: rgba(255, 218, 185, 0.25); + color: #FFDAB9; + border-color: #FFDAB9; +} + +.status-6 { + background-color: rgba(240, 230, 140, 0.25); + color: #F0E68C; + border-color: #F0E68C; +} + +.status-7 { + background-color: rgba(255, 255, 224, 0.25); + color: #FFFFE0; + border-color: #FFFFE0; +} + +.status-8 { + background-color: rgba(144, 238, 144, 0.25); + color: #90EE90; + border-color: #90EE90; +} + +.status-9 { + background-color: rgba(60, 179, 113, 0.25); + color: #3CB371; + border-color: #3CB371; +} + +.status-10 { + background-color: rgba(0, 255, 255, 0.25); + color: #00FFFF; + border-color: #00FFFF; +} + +.status-11 { + background-color: rgba(135, 206, 250, 0.25); + color: #87CEFA; + border-color: #87CEFA; +} + +.status-12 { + background-color: rgba(65, 105, 225, 0.25); + color: #4169E1; + border-color: #4169E1; +} + +.status-13 { + background-color: rgba(0, 0, 255, 0.25); + color: #0000FF; + border-color: #0000FF; +} \ No newline at end of file diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss index 6a1f64a..6e55215 100644 --- a/app/assets/stylesheets/variables.scss +++ b/app/assets/stylesheets/variables.scss @@ -1,3 +1,5 @@ +$black: #000000; +$light_black: #141617; $gray-dark: #373a3c; $gray: #55595c; $gray-light: #818a91; @@ -5,7 +7,7 @@ $gray-lighter: #c4c3ca; $gray-lightest: #f4f4f4; $color-highlight: lightgoldenrodyellow; -$blue: #3a67e8; +$blue: #385EFF; $orange: #ffac33; $theme-colors: ( @@ -19,10 +21,12 @@ $theme-colors: ( dark: $gray-dark ); -$headings-font-family: 'Merriweather', 'Georgia', 'Times New Roman', serif; +$headings-font-family: 'Roboto Mono', 'Georgia', 'Times New Roman', serif; $font-family-sans-serif: 'Roboto', sans-serif; $font-weight-normal: 300; +$font-weight-semi-bold: 500; $font-weight-bold: 700; +$font-weight-black: 900; $modal-backdrop-bg: white; $modal-backdrop-opacity: .7; diff --git a/app/controllers/admin/metais/project_origins_controller.rb b/app/controllers/admin/metais/project_origins_controller.rb new file mode 100644 index 0000000..3812c70 --- /dev/null +++ b/app/controllers/admin/metais/project_origins_controller.rb @@ -0,0 +1,111 @@ +class Admin::Metais::ProjectOriginsController < ApplicationController + + def create + @project = Metais::Project.find(params[:project_id]) + @project_origin = @project.project_origins.build(project_origin_params) + if @project_origin.save + redirect_to admin_metais_project_path(@project), notice: 'Projekt bol úspešne vytvorený.' + else + render :new + end + end + + def edit + @project = Metais::Project.find(params[:project_id]) + @project_info = @project.get_project_origin_info + @project_origin = Metais::ProjectOrigin.find(params[:id]) + @project_origins = @project.project_origins + + @assumption_events = [] + @real_events = [] + @project_origins.each do |project_origin| + @assumption_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Predpoklad'))) + @real_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Realita'))) + end + @assumption_events.sort_by! { |event| event.date } + @real_events.sort_by! { |event| event.date } + + @all_suppliers = [] + @project_origins.each do |project_origin| + @all_suppliers.concat(project_origin.suppliers) + end + @all_suppliers.sort_by! { |event| event.date || DateTime::Infinity.new } + + @project_origin = Metais::ProjectOrigin.includes(:documents, :suppliers).find(@project_origin.id) + end + + def update + @project = Metais::Project.find(params[:project_id]) + @project_origin = Metais::ProjectOrigin.find(params[:id]) + + if @project_origin.update(project_origin_params.except(:project_events)) + if project_origin_params[:project_events] + event_params = project_origin_params[:project_events] + unless event_params.values.all?(&:blank?) + @project_origin.events.create!(name: event_params[:name], value: event_params[:value], date: event_params[:date], origin_type: Metais::OriginType.find_by(name: 'Human')) + end + end + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Projekt bol úspešne aktualizovaný.' + else + render :edit + end + end + + def add_event + @project = Metais::Project.find(params[:project_id]) + @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) + @project_info = @project.get_project_origin_info + + origin_type = Metais::OriginType.find_by(name: params[:event][:origin_type]) + event_type = Metais::ProjectEventType.find_by(name: params[:event][:event_type]) + event_params = params.require(:event).permit(:name, :value, :date) + .merge(origin_type: origin_type, event_type: event_type) + @event = @project_origin.events.create(event_params) + + if @event.save + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Event bol úspešné pridaný.' + else + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), alert: 'Error encountered while creating an event: ' + @event.errors.full_messages.to_sentence + end + end + + def add_supplier + @project = Metais::Project.find(params[:project_id]) + @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) + + origin_type = Metais::OriginType.find_by(name: params[:supplier][:origin_type]) + supplier_type = Metais::SupplierType.find(params[:supplier][:supplier_type]) + + supplier_params = params.require(:supplier).permit(:name, :value) + .merge(name: params[:supplier][:value], origin_type: origin_type, supplier_type: supplier_type) + @supplier = @project_origin.suppliers.create(supplier_params) + + if @supplier.save + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Supplier bol úspešné pridaný.' + else + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), alert: 'Error encountered while creating an event: ' + @event.errors.full_messages.to_sentence + end + end + + def remove_event + @project = Metais::Project.find(params[:project_id]) + @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) + @project_event = Metais::ProjectEvent.find(params[:event_id]) + @project_event.destroy + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Event z harmonogramu bol úspešne odstránený.' + end + + def remove_supplier + @project = Metais::Project.find(params[:project_id]) + @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) + @project_supplier = Metais::ProjectSupplier.find(params[:supplier_id]) + @project_supplier.destroy + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Supplier bol úspešne odstránený.' + end + + private + + def project_origin_params + params.require(:metais_project_origin).permit(:title, :description, :guarantor, :project_manager, :start_date, :end_date, :status, :phase, :finance_source, :investment, :operation, :approved_investment, :approved_operation, :supplier, :targets_text, :documents_text, :links_text, project_events: [:name, :value, :date]) + end +end \ No newline at end of file diff --git a/app/controllers/admin/metais/projects_controller.rb b/app/controllers/admin/metais/projects_controller.rb new file mode 100644 index 0000000..2752c67 --- /dev/null +++ b/app/controllers/admin/metais/projects_controller.rb @@ -0,0 +1,91 @@ +class Admin::Metais::ProjectsController < AdminController + def index + per_page = 25 + page = params[:page] || 1 + + @projects = Metais::Project.all + + if filtering_params_present? + @projects = @projects.joins(:project_origins) + end + + if params[:guarantor].present? + @projects = @projects.where('project_origins.guarantor = ?', params[:guarantor]) + end + + if params[:title].present? + @projects = @projects.where('project_origins.title ILIKE ?', "%#{params[:title]}%") + end + + if params[:status].present? + @projects = @projects.where('project_origins.status = ?', params[:status]) + end + + if params[:code].present? + @projects = @projects.where('metais.projects.code = ?', params[:code]) + end + + if params[:min_price].present? + @projects = @projects.where('COALESCE(project_origins.investment, 0) >= ?', params[:min_price]) + end + + if params[:max_price].present? + @projects = @projects.where('COALESCE(project_origins.investment, 0) <= ?', params[:max_price]) + end + + case params[:change_date] + when 'latest' + @projects = @projects.order(updated_at: :desc) + when 'oldest' + @projects = @projects.order(updated_at: :asc) + end + + @projects = @projects.distinct + @projects = @projects.page(page).per(per_page) + end + + + def show + @project = Metais::Project.find(params[:id]) + end + def edit + @project = Metais::Project.find(params[:id]) + end + + def update + @project = Metais::Project.find(params[:id]) + if @project.update(project_params) + redirect_to admin_metais_project_path(@project), notice: 'Project was successfully updated.' + else + render :edit + end + end + + def create_human_origin + @project = Metais::Project.find(params[:id]) + origin_type = Metais::OriginType.find_by(name: 'Human') + fail 'OriginType not found' unless origin_type + + human_origin = Metais::ProjectOrigin.find_or_initialize_by(project: @project, origin_type: origin_type) + if human_origin.new_record? + human_origin.title = @project.project_origins.first.title + human_origin.save! + end + redirect_to edit_admin_metais_project_project_origin_path(@project, human_origin) + end + + private + + def filtering_params_present? + params[:guarantor].present? || + params[:title].present? || + params[:status].present? || + params[:code].present? || + params[:min_price].present? || + params[:max_price].present? + end + + def project_params + params.require(:project).permit(:guarantor, :finance_source, :status) + end +end diff --git a/app/controllers/metais/projects_controller.rb b/app/controllers/metais/projects_controller.rb new file mode 100644 index 0000000..5994e0d --- /dev/null +++ b/app/controllers/metais/projects_controller.rb @@ -0,0 +1,92 @@ +class Metais::ProjectsController < ApplicationController + def index + per_page = 25 + page = params[:page] || 1 + + @projects = Metais::Project.all + + if params[:guarantor].present? || params[:title].present? || params[:status].present? || params[:code].present? || params[:min_price].present? || params[:max_price].present? || params[:sort].present? + @projects = @projects.joins(:project_origins) + end + + if params[:guarantor].present? + @projects = @projects.where('project_origins.guarantor = ?', params[:guarantor]) + end + + if params[:title].present? + project_ids = @projects.where('project_origins.title ILIKE ?', "%#{params[:title]}%").distinct.pluck(:id) + @projects = @projects.where(id: project_ids) + end + + if params[:status].present? + @projects = @projects.where('project_origins.status = ?', params[:status]) + end + + if params[:code].present? + @projects = @projects.where('code = ?', params[:code]) + end + + if params[:min_price].present? + @projects = @projects.where('project_origins.investment >= ?', params[:min_price]) + end + + if params[:max_price].present? + @projects = @projects.where('project_origins.investment <= ?', params[:max_price]) + end + + if params[:has_evaluation].present? + @projects = @projects.joins(:combined_projects) + + if params[:has_evaluation] == 'yes' + @projects = @projects.where.not(combined_projects: { evaluation_id: nil }) + elsif params[:has_evaluation] == 'no' + @projects = @projects.where(combined_projects: { evaluation_id: nil }) + end + end + + case params[:sort] + when 'alpha' + @projects = @projects.order('project_origins.title ASC') + @projects = @projects.distinct.select("metais.projects.*, project_origins.title") + when 'date' + @projects = @projects.order('project_origins.metais_created_at ASC') + @projects = @projects.distinct.select("metais.projects.*, project_origins.metais_created_at") + + when 'price' + @projects = @projects.order('project_origins.investment ASC') + @projects = @projects.distinct.select("metais.projects.*, project_origins.investment") + + else + @projects = @projects.order(updated_at: :desc) + @projects = @projects.distinct.select("metais.projects.*") + + end + + @projects = @projects.page(page).per(per_page) + end + + def show + @project = Metais::Project.find(params[:id]) + @combined_project = CombinedProject.find_by(metais_project_id: @project.id) + @project_info = @project.get_project_origin_info + @project_origins = @project.project_origins + + @assumption_events = [] + @real_events = [] + @project_origins.each do |project_origin| + @assumption_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Predpoklad'))) + @real_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Realita'))) + end + @assumption_events.sort_by! { |event| event.date } + @real_events.sort_by! { |event| event.date } + + @all_suppliers = [] + @project_origins.each do |project_origin| + @all_suppliers.concat(project_origin.suppliers) + end + @all_suppliers.sort_by! { |event| event.date || DateTime::Infinity.new } + + @project_origin = @project.project_origins.first + @project_origin = Metais::ProjectOrigin.includes(:documents, :suppliers).find(@project_origin.id) + end +end diff --git a/app/controllers/phase_revision_controller.rb b/app/controllers/phase_revision_controller.rb index 82a4b62..c46be23 100644 --- a/app/controllers/phase_revision_controller.rb +++ b/app/controllers/phase_revision_controller.rb @@ -2,6 +2,7 @@ class PhaseRevisionController < ApplicationController def show @project = Project.find_by!(id: params[:project_id]) @phase_revision = PhaseRevision.find_published_revision(@project.id, params[:revision_type]) + @combined_project = CombinedProject.find_by(evaluation_id: @project.id) if @phase_revision @revision = @phase_revision.revision diff --git a/app/helpers/metais/projects_helper.rb b/app/helpers/metais/projects_helper.rb new file mode 100644 index 0000000..ffd2f89 --- /dev/null +++ b/app/helpers/metais/projects_helper.rb @@ -0,0 +1,33 @@ +module Metais::ProjectsHelper + def origin_type_logo(origin_type) + if origin_type.is_a?(Integer) + case origin_type + when 3 + 'icons/sd_logo.png' + when 2 + 'icons/ai_logo.png' + else + 'icons/metais_logo.png' + end + else + case origin_type.name + when 'Human' + 'icons/sd_logo.png' + when 'AI' + 'icons/ai_logo.png' + else + 'icons/metais_logo.png' + end + end + end + + def convert_to_list(text) + items = text.split('\n') + + list_items = items.map do |item| + ActionController::Base.helpers.sanitize("
  • #{item}
  • ") + end + + ActionController::Base.helpers.sanitize("
      #{list_items.join}
    ") + end +end diff --git a/app/jobs/link_metais_projects_and_evaluations_job.rb b/app/jobs/link_metais_projects_and_evaluations_job.rb new file mode 100644 index 0000000..a2035d4 --- /dev/null +++ b/app/jobs/link_metais_projects_and_evaluations_job.rb @@ -0,0 +1,16 @@ +class LinkMetaisProjectsAndEvaluationsJob < ApplicationJob + queue_as :default + + def perform + Metais::Project.find_each do |metais_project| + code = metais_project.code + + evaluation = Project.find_by(metais_code: code) + + CombinedProject.create!( + evaluation: evaluation, + metais_project: metais_project + ) + end + end +end diff --git a/app/jobs/metais/daily_sync_projects_job.rb b/app/jobs/metais/daily_sync_projects_job.rb new file mode 100644 index 0000000..895bd0a --- /dev/null +++ b/app/jobs/metais/daily_sync_projects_job.rb @@ -0,0 +1,13 @@ +class Metais::DailySyncProjectsJob < ApplicationJob + queue_as :metais + + def perform + Datahub::Metais::Project.where('updated_at > ?', Time.now - 1.day).find_each do |metais_project| + project = Metais::Project.find_or_initialize_by(code: metais_project.latest_version.kod_metais, + uuid: metais_project.uuid) + project.save! + + Metais::SyncProjectJob.perform_later(project, metais_project) + end + end +end diff --git a/app/jobs/metais/initial_sync_projects_job.rb b/app/jobs/metais/initial_sync_projects_job.rb new file mode 100644 index 0000000..884145d --- /dev/null +++ b/app/jobs/metais/initial_sync_projects_job.rb @@ -0,0 +1,13 @@ +class Metais::InitialSyncProjectsJob < ApplicationJob + queue_as :metais + + def perform + Datahub::Metais::Project.find_each do |metais_project| + project = Metais::Project.find_or_initialize_by(code: metais_project.latest_version.kod_metais, + uuid: metais_project.uuid) + project.save! + + Metais::SyncProjectJob.perform_later(project, metais_project) + end + end +end diff --git a/app/jobs/metais/sync_project_documents_job.rb b/app/jobs/metais/sync_project_documents_job.rb new file mode 100644 index 0000000..409e52e --- /dev/null +++ b/app/jobs/metais/sync_project_documents_job.rb @@ -0,0 +1,17 @@ +class Metais::SyncProjectDocumentsJob < ApplicationJob + queue_as :metais + + def perform(project_origin, metais_project) + metais_project.documents.each do |document| + project_document = Metais::ProjectDocument.find_or_initialize_by(uuid: document.uuid, + project_origin: project_origin, + origin_type: Metais::OriginType.find_by(name: 'MetaIS')) + + project_document.name = document.latest_version.nazov + project_document.filename = document.latest_version.filename + project_document.value = 'https://metais.vicepremier.gov.sk/dms/file/' + document.uuid + + project_document.save! + end + end +end diff --git a/app/jobs/metais/sync_project_events_job.rb b/app/jobs/metais/sync_project_events_job.rb new file mode 100644 index 0000000..f5edd8d --- /dev/null +++ b/app/jobs/metais/sync_project_events_job.rb @@ -0,0 +1,248 @@ +class Metais::SyncProjectEventsJob < ApplicationJob + queue_as :metais + + def perform(project_origin, metais_project) + origin_type = Metais::OriginType.find_by(name: 'MetaIS') + project_changes = Datahub::Metais::ProjectChange.where(project_version: metais_project.latest_version) + + project_changes.each do |change| + if change.field == 'status' + event_date = change.created_at + event_name = 'Zmena stavu projektu' + event_value = "Stav projektu bol zmenený z + #{Datahub::Metais::CodelistProjectState.find_by(code: change.old_value)&.nazov || change.new_value} na + #{Datahub::Metais::CodelistProjectState.find_by(code: change.new_value)&.nazov || change.new_value}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'zmena_stavu' + event_date = change.created_at + event_name = 'Zmena dátumu pre stav projektu' + event_value = "Dátum pre stav projektu bol zmenený z + #{change.old_value.present? ? "#{DateTime.parse(change.old_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"} na + #{change.new_value.present? ? "#{DateTime.parse(change.new_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, date: event_date) + project_event.save! + elsif change.field == 'nazov' + event_date = change.created_at + event_name = 'Zmena názvu projektu' + event_value = "Názov projektu bol zmenený z #{change.old_value} na #{change.new_value}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, date: event_date) + project_event.save! + + elsif change.field == 'faza_projektu' + event_date = change.created_at + event_name = 'Zmena fázy projektu' + event_value = "Fáza projektu bola zmenená z + #{Datahub::Metais::CodelistProjectPhase.find_by(code: change.old_value)&.nazov || change.old_value} na + #{Datahub::Metais::CodelistProjectPhase.find_by(code: change.new_value)&.nazov || change.new_value}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + elsif change.field == 'program' + event_date = change.created_at + event_name = 'Zmena zdroja financovania projektu' + event_value = "Zdroj financovania projektu bol zmenený z + #{Datahub::Metais::CodelistProgram.find_by(uuid: change.old_value)&.nazov || change.old_value} na + #{Datahub::Metais::CodelistProgram.find_by(uuid: change.new_value)&.nazov || change.new_value}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + elsif change.field == 'schvalene_rocne_naklady' + event_date = change.created_at + event_name = 'Zmena schválených ročných nákladov projektu' + event_value = "Schválené ročné náklady projektu boli zmenené z + #{change.old_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.old_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"} na + #{change.new_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.new_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'schvaleny_rozpocet' + event_date = change.created_at + event_name = 'Zmena schváleného rozpočtu projektu' + event_value = "Schválený rozpočet projektu bol zmenený z + #{change.old_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.old_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"} na + #{change.new_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.new_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'suma_vydavkov' + event_date = change.created_at + event_name = 'Zmena rozpočtu projektu' + event_value = "Rozpočet projektu bol zmenený z + #{change.old_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.old_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"} na + #{change.new_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.new_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'rocne_naklady' + event_date = change.created_at + event_name = 'Zmena ročných nákladov projektu' + event_value = "Ročné náklady projektu boli zmenené z + #{change.old_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.old_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"} na + #{change.new_value.to_s.empty? ? 'N/A' : "#{number_to_currency(change.new_value.to_f, unit: '€', separator: ',', delimiter: ' ', format: '%n %u')}"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'datum_zacatia' + event_date = change.created_at + event_name = 'Zmena dátumu začatia projektu' + event_value = "Dátum začatia projektu bol zmenený z + #{change.old_value.present? ? "#{DateTime.parse(change.old_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"} na + #{change.new_value.present? ? "#{DateTime.parse(change.new_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'termin_ukoncenia' + event_date = change.created_at + event_name = 'Zmena termínu ukončenia projektu' + event_value = "Termín ukončenia projektu bol zmenený z + #{change.old_value.present? ? "#{DateTime.parse(change.old_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"} na + #{change.new_value.present? ? "#{DateTime.parse(change.new_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'prijimatel' + event_date = change.created_at + event_name = 'Zmena gestora projektu' + event_value = "Gestor projektu bol zmenený z #{change.old_value.present?? change.old_value : 'N/A'} na #{change.new_value.present?? change.new_value : 'N/A'}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'datum_nfp' + event_date = change.created_at + event_name = 'Zmena dátumu NFP projektu' + event_value = "Dátum NFP projektu bol zmenený z + #{change.old_value.present? ? "#{DateTime.parse(change.old_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"} na + #{change.new_value.present? ? "#{DateTime.parse(change.new_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'link_nfp' + event_date = change.created_at + event_name = 'Zmena NFP projektu' + event_value = "NFP projektu bolo zmenené z #{change.old_value.present?? change.old_value : 'N/A'} na #{change.new_value.present?? change.new_value : 'N/A'}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'zmluva_o_dielo' + event_date = change.created_at + event_name = 'Zmena dátumu CRZ projektu' + event_value = "Dátum CRZ projektu bol zmenený z + #{change.old_value.present? ? "#{DateTime.parse(change.old_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"} na + #{change.new_value.present? ? "#{DateTime.parse(change.new_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'zmluva_o_dielo_crz' + event_date = change.created_at + event_name = 'Zmena CRZ projektu' + event_value = "CRZ projektu bolo zmenené z #{change.old_value.present?? change.old_value : 'N/A'} na #{change.new_value.present?? change.new_value : 'N/A'}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'vyhlasenie_vo' + event_date = change.created_at + event_name = 'Zmena dátumu VO projektu' + event_value = "Dátum VO projektu bol zmenený z + #{change.old_value.present? ? "#{DateTime.parse(change.old_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"} na + #{change.new_value.present? ? "#{DateTime.parse(change.new_value).in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y')}" : "N/A"}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + + elsif change.field == 'vo' + event_date = change.created_at + event_name = 'Zmena VO projektu' + event_value = "VO projektu bolo zmenené z #{change.old_value.present?? change.old_value : 'N/A'} na #{change.new_value.present?? change.new_value : 'N/A'}" + + project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, + origin_type: origin_type, + name: event_name, + value: event_value, + date: event_date) + project_event.save! + end + end + end +end diff --git a/app/jobs/metais/sync_project_job.rb b/app/jobs/metais/sync_project_job.rb new file mode 100644 index 0000000..84d362e --- /dev/null +++ b/app/jobs/metais/sync_project_job.rb @@ -0,0 +1,30 @@ +class Metais::SyncProjectJob < ApplicationJob + queue_as :metais + + def perform(project, metais_project) + project_origin = Metais::ProjectOrigin.find_or_initialize_by(project: project, + origin_type: Metais::OriginType.find_by(name: 'MetaIS')) + + project_origin.title = metais_project.latest_version.nazov + project_origin.description = metais_project.latest_version.popis + project_origin.status = Datahub::Metais::CodelistProjectState.find_by(code: metais_project.latest_version.status)&.nazov || metais_project.latest_version.status + project_origin.phase = Datahub::Metais::CodelistProjectPhase.find_by(code: metais_project.latest_version.faza_projektu)&.nazov || metais_project.latest_version.faza_projektu + project_origin.guarantor = metais_project.latest_version.prijimatel + + project_origin.metais_created_at = metais_project.latest_version.metais_created_at + project_origin.start_date = metais_project.latest_version.datum_zacatia + project_origin.end_date = metais_project.latest_version.termin_ukoncenia + + project_origin.finance_source = Datahub::Metais::CodelistProgram.find_by(uuid: metais_project.latest_version.program)&.nazov || metais_project.latest_version.program + project_origin.investment = metais_project.latest_version.suma_vydavkov + project_origin.operation = metais_project.latest_version.rocne_naklady + project_origin.approved_investment = metais_project.latest_version.schvaleny_rozpocet + project_origin.approved_operation = metais_project.latest_version.schvalene_rocne_naklady + + project_origin.save! + + Metais::SyncProjectSuppliersJob.perform_later(project_origin, metais_project) + Metais::SyncProjectDocumentsJob.perform_later(project_origin, metais_project) + Metais::SyncProjectEventsJob.perform_later(project_origin, metais_project) + end +end diff --git a/app/jobs/metais/sync_project_suppliers_job.rb b/app/jobs/metais/sync_project_suppliers_job.rb new file mode 100644 index 0000000..6b69b89 --- /dev/null +++ b/app/jobs/metais/sync_project_suppliers_job.rb @@ -0,0 +1,49 @@ +class Metais::SyncProjectSuppliersJob < ApplicationJob + queue_as :metais + + ORIGIN_TYPE = Metais::OriginType.find_by(name: 'MetaIS') + + def perform(project_origin, metais_project) + if metais_project.latest_version.link_nfp.present? + supplier_type = Metais::SupplierType.find_by(name: "NFP") + + project_supplier = Metais::ProjectSupplier.find_or_initialize_by( + name: metais_project.latest_version.link_nfp, + value: metais_project.latest_version.link_nfp, + date: metais_project.latest_version.datum_nfp, + project_origin: project_origin, + origin_type: ORIGIN_TYPE, + supplier_type: supplier_type) + + project_supplier.save! + end + + if metais_project.latest_version.vo.present? + supplier_type = Metais::SupplierType.find_by(name: "VO") + + project_supplier = Metais::ProjectSupplier.find_or_initialize_by( + name: metais_project.latest_version.vo, + value: metais_project.latest_version.vo, + date: metais_project.latest_version.vyhlasenie_vo, + project_origin: project_origin, + origin_type: ORIGIN_TYPE, + supplier_type: supplier_type) + + project_supplier.save! + end + + if metais_project.latest_version.zmluva_o_dielo_crz.present? + supplier_type = Metais::SupplierType.find_by(name: "CRZ") + + project_supplier = Metais::ProjectSupplier.find_or_initialize_by( + name: metais_project.latest_version.zmluva_o_dielo_crz, + value: metais_project.latest_version.zmluva_o_dielo_crz, + date: metais_project.latest_version.zmluva_o_dielo, + project_origin: project_origin, + origin_type: ORIGIN_TYPE, + supplier_type: supplier_type) + + project_supplier.save! + end + end +end diff --git a/app/jobs/set_metais_codes_for_projects_job.rb b/app/jobs/set_metais_codes_for_projects_job.rb new file mode 100644 index 0000000..44ef198 --- /dev/null +++ b/app/jobs/set_metais_codes_for_projects_job.rb @@ -0,0 +1,93 @@ +class SetMetaisCodesForProjectsJob < ApplicationJob + queue_as :default + + MAPPING = { + 9241 => 2477, + 8848 => 1734, + 8989 => nil, + 4284 => 326, + 7705 => 1097, + 8467 => 1840, + 4228 => 1552, + 6349 => 1616, + 6175 => 320, + 6054 => 515, + 8024 => 1247, + 8972 => 2263, + 6257 => 329, + 8119 => 1450, + 6258 => 491, + 4288 => 692, + 7713 => 1154, + 5599 => 476, + 4211 => 487, + 5645 => 471, + 5909 => nil, + 5033 => 346, + 6215 => 530, + 4493 => 347, + 4196 => 327, + 7712 => 1060, + 5048 => 376, + 4929 => 368, + 6309 => 578, + 4237 => 2545, + 6018 => 514, + 6350 => 565, + 4576 => 307, + 4599 => nil, + 6040 => 489, + 4283 => nil, + 8026 => 991, + 6216 => nil, + 5637 => 457, + 6308 => nil, + 6352 => nil, + 6017 => 483, + 5916 => 508, + 8255 => 1594, + 4898 => 473, + 5641 => 380, + 4431 => 361, + 4287 => nil, + 5640 => 1159, + 4035 => 611, + 4263 => 490, + 4262 => nil, + 8320 => 1760, + 5931 => 350, + 5928 => nil, + 5912 => 464, + 6346 => nil, + 8419 => 1479, + 8971 => 1774, + 5703 => 462, + 5911 => 488, + 4494 => nil, + 6256 => 536, + 6048 => 381, + 8462 => 2055, + 4229 => nil, + 4248 => 369, + 6347 => 778, + 8431 => 2054, + 8420 => 1934, + 4334 => 359, + 4329 => nil, + 4362 => 315, + 9041 => 2269, + 5402 => nil, + 4260 => 1704, + 6344 => 702, + } + + def perform + MAPPING.each do |project_id, metais_code| + project = Project.find_by_id(project_id) + if project + project.metais_code = "projekt_#{metais_code}" if metais_code + project.save + end + end + end +end diff --git a/app/models/combined_project.rb b/app/models/combined_project.rb new file mode 100644 index 0000000..40ce1c1 --- /dev/null +++ b/app/models/combined_project.rb @@ -0,0 +1,15 @@ +# == Schema Information +# +# Table name: combined_projects +# +# id :integer not null, primary key +# metais_project_id :integer not null +# evaluation_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# + +class CombinedProject < ApplicationRecord + belongs_to :metais_project, class_name: 'Metais::Project', foreign_key: 'metais_project_id' + belongs_to :evaluation, class_name: 'Project', foreign_key: 'evaluation_id', optional: true +end diff --git a/app/models/datahub/metais/codelist_program.rb b/app/models/datahub/metais/codelist_program.rb new file mode 100644 index 0000000..c85f957 --- /dev/null +++ b/app/models/datahub/metais/codelist_program.rb @@ -0,0 +1,3 @@ +class Datahub::Metais::CodelistProgram < DatahubRecord + self.table_name = 'metais.codelist_program' +end \ No newline at end of file diff --git a/app/models/datahub/metais/codelist_project_phase.rb b/app/models/datahub/metais/codelist_project_phase.rb new file mode 100644 index 0000000..42ec601 --- /dev/null +++ b/app/models/datahub/metais/codelist_project_phase.rb @@ -0,0 +1,3 @@ +class Datahub::Metais::CodelistProjectPhase < DatahubRecord + self.table_name = 'metais.codelist_project_phase' +end \ No newline at end of file diff --git a/app/models/datahub/metais/codelist_project_state.rb b/app/models/datahub/metais/codelist_project_state.rb new file mode 100644 index 0000000..6063468 --- /dev/null +++ b/app/models/datahub/metais/codelist_project_state.rb @@ -0,0 +1,3 @@ +class Datahub::Metais::CodelistProjectState < DatahubRecord + self.table_name = 'metais.codelist_project_state' +end \ No newline at end of file diff --git a/app/models/datahub/metais/project.rb b/app/models/datahub/metais/project.rb new file mode 100644 index 0000000..3fa5394 --- /dev/null +++ b/app/models/datahub/metais/project.rb @@ -0,0 +1,7 @@ +class Datahub::Metais::Project < DatahubRecord + self.table_name = "metais.projects" + + has_many :documents, class_name: 'Datahub::Metais::ProjectDocument', foreign_key: 'project_id' + has_many :versions, class_name: 'Datahub::Metais::ProjectVersion', foreign_key: 'project_id' + belongs_to :latest_version, class_name: 'Datahub::Metais::ProjectVersion', foreign_key: 'latest_version_id', optional: true +end \ No newline at end of file diff --git a/app/models/datahub/metais/project_change.rb b/app/models/datahub/metais/project_change.rb new file mode 100644 index 0000000..6effb8a --- /dev/null +++ b/app/models/datahub/metais/project_change.rb @@ -0,0 +1,5 @@ +class Datahub::Metais::ProjectChange < DatahubRecord + self.table_name = "metais.project_changes" + + belongs_to :project_version, class_name: 'Datahub::Metais::ProjectVersion' +end \ No newline at end of file diff --git a/app/models/datahub/metais/project_document.rb b/app/models/datahub/metais/project_document.rb new file mode 100644 index 0000000..6a6f12d --- /dev/null +++ b/app/models/datahub/metais/project_document.rb @@ -0,0 +1,7 @@ +class Datahub::Metais::ProjectDocument < DatahubRecord + self.table_name = "metais.project_documents" + + belongs_to :project, class_name: 'Datahub::Metais::Project', foreign_key: 'project_id' + has_many :versions, class_name: 'Datahub::Metais::ProjectDocumentVersion' + belongs_to :latest_version, class_name: 'Datahub::Metais::ProjectDocumentVersion', foreign_key: 'latest_version_id', optional: true +end \ No newline at end of file diff --git a/app/models/datahub/metais/project_document_version.rb b/app/models/datahub/metais/project_document_version.rb new file mode 100644 index 0000000..57343ad --- /dev/null +++ b/app/models/datahub/metais/project_document_version.rb @@ -0,0 +1,5 @@ +class Datahub::Metais::ProjectDocumentVersion < DatahubRecord + self.table_name = "metais.project_document_versions" + + belongs_to :document, class_name: 'Datahub::Metais::ProjectDocument', foreign_key: 'document_id' +end \ No newline at end of file diff --git a/app/models/datahub/metais/project_version.rb b/app/models/datahub/metais/project_version.rb new file mode 100644 index 0000000..2593ec2 --- /dev/null +++ b/app/models/datahub/metais/project_version.rb @@ -0,0 +1,5 @@ +class Datahub::Metais::ProjectVersion < DatahubRecord + self.table_name = "metais.project_versions" + + belongs_to :project, class_name: 'Datahub::Metais::Project', foreign_key: 'project_id' +end \ No newline at end of file diff --git a/app/models/datahub_record.rb b/app/models/datahub_record.rb new file mode 100644 index 0000000..07d5642 --- /dev/null +++ b/app/models/datahub_record.rb @@ -0,0 +1,4 @@ +class DatahubRecord < ActiveRecord::Base + self.abstract_class = true + establish_connection :"#{Rails.env}_datahub" +end \ No newline at end of file diff --git a/app/models/metais/origin_type.rb b/app/models/metais/origin_type.rb new file mode 100644 index 0000000..0d8332f --- /dev/null +++ b/app/models/metais/origin_type.rb @@ -0,0 +1,12 @@ +# == Schema Information +# +# Table name: metais.origin_types +# +# id :integer not null, primary key +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null + +class Metais::OriginType < ApplicationRecord + self.table_name = "metais.origin_types" +end diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb new file mode 100644 index 0000000..53d3e8d --- /dev/null +++ b/app/models/metais/project.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: metais.projects +# +# id :integer not null, primary key +# code :string not null +# created_at :datetime not null +# updated_at :datetime not null + +class Metais::Project < ApplicationRecord + self.table_name = "metais.projects" + + has_many :combined_projects, class_name: 'CombinedProject', foreign_key: 'metais_project_id' + has_many :project_origins, :class_name => 'Metais::ProjectOrigin' + + def get_project_origin_info + fields = %w[title status description guarantor project_manager start_date end_date + finance_source investment operation approved_investment approved_operation + supplier targets_text events_text documents_text links_text] + + origins = self.project_origins.sort_by { |origin| -origin.origin_type_id } + + project_info = OpenStruct.new + fields.each do |field| + origin = origins.detect { |origin| !origin.send(field).nil? } + value = origin&.send(field) + if value + project_info.send("#{field}=", Metais::ValueWithOrigin.new(value, origin.origin_type_id)) + end + end + + project_info + end +end diff --git a/app/models/metais/project_document.rb b/app/models/metais/project_document.rb new file mode 100644 index 0000000..2f3c360 --- /dev/null +++ b/app/models/metais/project_document.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: metais.project_documents +# +# id :integer not null, primary key +# name :string not null +# value :string not null +# created_at :datetime not null +# updated_at :datetime not null + +class Metais::ProjectDocument < ApplicationRecord + self.table_name = "metais.project_documents" + + belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' + belongs_to :origin_type, class_name: 'Metais::OriginType' +end diff --git a/app/models/metais/project_event.rb b/app/models/metais/project_event.rb new file mode 100644 index 0000000..acc9f8d --- /dev/null +++ b/app/models/metais/project_event.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: metais.project_events +# +# id :integer not null, primary key +# name :string not null +# value :string not null +# date :datetime not null +# created_at :datetime not null +# updated_at :datetime not null + +class Metais::ProjectEvent < ApplicationRecord + self.table_name = "metais.project_events" + + belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' + belongs_to :origin_type, class_name: 'Metais::OriginType' + belongs_to :event_type, class_name: 'Metais::ProjectEventType' +end diff --git a/app/models/metais/project_event_type.rb b/app/models/metais/project_event_type.rb new file mode 100644 index 0000000..27af73b --- /dev/null +++ b/app/models/metais/project_event_type.rb @@ -0,0 +1,5 @@ +class Metais::ProjectEventType < ApplicationRecord + self.table_name = "metais.project_event_types" + + has_many :project_events, class_name: 'Metais::ProjectEvent' +end diff --git a/app/models/metais/project_link.rb b/app/models/metais/project_link.rb new file mode 100644 index 0000000..abe3ed7 --- /dev/null +++ b/app/models/metais/project_link.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: metais.project_links +# +# id :integer not null, primary key +# name :string not null +# value :string not null +# created_at :datetime not null +# updated_at :datetime not null + +class Metais::ProjectLink < ApplicationRecord + self.table_name = "metais.project_links" + + belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' + belongs_to :origin_type, class_name: 'Metais::OriginType' +end diff --git a/app/models/metais/project_origin.rb b/app/models/metais/project_origin.rb new file mode 100644 index 0000000..82fd51f --- /dev/null +++ b/app/models/metais/project_origin.rb @@ -0,0 +1,51 @@ +# == Schema Information +# +# Table name: metais.project_origins +# +# id :integer not null, primary key +# project_id :integer not null +# origin_type_id :integer not null +# +# title :string not null +# status :string +# description :text +# guarantor :string +# project_manager :string +# start_date :datetime +# end_date :datetime +# +# source :string +# investment :decimal(15,2) +# operation :decimal(15,2) +# +# supplier :string +# +# targets_text :text +# events_text :text +# documents_text :text +# links_text :text +# +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_metais_project_origins_on_project_id (project_id) +# index_metais_project_origins_on_origin_type_id (origin_type_id) +# +# Foreign key constraints +# +# fk_metais_project_origins_project_id (project_id => metais.projects.id) +# fk_metais_project_origins_origin_type_id (origin_type_id => metais.origin_types.id) + +class Metais::ProjectOrigin < ApplicationRecord + self.table_name = "metais.project_origins" + + belongs_to :project, :class_name => 'Metais::Project' + belongs_to :origin_type, :class_name => 'Metais::OriginType' + + has_many :documents, class_name: 'Metais::ProjectDocument', foreign_key: 'project_origin_id' + has_many :suppliers, class_name: 'Metais::ProjectSupplier', foreign_key: 'project_origin_id' + has_many :events, class_name: 'Metais::ProjectEvent', foreign_key: 'project_origin_id' + has_many :links, class_name: 'Metais::ProjectLink', foreign_key: 'project_origin_id' +end diff --git a/app/models/metais/project_supplier.rb b/app/models/metais/project_supplier.rb new file mode 100644 index 0000000..398eab2 --- /dev/null +++ b/app/models/metais/project_supplier.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: metais.project_suppliers +# +# id :integer not null, primary key +# name :string not null +# value :string not null +# created_at :datetime not null +# updated_at :datetime not null + +class Metais::ProjectSupplier < ApplicationRecord + self.table_name = "metais.project_suppliers" + + belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' + belongs_to :origin_type, class_name: 'Metais::OriginType' + belongs_to :supplier_type, class_name: 'Metais::SupplierType' +end diff --git a/app/models/metais/supplier_type.rb b/app/models/metais/supplier_type.rb new file mode 100644 index 0000000..b2e6d62 --- /dev/null +++ b/app/models/metais/supplier_type.rb @@ -0,0 +1,12 @@ +# == Schema Information +# +# Table name: metais.supplier_types +# +# id :integer not null, primary key +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null + +class Metais::SupplierType < ApplicationRecord + self.table_name = "metais.supplier_types" +end diff --git a/app/models/metais/value_with_origin.rb b/app/models/metais/value_with_origin.rb new file mode 100644 index 0000000..3f37339 --- /dev/null +++ b/app/models/metais/value_with_origin.rb @@ -0,0 +1,12 @@ +class Metais::ValueWithOrigin < BasicObject + attr_reader :origin + + def initialize(value, origin) + @value = value + @origin = origin + end + + def method_missing(name, *args) + @value.send(name, *args) + end +end \ No newline at end of file diff --git a/app/models/project.rb b/app/models/project.rb index 241b1c4..f2e58a6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -9,6 +9,7 @@ class Project < ApplicationRecord has_many :phases + has_many :combined_projects, class_name: 'CombinedProject', foreign_key: 'evaluation_id' def has_published_phases? phases.any? { |phase| phase.published_revision.present? } diff --git a/app/views/admin/metais/project_origins/edit.html.erb b/app/views/admin/metais/project_origins/edit.html.erb new file mode 100644 index 0000000..f57f0e1 --- /dev/null +++ b/app/views/admin/metais/project_origins/edit.html.erb @@ -0,0 +1,205 @@ +<%= form_with(model: [@project, @project_origin], url: admin_metais_project_project_origin_path(@project, @project_origin), method: :patch, local: true) do |form| %> +
    + +
    +
    +
    <%= form.label :title, "Názov projektu" %>
    + <%= form.text_field :title, value: @project_info.title, class: "form-control" %> +
    +
    +
    <%= form.label :description, "Popis" %>
    + <%= form.text_area :description, value: @project_info.description, class: "form-control", rows: 12 %> +
    +
    + +
    +
    +
    <%= form.label :guarantor, "Garant" %>
    + <%= form.text_field :guarantor, value: @project_info.guarantor, class: "form-control" %> +
    +
    +
    <%= form.label :project_manager, "Projektový manažér" %>
    + <%= form.text_field :project_manager, value: @project_info.project_manager, class: "form-control" %> +
    + +
    +
    <%= form.label :finance_source, "Zdroj financovania" %>
    + <%= form.text_field :finance_source, value: @project_info.finance_source, class: "form-control" %> +
    +
    +
    <%= form.label :status, "Stav" %>
    + <%= form.select :status, Metais::ProjectOrigin.pluck(:status).uniq.reject(&:blank?), {}, { class: "form-control" } %> +
    <%= form.label :phase, "Fáza" %>
    + <%= form.select :phase, Metais::ProjectOrigin.pluck(:phase).uniq.reject(&:blank?), {}, { class: "form-control" } %> +
    + +
    +
    <%= form.label :start_date, "Dátum začiatku" %>
    + <%= form.date_field :start_date, value: @project_info.start_date.strftime("%F"), class: "form-control datepicker" %> +
    +
    +
    <%= form.label :end_date, "Termín ukončenia " %>
    + <%= form.date_field :end_date, value: @project_info.end_date.strftime("%F"), class: "form-control" %> +
    + +
    +
    Rozpočet projektu
    +
    +
    +
    <%= form.label :investment, "Investícia" %>
    + <%= form.number_field :investment, value: @project_info.investment, step: 10000, class: "form-control" %> +
    <%= form.label :operation, "Ročné náklady" %>
    + <%= form.number_field :operation, value: @project_info.operation, step: 10000, class: "form-control" %> +
    +
    +
    <%= form.label :approved_investment, "Schválená investícia" %>
    + <%= form.number_field :approved_investment, value: @project_info.approved_investment, step: 10000, class: "form-control" %> +
    <%= form.label :approved_operation, "Schválené ročné náklady" %>
    + <%= form.number_field :approved_operation, value: @project_info.approved_operation, step: 10000, class: "form-control" %> +
    +
    +
    +
    + +
    + +
    +
    +
    <%= form.label :targets, "Merateľné ciele a ukazovatele" %>
    + <%= form.text_area :targets, value: @project_info.targets_text, class: "form-control", rows: 5 %> +
    +
    + +
    +
    +
    <%= form.label :documents, "Dokumenty" %>
    + <%= form.text_area :documents, value: @project_info.documents_text, class: "form-control", rows: 2 %> +
    +
    + +
    +
    +
    <%= form.label :links, "Linky" %>
    + <%= form.text_area :links, value: @project_info.links_text, class: "form-control", rows: 2 %> +
    +
    + +
    +
    +
    Dodávateľ projektu
    +
    <%= form.label :supplier, "Dodávateľ" %>
    + <%= form.text_field :supplier, value: @project_info.supplier, class: "form-control" %> +
    +
    +
    + <%= form.submit 'Uložiť', class: 'btn btn-primary' %> +
    +
    +<% end %> + +
    +
    +
    +
    VO, CRZ, NFP
    + + + + + + + + + + <% @all_suppliers.each do |supplier| %> + + + + + + <% end %> + + <%= form_with(url: admin_metais_project_project_origin_add_supplier_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> + + + <%= hidden_field_tag 'supplier[origin_type]', 'Human' %> + + <% end %> + + +
    TypLinkAkcia
    <%= supplier.supplier_type.name if supplier.supplier_type.present? %><%= link_to supplier.name, supplier.value, class: "w-100" %> + <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_supplier_path(project_id: @project.id, project_origin_id: @project_origin.id, supplier_id: supplier.id), method: :delete, data: { confirm: 'Naozaj chcete item odstrániť?' } %> +
    <%= f.select 'supplier[supplier_type]', Metais::SupplierType.pluck(:name, :id), {}, {class: 'form-control'} %><%= f.text_field 'supplier[value]', class: "w-100 form-control" %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    +
    +
    +
    Harmonogram
    +
    Predpokladaný harmonogram
    + + + + + + + + + + + <% @assumption_events.each do |event| %> + + + + + + + <% end %> + + <%= form_with(url: admin_metais_project_project_origin_add_event_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> + + + + <%= hidden_field_tag 'event[origin_type]', 'Human' %> + <%= hidden_field_tag 'event[event_type]', 'Predpoklad' %> + + <% end %> + + +
    NázovPopisDátumAkcia
    <%= event.name %><%= event.value %><%= event.date.in_time_zone('Europe/Bratislava').strftime('%d.%m.%Y') %> + <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_event_path(project_id: @project.id, project_origin_id: @project_origin.id, event_id: event.id), method: :delete, data: { confirm: 'Naozaj chcete aktivitu odstrániť?' } %> +
    <%= f.text_field 'event[name]', class: "w-100 form-control" %><%= f.text_field 'event[value]', class: "w-100 form-control" %><%= f.date_field 'event[date]' %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    +
    +
    +
    Reálný harmonogram
    + + + + + + + + + + + <% @real_events.each do |event| %> + + + + + + + <% end %> + + <%= form_with(url: admin_metais_project_project_origin_add_event_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> + + + + <%= hidden_field_tag 'event[origin_type]', 'Human' %> + <%= hidden_field_tag 'event[event_type]', 'Realita' %> + + <% end %> + + +
    NázovPopisDátumAkcia
    <%= event.name %><%= event.value %><%= event.date.in_time_zone('Europe/Bratislava').strftime('%d.%m.%Y') %> + <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_event_path(project_id: @project.id, project_origin_id: @project_origin.id, event_id: event.id), method: :delete, data: { confirm: 'Naozaj chcete aktivitu odstrániť?' } %> +
    <%= f.text_field 'event[name]', class: "w-100" %><%= f.text_field 'event[value]', class: "w-100" %><%= f.date_field 'event[date]' %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    +
    +
    +
    \ No newline at end of file diff --git a/app/views/admin/metais/projects/index.html.erb b/app/views/admin/metais/projects/index.html.erb new file mode 100644 index 0000000..e9ada78 --- /dev/null +++ b/app/views/admin/metais/projects/index.html.erb @@ -0,0 +1,110 @@ +

    Admin

    + +
    +
    MetaIS projekty
    +
    + + + +
    + <%= form_with url: admin_metais_projects_path, method: :get, class: 'form my-2 my-lg-0', local: true do %> +
    +
    +
    + <%= label_tag :title, 'Názov:' %> + <%= text_field_tag :title, params[:title], class: 'form-control' %> +
    +
    + <%= label_tag :status, 'Status projektu:', class: 'mr-2' %> + <%= select_tag :status, options_for_select(Metais::ProjectOrigin.pluck(:status).reject(&:blank?).uniq, params[:status]), include_blank: true, class: 'form-control mr-2' %> +
    +
    + <%= label_tag :min_price, 'Cena investície od:', class: 'mr-2' %> + <%= number_field_tag :min_price, params[:min_price], in: 0..9999999999, step: 1000, class: 'form-control mr-2' %> +
    +
    + <%= label_tag :change_date, 'Dátum poslednej zmeny:', class: 'mr-2' %> + <%= select_tag :change_date, options_for_select([ + ['Najnovšie', 'latest'], + ['Najstaršie', 'oldest'] + ], params[:change_date]), include_blank: 'Vyberte usporiadanie...', class: 'form-control mr-2' %> +
    +
    + + +
    +
    + <%= label_tag :guarantor, 'Gestor:', class: 'mr-2' %> + <%= select_tag :guarantor, options_for_select(Metais::ProjectOrigin.pluck(:guarantor).reject(&:blank?).uniq, params[:guarantor]), include_blank: true, class: 'form-control' %> +
    +
    + <%= label_tag :code, 'MetaIS Kód:', class: 'mr-2' %> + <%= text_field_tag :code, params[:code], class: 'form-control mr-2' %> +
    +
    + <%= label_tag :max_price, 'Cena investície do:', class: 'mr-2' %> + <%= number_field_tag :max_price, params[:max_price], in: 0..999999999999, step: 1000, class: 'form-control mr-2' %> +
    +
    + <%= label_tag :has_evaluation, 'Redflag hodnotenie?', class: 'mr-2' %> + <%= check_box_tag :has_evaluation, '1', params[:has_evaluation].present?, class: 'mr-2' %> +
    +
    +
    + +
    +
    + <%= submit_tag 'Filter', class: 'btn btn-primary mr-2' %> + <%= link_to 'Reset', admin_metais_projects_path, class: 'btn btn-secondary' %> +
    +
    + <% end %> +
    + + + + + + + + + + + + + + + + <% @projects.each do |project| %> + + + + + + + + + + + <% end %> + +
    IDNázovOriginsDokumentyDodávateliaAktivityLinkyAkcie
    <%= project.id %> + <%= link_to project.project_origins.first.title, admin_metais_project_path(project) %> + + <%= project.project_origins.count %> + + <%= project.project_origins.joins(:documents).count %> + + <%= project.project_origins.joins(:suppliers).count %> + + <%= project.project_origins.joins(:events).count %> + + <%= project.project_origins.joins(:links).count %> + + <%= link_to 'Upraviť', create_human_origin_admin_metais_project_path(project), method: :post, class: 'btn btn-secondary btn-sm' %> +
    +
    + <%= paginate @projects %> +
    \ No newline at end of file diff --git a/app/views/admin/metais/projects/show.html.erb b/app/views/admin/metais/projects/show.html.erb new file mode 100644 index 0000000..6bf2a76 --- /dev/null +++ b/app/views/admin/metais/projects/show.html.erb @@ -0,0 +1,63 @@ +

    Admin

    + +
    +
    +
    Metais Project Origins
    +
    +
    + +

    + <%= @project.get_project_origin_info.title %> +

    + +
    + <% human_origin = @project.project_origins.find_by(origin_type: Metais::OriginType.find_by(name: 'Human')) %> + <% if human_origin %> + <%= link_to 'Edit Human Origin', edit_admin_metais_project_project_origin_path(@project, human_origin), class: 'btn btn-primary' %> + <% else %> + <%= button_to 'Create Human Origin', create_human_origin_admin_metais_project_path(@project), method: :post, class: 'btn btn-primary' %> + <% end %> +
    + + + + + + + + + + + + + + + <% @project.project_origins.each do |project_origin| %> + + + + + + + + <% if project_origin.origin_type.name == 'Human'%> + + <% else %> + + <% end %> + <% end %> + +
    IDTypDokumentyDodávateliaAktivityLinkyAkcie
    <%= project_origin.id %> + <%= link_to project_origin.origin_type.name, metais_project_path(project_origin.project) %> + + <%= project_origin.documents.count %> + + <%= project_origin.suppliers.count %> + + <%= project_origin.events.count %> + + <%= project_origin.links.count %> + + <%= link_to 'Edit', edit_admin_metais_project_project_origin_path(project_origin.project, project_origin) %> + +
    \ No newline at end of file diff --git a/app/views/components/_footer.html.erb b/app/views/components/_footer.html.erb new file mode 100644 index 0000000..5b6daa9 --- /dev/null +++ b/app/views/components/_footer.html.erb @@ -0,0 +1,92 @@ + \ No newline at end of file diff --git a/app/views/components/_navbar.html.erb b/app/views/components/_navbar.html.erb new file mode 100644 index 0000000..85baf1d --- /dev/null +++ b/app/views/components/_navbar.html.erb @@ -0,0 +1,45 @@ + +
    \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index fb4a916..0a8a332 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -46,110 +46,13 @@ - -
    + <% end %> <%= yield %> - + <%= render 'components/footer' %> diff --git a/app/views/metais/projects/index.html.erb b/app/views/metais/projects/index.html.erb new file mode 100644 index 0000000..1a9b072 --- /dev/null +++ b/app/views/metais/projects/index.html.erb @@ -0,0 +1,380 @@ +
    +

    Zoznam štátných IT projektov

    +

    Moderný, prehľadný a jednoduchý prístup k štátným IT projektomnie je budúcnosť... Nemusíte tráviť hodiny čítaním dlhých dokumentácií, ktoré nedávajú zmysel. Naše riešenie Vám prináša všetky potrebné informácie.

    + +
    + <%= form_with url: metais_projects_path, method: :get, class: 'form my-2 my-lg-0', local: true, id: "form" do |form| %> + <%= hidden_field_tag :sort, params[:sort] %> + <%= hidden_field_tag :sort_direction, params[:sort_direction] %> + <%= hidden_field_tag :status, params[:status] %> + <%= hidden_field_tag :guarantor, params[:guarantor] %> + <%= hidden_field_tag :has_evaluation, params[:has_evaluation] %> + + +
    +
    +
    + <%= text_field_tag :title, params[:title], class: 'form-control mr-1', placeholder: "Meno projektu" %> + +
    +
    +
    +
    + +
    +
    + <% sort_options = { + '' => 'Odporúčané', + 'alpha' => 'Abecedne', + 'date' => 'Podľa dátumu', + 'price' => 'Podľa ceny'} %> + + <% current_sort = params[:sort] || '' %> + <% current_sort_label = sort_options[current_sort] %> + + + +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + <% end %> +
    + + + + + + + + + + <% @projects.each do |project| %> + + + + + <% end %> + +
    ProjektCena
    +
    + <%= link_to project.project_origins.first.title, metais_project_path(project), class: 'table-title-item'%> +
    +

    Dátum poslednej zmeny:

    + <%= + content_tag(:p) do + if project.updated_at.present? + content_tag(:small, l(project.updated_at.in_time_zone('Europe/Bratislava'), format: "%d.%m.%Y"), class: "font-italic") + else + content_tag(:small, "Neuvedená", style: "color: grey;", class: "font-italic tag-tooltip", title: "Dátum poslednej zmeny projektu nie je známy") + end + end + %> +
    +
    +
    +

    Gestor:

    + <%= + if project.get_project_origin_info.guarantor.present? + project.get_project_origin_info.guarantor + else + content_tag(:span, "Neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Gestor projektu nie je známy") + end + %> +
    + +
    +

    Stav:

    + <% + status = project.get_project_origin_info.status&.strip + status_class = case status + when 'Rozpracovaný' + 'status-1' + when 'Ohlásený' + 'status-2' + when 'Plánovaný' + 'status-3' + when 'Hodnotený' + 'status-4' + when 'Vrátený na dopracovanie' + 'status-5' + when 'Schválený' + 'status-6' + when 'Zrušený' + 'status-7' + when 'Realizovaný' + 'status-8' + when 'Ukončený' + 'status-9' + when 'Nasadený' + 'status-10' + when 'Znovu hodnotený' + 'status-11' + when 'Neschválený' + 'status-12' + when 'Mimoriadne ukončený' + 'status-13' + else + '' + end + %> + <%= + if status.present? + content_tag(:span, status, class: "state-badge #{status_class}") + else + content_tag(:span, "Neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Stav a fáza projektu nie sú známe") + end + %> +
    +
    +

    Cena:

    + <%= + if project.get_project_origin_info.approved_investment.present? && project.get_project_origin_info.approved_investment > 0 + number_to_currency(project.get_project_origin_info.approved_investment, unit: "€", separator: ",", delimiter: " ", format: "%n %u") + elsif project.get_project_origin_info.investment.present? && project.get_project_origin_info.investment > 0 + number_to_currency(project.get_project_origin_info.investment, unit: "€", separator: ",", delimiter: " ", format: "%n %u") + else + content_tag(:span, "Neuvedená", style: "color: grey;", class: "font-italic tag-tooltip", title: "Investičné náklady projektu nie sú známe") + end + %> +
    +
    +
    +
    + <%= + if project.get_project_origin_info.approved_investment.present? && project.get_project_origin_info.approved_investment > 0 + number_to_currency(project.get_project_origin_info.approved_investment, unit: "€", separator: ",", delimiter: " ", format: "%n %u") + elsif project.get_project_origin_info.investment.present? && project.get_project_origin_info.investment > 0 + number_to_currency(project.get_project_origin_info.investment, unit: "€", separator: ",", delimiter: " ", format: "%n %u") + else + content_tag(:span, "Neuvedená", style: "color: grey;", class: "font-italic tag-tooltip", title: "Investičné náklady projektu nie sú známe") + end + %> +
    + +
    + + diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb new file mode 100644 index 0000000..c626979 --- /dev/null +++ b/app/views/metais/projects/show.html.erb @@ -0,0 +1,244 @@ +
    + +
    +
    +

    <%= @project_info.title %>

    +

    <%= @project_info.description %>

    +
    +
    + +
    +
    +
    +
    +
    Garant <%= help_icon_if_blank(@project_info.guarantor) %>
    +

    <%= @project_info.guarantor %>

    +
    +
    +
    Projektový manažér + <% if @project_info.project_manager.present? %> + <%= image_tag origin_type_logo(@project_info.project_manager.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% end %> + <%= help_icon_if_blank(@project_info.project_manager) %> +
    +

    <%= @project_info.project_manager %>

    +
    +
    + +
    +
    +
    Zdroj financovania <%= help_icon_if_blank(@project_info.finance_source) %>
    +

    <%= @project_info.finance_source %>

    +
    +
    +
    Aktuálny stav <%= help_icon_if_blank(@project_info.status || @project_info.phase) %>
    +

    Stav: <%= @project_info.status %>

    +

    Fáza: <%= @project_info.phase %>

    +
    +
    + +
    +
    +
    Dátum začiatku <%= help_icon_if_blank(@project_info.start_date) %>
    +

    <%= @project_info.start_date&.strftime('%d.%m.%Y') %>

    +
    +
    +
    Termín ukončenia <%= help_icon_if_blank(@project_info.end_date) %>
    +

    <%= @project_info.end_date&.strftime('%d.%m.%Y') %>

    +
    +
    + +
    +
    +
    MetaIS <%= help_icon_if_blank(@project.code) %>
    + <%= link_to @project.code, "https://metais.vicepremier.gov.sk/detail/Projekt/#{@project.uuid}/cimaster?tab=basicForm", target: "_blank" %> +
    +
    +
    Posledná zmena <%= help_icon_if_blank(@project_info.end_date) %>
    +

    <%= @project.updated_at&.strftime('%d.%m.%Y') %>

    +
    +
    +
    + +
    +
    Rozpočet projektu
    +
    +

    Investícia: <%= number_to_currency(@project_info.investment, :unit => "€", :separator => ",", :delimiter => " ", :format => "%n %u")%>

    +
    +
    +

    Schválená investícia: <%= number_to_currency(@project_info.approved_investment, :unit => "€", :separator => ",", :delimiter => " ", :format => "%n %u")%>

    +
    + +
    + +
    +

    Ročné náklady: <%= number_to_currency(@project_info.operation, :unit => "€", :separator => ",", :delimiter => " ", :format => "%n %u")%>

    +
    +
    +

    Schválené ročné náklady: <%= number_to_currency(@project_info.approved_operation, :unit => "€", :separator => ",", :delimiter => " ", :format => "%n %u")%>

    +
    +
    +
    + + <% if @combined_project&.evaluation&.phases.present? %> + <% @combined_project.evaluation.phases.each do |phase| %> + <% if phase.published_revision.present? %> +
    + <% if phase.phase_type.name == 'Prípravná fáza' %> +
    <%= link_to 'Hodnotenie prípravnej fázy', project_show_revision_type_path(phase.project.id, 'hodnotenie-pripravy')%>
    + <% else %> +
    <%= link_to 'Hodnotenie fázy produkt', project_show_revision_type_path(phase.project.id, 'hodnotenie-produkt')%>
    + <% end %> +
    +
    + <% if phase.published_revision.summary.present? %> +
    +
    +
    Zhrnutie hodnotenia Red Flags
    +

    <%= phase.published_revision.summary %>

    +
    +
    + <% end %> + + <% if phase.published_revision.recommendation.present? %> +
    +
    +
    Stanovisko Slovensko.Digital
    +

    <%= phase.published_revision.recommendation %>

    +
    +
    + <% end %> +
    +
    + <% end %> + <% end %> + <% end %> + +
    + +
    +
    +
    Merateľné ciele a ukazovatele + <% if @project_info.targets_text.present? %> + <%= image_tag origin_type_logo(@project_info.targets_text.origin), style: "width: 18px; height: 18px; margin-left: 6px;" %> + <% end %> + <%= help_icon_if_blank(@project_info.targets_text) %> +
    + <% if @project_info.targets_text.present? %> + <%= convert_to_list(@project_info.targets_text) %> + <% else %> + Žiadne ciele neboli nájdené. + <% end %> +
    +
    + +
    +
    +
    Harmonogram
    +
    +
    +
    +

    Predpokladaný harmonogram projektu

    +
    +
    + <% @assumption_events.each do |event| %> +
    +
    + + + +
    +

    + <%= event.name %> <%= image_tag origin_type_logo(event.origin_type), style: "width: 12px; height: 12px;", class: 'mr-2' if event.origin_type.present? %> +

    +

    <%= event.value %>

    + <%= event.date.in_time_zone('Europe/Bratislava').strftime('%m/%Y') %> +
    +
    +
    + <% end %> +
    +
    +
    +
    +
    +
    +

    Realný harmonogram projektu

    +
    +
    + <% @real_events.each do |event| %> +
    +
    + + + +
    +

    + <%= event.name %> <%= image_tag origin_type_logo(event.origin_type), style: "width: 12px; height: 12px;", class: 'mr-2' if event.origin_type.present? %> +

    +

    <%= event.value %>

    + <%= event.date.in_time_zone('Europe/Bratislava').strftime('%m/%Y') %> +
    +
    +
    + <% end %> +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    Dodávateľ projektu
    + <% if @project_info.supplier.present? %> +

    Dodávateľ: <%= @project_info.supplier %> <%= image_tag origin_type_logo(@project_info.supplier.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %>

    + <% end %> +
      + <% @all_suppliers.each do |supplier| %> + <%= supplier.supplier_type.name if supplier.supplier_type.present? %> +
    • + <%= image_tag origin_type_logo(supplier.origin_type), style: "width: 12px; height: 12px;" if supplier.origin_type.present? %> + <%= link_to supplier.name, supplier.value %> +
    • + <% end %> +
    +
    +
    + +
    +
    +
    Dokumenty
    + <% if @project_info.documents_text.present? %> +

    <%= @project_info.documents_text %>

    + <% end %> +
      + <% @project_origin.documents.each do |doc| %> +
    • + <%= image_tag origin_type_logo(doc.origin_type), style: "width: 12px; height: 12px;" if doc.origin_type.present? %> + <%= link_to doc.name, doc.value %> +
    • + <% end %> +
    +
    +
    + +
    +
    +
    Linky
    + <% if @project_info.links_text.present? %> +

    <%= @project_info.links_text %>

    + <% end %> +
      + <% @project_origin.links.each do |link| %> +
    • + <%= image_tag origin_type_logo(link.origin_type), style: "width: 12px; height: 12px;" if link.origin_type.present? %> + <%= link_to link.name, link.value %> +
    • + <% end %> +
    +
    +
    +
    diff --git a/app/views/phase_revision/show.html.erb b/app/views/phase_revision/show.html.erb index 74af6cf..1ad6ccf 100644 --- a/app/views/phase_revision/show.html.erb +++ b/app/views/phase_revision/show.html.erb @@ -74,6 +74,17 @@ + <% if @combined_project.present? %> +
    +
    +
    Viac o projekte
    + <% if @combined_project&.metais_project.present? %> + <%= link_to @combined_project.metais_project.get_project_origin_info.title, metais_project_path(@combined_project.metais_project) %> + <% end %> +
    +
    + <% end %> +
    diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 5272297..269dc61 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -1,55 +1,131 @@ -
    +

    Zoznam hodnotených projektov

    Chýba Vám tu nejaký projekt? Nezdá sa Vám hodnotenie? Toto hodnotenie je možné <%= link_to 'dopĺňať a upravovať', contribute_path %>.

    -
    - <%= form_with url: projects_path, method: :get, local: true, id: "sort-form" do |form| %> - <% form.label :sort %> - <%= form.select :sort, options_for_select([ - ['Usporiadať', '', {disabled: true, selected: true}], - ['Abecedne (A-Z)', 'alpha'], - ['Abecedne (Z-A)', 'alpha_reverse'], - ['Podľa dátumu (od najnovších)', 'newest'], - ['Podľa dátumu (od najstarších)', 'oldest'], - ['Hodnotenie prípravy (vzostupne)', 'preparation_lowest'], - ['Hodnotenie prípravy (zostupne)', 'preparation_highest'], - ['Hodnotenie produktu (vzostupne)', 'product_lowest'], - ['Hodnotenie produktu (zostupne)', 'product_highest'], - ], params[:sort]), {}, { class: 'bg-primary pl-2 py-2 rounded border-0 text-white', onchange: "this.form.submit();" } %> +
    + <%= form_with url: projects_path, method: :get, class: 'form my-2 my-lg-0', local: true do |form| %> +
    +
    +
    + <%= text_field_tag :title, params[:title], class: 'form-control' %> +
    +
    +
    +
    + <% sort_options = { + '' => 'Odporúčané', + 'alpha' => 'Abecedne', + 'date' => 'Podľa dátumu', + 'price' => 'Podľa ceny'} %> + + <% current_sort = params[:sort] || '' %> + <% current_sort_label = sort_options[current_sort] %> + + + +
    +
    +
    <% end %> - <%= link_to "Reset", projects_path, class: 'btn btn-link' %>
    - +
    - - + + - - - + <% @projects.each do |project| %> - <% prep_revision = project.phases.select { |phase| phase.phase_type.name == 'Prípravná fáza' && phase.published_revision.present? }.first %> <% prep_revision = prep_revision&.published_revision %> <% prep_page = prep_revision&.revision&.page %> - - - - - - + + - + <%= hidden_field_tag 'event[origin_type]', 'Human' %> <%= hidden_field_tag 'event[event_type]', 'Predpoklad' %> @@ -190,9 +161,9 @@ <% end %> <%= form_with(url: admin_metais_project_project_origin_add_event_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> - - - + + + <%= hidden_field_tag 'event[origin_type]', 'Human' %> <%= hidden_field_tag 'event[event_type]', 'Realita' %> @@ -202,4 +173,89 @@
    Projekt
    Projekt Hodnotenie prípravy Hodnotenie produktuPosledná aktualizáciaDetail
    - <% if project.has_published_phases? %> - <% latest_published_revision = project.phases.select { |phase| phase.published_revision.present? }&.max_by { |phase| phase.published_revision.published_at }&.published_revision %> - <%= latest_published_revision&.title %> - <% latest_stage = latest_published_revision&.stage %> - <% if latest_stage %> -
    - <%= latest_stage %> +
    +
    + <% if project.has_published_phases? %> + <% latest_published_revision = project.phases.select { |phase| phase.published_revision.present? }&.max_by { |phase| phase.published_revision.published_at }&.published_revision %> + <% if project.combined_projects.present? %> + <%= link_to latest_published_revision&.title, metais_project_path(id: project.combined_projects.first.metais_project.id), class: 'table-title-item' %> + <% else %> + <%= content_tag(:p, latest_published_revision&.title, class: "table-price") %> + <% end %> +
    +

    Dátum poslednej aktualizácie:

    + <%= + content_tag(:p) do + latest_revision_date = project.phases.map { |phase| phase.published_revision&.published_at }.compact.max + if latest_revision_date.present? + content_tag(:small, l(latest_revision_date.in_time_zone('Europe/Bratislava'), format: "%d.%m.%Y"), class: "font-italic") + else + content_tag(:small, "Neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Dátum poslednej aktualizácie projektu nie je známy") + end + end + %> +
    +
    +
    +

    Stav:

    + <% + latest_stage = latest_published_revision&.stage + status_class = case latest_stage&.name + when 'Príprava projektu' + 'status-1' + when 'Obstarávanie' + 'status-2' + when 'Zrušený projekt' + 'status-3' + when 'Zastavený projekt' + 'status-4' + when 'Vrátený na dopracovanie' + 'status-5' + when 'Schválený' + 'status-6' + when 'Zrušený' + 'status-7' + else + '' + end + %> + <%= + if latest_stage.present? + content_tag(:span, latest_stage, class: "state-badge #{status_class}") + else + content_tag(:span, "Neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Stav a fáza projektu nie sú známe") + end + %> +
    + +
    +

    Detailné zobrazenie:

    + +
    <% end %> - <% end %> +
    + <% if prep_page.nil? %> - <% elsif prep_revision.redflags_count > 0 %> @@ -76,7 +152,7 @@ <% prod_revision = project.phases.select { |phase| phase.phase_type.name == 'Fáza produkt' && phase.published_revision.present? }.first %> <% prod_revision = prod_revision&.published_revision %> <% prod_page = prod_revision&.revision&.page%> - + <% if prod_page.nil? %> - <% elsif prod_revision&.redflags_count > 0 %> @@ -99,18 +175,9 @@ <% end %> - <% latest_revision_date = project.phases.map { |phase| phase.published_revision&.published_at }.compact.max %> - <%= latest_revision_date.strftime('%d.%m.%Y') if latest_revision_date.present? %> - - -
    <%= f.text_field 'event[name]', class: "w-100" %><%= f.text_field 'event[value]', class: "w-100" %><%= f.date_field 'event[date]' %><%= f.text_field 'event[name]', class: "w-100 form-control" %><%= f.text_field 'event[value]', class: "w-100 form-control" %><%= f.date_field 'event[date]', class: "form-control" %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    +
    +
    VO, CRZ, NFP
    + + + + + + + + + + <% @all_suppliers.each do |supplier| %> + + + + + + <% end %> + + <%= form_with(url: admin_metais_project_project_origin_add_supplier_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> + + + <%= hidden_field_tag 'supplier[origin_type]', 'Human' %> + + <% end %> + + +
    TypLinkAkcia
    <%= supplier.supplier_type.name if supplier.supplier_type.present? %><%= link_to supplier.name, supplier.value, class: "w-100" %> + <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_supplier_path(project_id: @project.id, project_origin_id: @project_origin.id, supplier_id: supplier.id), method: :delete, data: { confirm: 'Naozaj chcete item odstrániť?' } %> +
    <%= f.select 'supplier[supplier_type]', Metais::SupplierType.pluck(:name, :id), {}, {class: 'form-control'} %><%= f.text_field 'supplier[value]', class: "w-100 form-control" %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    +
    +
    +
    Dokumenty
    + + + + + + + + + + <% @all_documents.each do |document| %> + + + + + + <% end %> + +
    NázovLinkAkcia
    <%= document.name %><%= link_to document.value, document.value %> + <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_document_path(project_id: @project.id, project_origin_id: @project_origin.id, document_id: document.id), method: :delete, data: { confirm: 'Naozaj chcete item odstrániť?' } %> +
    +
    +
    +
    Linky
    + + + + + + + + + + <% @all_links.each do |link| %> + + + + + + <% end %> + + <%= form_with(url: admin_metais_project_project_origin_add_link_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> + + + <%= hidden_field_tag 'link[origin_type]', 'Human' %> + + <% end %> + + +
    NázovLinkAkcia
    <%= link.name %><%= link_to link.value, link.value %> + <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_link_path(project_id: @project.id, project_origin_id: @project_origin.id, link_id: link.id), method: :delete, data: { confirm: 'Naozaj chcete item odstrániť?' } %> +
    <%= f.text_field 'link[name]', class: "w-100 form-control" %><%= f.text_field 'link[value]', class: "w-100 form-control" %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    +
    \ No newline at end of file diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb index c626979..aa678c3 100644 --- a/app/views/metais/projects/show.html.erb +++ b/app/views/metais/projects/show.html.erb @@ -194,7 +194,7 @@
    Dodávateľ projektu
    <% if @project_info.supplier.present? %> -

    Dodávateľ: <%= @project_info.supplier %> <%= image_tag origin_type_logo(@project_info.supplier.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %>

    +

    Dodávateľ: <%= @project_info.supplier %> <%= image_tag origin_type_logo(@project_info.supplier.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %>

    <% end %>
      <% @all_suppliers.each do |supplier| %> diff --git a/config/clock.rb b/config/clock.rb index 9a701ad..c050ec5 100644 --- a/config/clock.rb +++ b/config/clock.rb @@ -11,5 +11,9 @@ module Clockwork Rake::Task[job].reenable Rake::Task[job].invoke end + every(1.day, 'redflags:sync', at: '9:00') + + every(1.day, 'metais:daily_sync', at: '10:00') + every(1.day, 'metais:daily_sync_evaluations', at: '11:00') end diff --git a/config/routes.rb b/config/routes.rb index 0aa6f32..e050594 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,9 +32,12 @@ resources :project_origins, only: [:edit, :update, :create] do delete 'remove_event/:event_id', to: 'project_origins#remove_event', as: 'remove_event' delete 'remove_supplier/:supplier_id', to: 'project_origins#remove_supplier', as: 'remove_supplier' + delete 'remove_link/:link_id', to: 'project_origins#remove_link', as: 'remove_link' + delete 'remove_document/:document_id', to: 'project_origins#remove_document', as: 'remove_document' post 'add_event', to: 'project_origins#add_event', as: 'add_event' post 'add_supplier', to: 'project_origins#add_supplier', as: 'add_supplier' + post 'add_link', to: 'project_origins#add_link', as: 'add_link' end end end diff --git a/db/migrate/20240821210736_add_state_change_date_to_metais_project_origin.rb b/db/migrate/20240821210736_add_state_change_date_to_metais_project_origin.rb new file mode 100644 index 0000000..a8f4950 --- /dev/null +++ b/db/migrate/20240821210736_add_state_change_date_to_metais_project_origin.rb @@ -0,0 +1,5 @@ +class AddStateChangeDateToMetaisProjectOrigin < ActiveRecord::Migration[5.1] + def change + add_column :'metais.project_origins', :status_change_date, :datetime + end +end diff --git a/db/migrate/20240821212620_add_supplier_cin_to_metais_project_origins.rb b/db/migrate/20240821212620_add_supplier_cin_to_metais_project_origins.rb new file mode 100644 index 0000000..9103b2f --- /dev/null +++ b/db/migrate/20240821212620_add_supplier_cin_to_metais_project_origins.rb @@ -0,0 +1,5 @@ +class AddSupplierCinToMetaisProjectOrigins < ActiveRecord::Migration[5.1] + def change + add_column :'metais.project_origins', :supplier_cin, :bigint + end +end diff --git a/db/structure.sql b/db/structure.sql index b9ebe50..8e41a20 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -216,7 +216,9 @@ CREATE TABLE metais.project_origins ( phase character varying, approved_investment numeric(15,2), approved_operation numeric(15,2), - metais_created_at timestamp without time zone + metais_created_at timestamp without time zone, + status_change_date timestamp without time zone, + supplier_cin bigint ); @@ -603,6 +605,38 @@ CREATE TABLE public.projects ( ); +-- +-- Name: projects2; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projects2 ( + id bigint NOT NULL, + metais_project_id bigint NOT NULL, + evaluation_id bigint, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: projects2_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.projects2_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: projects2_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.projects2_id_seq OWNED BY public.projects2.id; + + -- -- Name: projects_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -628,7 +662,7 @@ ALTER SEQUENCE public.projects_id_seq OWNED BY public.projects.id; CREATE TABLE public.que_jobs ( priority smallint DEFAULT 100 NOT NULL, - run_at timestamp without time zone DEFAULT now() NOT NULL, + run_at timestamp with time zone DEFAULT now() NOT NULL, job_id bigint NOT NULL, job_class text NOT NULL, args json DEFAULT '[]'::json NOT NULL, @@ -858,6 +892,13 @@ ALTER TABLE ONLY public.project_stages ALTER COLUMN id SET DEFAULT nextval('publ ALTER TABLE ONLY public.projects ALTER COLUMN id SET DEFAULT nextval('public.projects_id_seq'::regclass); +-- +-- Name: projects2 id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects2 ALTER COLUMN id SET DEFAULT nextval('public.projects2_id_seq'::regclass); + + -- -- Name: que_jobs job_id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1015,6 +1056,14 @@ ALTER TABLE ONLY public.project_stages ADD CONSTRAINT project_stages_pkey PRIMARY KEY (id); +-- +-- Name: projects2 projects2_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects2 + ADD CONSTRAINT projects2_pkey PRIMARY KEY (id); + + -- -- Name: projects projects_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1209,6 +1258,20 @@ CREATE INDEX index_phases_on_phase_type_id ON public.phases USING btree (phase_t CREATE INDEX index_phases_on_project_id ON public.phases USING btree (project_id); +-- +-- Name: index_projects2_on_evaluation_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_projects2_on_evaluation_id ON public.projects2 USING btree (evaluation_id); + + +-- +-- Name: index_projects2_on_metais_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_projects2_on_metais_project_id ON public.projects2 USING btree (metais_project_id); + + -- -- Name: index_revisions_on_page_id; Type: INDEX; Schema: public; Owner: - -- @@ -1335,19 +1398,19 @@ ALTER TABLE ONLY public.combined_projects -- --- Name: combined_projects fk_rails_18e52e7275; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: projects2 fk_rails_167b3161dd; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.combined_projects - ADD CONSTRAINT fk_rails_18e52e7275 FOREIGN KEY (metais_project_id) REFERENCES metais.projects(id); +ALTER TABLE ONLY public.projects2 + ADD CONSTRAINT fk_rails_167b3161dd FOREIGN KEY (evaluation_id) REFERENCES public.projects(id); -- --- Name: phase_revisions fk_rails_290d0a047c; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: combined_projects fk_rails_18e52e7275; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.phase_revisions - ADD CONSTRAINT fk_rails_290d0a047c FOREIGN KEY (revision_id) REFERENCES public.revisions(id); +ALTER TABLE ONLY public.combined_projects + ADD CONSTRAINT fk_rails_18e52e7275 FOREIGN KEY (metais_project_id) REFERENCES metais.projects(id); -- @@ -1358,6 +1421,22 @@ ALTER TABLE ONLY public.phases ADD CONSTRAINT fk_rails_7768cfc98c FOREIGN KEY (phase_type_id) REFERENCES public.phase_types(id); +-- +-- Name: phase_revision_ratings fk_rails_89f94d4743; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.phase_revision_ratings + ADD CONSTRAINT fk_rails_89f94d4743 FOREIGN KEY (phase_revision_id) REFERENCES public.phase_revisions(id); + + +-- +-- Name: phase_revision_ratings fk_rails_8f89f5f94e; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.phase_revision_ratings + ADD CONSTRAINT fk_rails_8f89f5f94e FOREIGN KEY (rating_type_id) REFERENCES public.rating_types(id); + + -- -- Name: pages fk_rails_9214ad0f21; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1367,11 +1446,19 @@ ALTER TABLE ONLY public.pages -- --- Name: phase_revision_ratings fk_rails_9a5c0eae42; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: phase_revisions fk_rails_9b7644f642; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.phase_revision_ratings - ADD CONSTRAINT fk_rails_9a5c0eae42 FOREIGN KEY (phase_revision_id) REFERENCES public.phase_revisions(id); +ALTER TABLE ONLY public.phase_revisions + ADD CONSTRAINT fk_rails_9b7644f642 FOREIGN KEY (stage_id) REFERENCES public.project_stages(id); + + +-- +-- Name: phase_revisions fk_rails_9bbd5a8be5; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.phase_revisions + ADD CONSTRAINT fk_rails_9bbd5a8be5 FOREIGN KEY (revision_id) REFERENCES public.revisions(id); -- @@ -1398,14 +1485,6 @@ ALTER TABLE ONLY public.revisions ADD CONSTRAINT fk_rails_d1037952e2 FOREIGN KEY (page_id) REFERENCES public.pages(id); --- --- Name: phase_revision_ratings fk_rails_df4b31c2b5; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.phase_revision_ratings - ADD CONSTRAINT fk_rails_df4b31c2b5 FOREIGN KEY (rating_type_id) REFERENCES public.rating_types(id); - - -- -- Name: pages fk_rails_ee4b1c338f; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1414,14 +1493,6 @@ ALTER TABLE ONLY public.pages ADD CONSTRAINT fk_rails_ee4b1c338f FOREIGN KEY (latest_revision_id) REFERENCES public.revisions(id); --- --- Name: phase_revisions fk_rails_fb290ef702; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.phase_revisions - ADD CONSTRAINT fk_rails_fb290ef702 FOREIGN KEY (stage_id) REFERENCES public.project_stages(id); - - -- -- Name: pages fk_rails_ffffd09d52; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1481,8 +1552,11 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240724072736'), ('20240730180654'), ('20240731124741'), +('20240801081124'), ('20240820080110'), ('20240820080303'), -('20240820193320'); +('20240820193320'), +('20240821210736'), +('20240821212620'); diff --git a/lib/tasks/redflags.rake b/lib/tasks/redflags.rake index a70a345..26198bc 100644 --- a/lib/tasks/redflags.rake +++ b/lib/tasks/redflags.rake @@ -3,3 +3,13 @@ namespace :redflags do SyncCategoryTopicsJob.perform_later(ENV.fetch('REDFLAGS_CATEGORY_SLUG')) end end + +namespace :metais do + task daily_sync: :environment do + Metais::DailySyncProjectsJob.perform_later + end + + task daily_sync_evaluations: :environment do + LinkMetaisProjectsAndEvaluationsJob.perform_later + end +end From 8290907f4b7f6f96e852fac4a4f4c52db5817558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Thu, 22 Aug 2024 10:05:17 +0200 Subject: [PATCH 04/39] try to use db scturcture in tests --- .github/workflows/slovensko_digital_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slovensko_digital_ci.yml b/.github/workflows/slovensko_digital_ci.yml index 45444a7..db9dc72 100644 --- a/.github/workflows/slovensko_digital_ci.yml +++ b/.github/workflows/slovensko_digital_ci.yml @@ -28,7 +28,7 @@ jobs: with: bundler-cache: true - - run: bundle exec rails db:create db:schema:load --trace + - run: bundle exec rails db:create db:structure:load --trace - run: bundle exec rspec gitlab-push: From 0556bd34f3f507a987bb2bb05f578df446543234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Thu, 22 Aug 2024 17:04:31 +0200 Subject: [PATCH 05/39] Added jobs for data extraction from python model --- .env | 2 + .../metais/project_data_extraction_job.rb | 24 ++++ .../project_data_extraction_result_job.rb | 120 ++++++++++++++++++ .../project_data_extraction_status_job.rb | 17 +++ app/jobs/metais/sync_project_documents_job.rb | 2 +- app/jobs/metais/sync_project_events_job.rb | 8 +- app/jobs/metais/sync_project_job.rb | 1 + app/jobs/sync_all_topics_job.rb | 7 +- ..._add_benefits_to_metais_project_origins.rb | 5 + ...description_to_metais_project_documents.rb | 5 + db/structure.sql | 10 +- 11 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 app/jobs/metais/project_data_extraction_job.rb create mode 100644 app/jobs/metais/project_data_extraction_result_job.rb create mode 100644 app/jobs/metais/project_data_extraction_status_job.rb create mode 100644 db/migrate/20240822112026_add_benefits_to_metais_project_origins.rb create mode 100644 db/migrate/20240822143116_add_description_to_metais_project_documents.rb diff --git a/.env b/.env index b24b3fc..602b71f 100644 --- a/.env +++ b/.env @@ -17,3 +17,5 @@ GOOGLE_APPLICATION_CREDENTIALS= GOOGLE_SHEET_ID= GOOGLE_SHEET_EXPORT_ID= GOOGLE_SHEET_SCRIPT_URL= + +API_URL= \ No newline at end of file diff --git a/app/jobs/metais/project_data_extraction_job.rb b/app/jobs/metais/project_data_extraction_job.rb new file mode 100644 index 0000000..227c3c9 --- /dev/null +++ b/app/jobs/metais/project_data_extraction_job.rb @@ -0,0 +1,24 @@ +require 'net/http' +require 'uri' + +class Metais::ProjectDataExtractionJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid) + url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}" + response = Net::HTTP.post(URI(url), '') + + unless response.is_a?(Net::HTTPAccepted) + error_message = "Response status is #{response.code}. Message: #{response.body}" + raise RuntimeError, error_message + end + + location = response['Location'] + if location.nil? + error_message = "Expected 'Location' header not found in the response." + raise RuntimeError, error_message + end + + Metais::ProjectDataExtractionStatusJob.perform_later(project_uuid, location) + end +end diff --git a/app/jobs/metais/project_data_extraction_result_job.rb b/app/jobs/metais/project_data_extraction_result_job.rb new file mode 100644 index 0000000..8f53ef8 --- /dev/null +++ b/app/jobs/metais/project_data_extraction_result_job.rb @@ -0,0 +1,120 @@ +require 'net/http' +require 'uri' +require 'json' + +class Metais::ProjectDataExtractionResultJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid, location_header) + url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}" + response = Net::HTTP.get_response(URI(url)) + + handle_response_errors(response) + + body = parse_json(response.body) + return unless body['status'] == 'Done' + + result = body['result'] + metais_project = find_metais_project(project_uuid) + project_origin = find_or_initialize_project_origin(metais_project) + + update_project_origin(project_origin, result) + process_harmonogram(result['harmonogram'], project_origin) + + send_delete_request(URI(url)) + end + + private + + def handle_response_errors(response) + case response.code.to_i + when 200, 202 + else + error_message = "Unexpected response status: #{response.code}, body: #{response.body}" + raise RuntimeError, error_message + end + end + + def parse_json(body) + JSON.parse(body) + rescue JSON::ParserError => e + error_message = "Failed to parse JSON response: #{e.message}" + raise RuntimeError, error_message + end + + def find_metais_project(project_uuid) + Metais::Project.find_by(uuid: project_uuid).tap do |project| + unless project + error_message = "Couldn't find MetaIS project with 'uuid'=#{project_uuid}" + raise RuntimeError, error_message + end + end + end + + def find_or_initialize_project_origin(metais_project) + metais_project.project_origins.find_or_initialize_by(title: metais_project.project_origins.first.title, + project: metais_project, + origin_type: Metais::OriginType.find_by(name: 'AI')) + end + + def update_project_origin(project_origin, result) + project_origin.project_manager = "#{result['responsible']['first_name']} #{result['responsible']['surname']}" + project_origin.approved_investment = result['capex'] unless result['capex'].zero? + project_origin.approved_operation = result['opex'] unless result['opex'].zero? + project_origin.benefits = result['declared'] unless result['declared'].zero? + project_origin.targets_text = format_targets_text(result) unless format_targets_text(result).empty? + project_origin.save! + end + + def format_targets_text(result) + kpis = result['kpis'].join("\n") unless result['kpis'].empty? + goals = result['goals'].join("\n") unless result['goals'].empty? + [kpis, goals].compact.join("\n") + end + + def process_harmonogram(harmonogram, project_origin) + origin_type = Metais::OriginType.find_by(name: 'AI') + event_type = Metais::ProjectEventType.find_by(name: 'Predpoklad') + + harmonogram.each do |event_data| + event_start_date = event_data['start_date'] || 'N/A' + event_end_date = event_data['end_date'] || 'N/A' + event_date = parse_event_date(event_data['start_date']) + event_name = event_data['item_name'] unless event_data['item_name'].empty? + event_value = "Projektu bude v stave #{event_data['item_name'].downcase} od #{event_start_date} do #{event_end_date}" + + project_event = Metais::ProjectEvent.find_or_initialize_by( + project_origin: project_origin, + origin_type: origin_type, + event_type: event_type, + name: event_name, + value: event_value, + date: event_date + ) + + unless project_event.save + error_message = "Error encountered while creating an event: #{project_event.errors.full_messages.to_sentence}" + raise RuntimeError, error_message + end + end + end + + def parse_event_date(start_date) + return if start_date.blank? + Date.strptime(start_date, "%m/%Y") + rescue ArgumentError => e + raise RuntimeError, e + end + + def send_delete_request(uri) + req = Net::HTTP::Delete.new(uri) + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| + http.request(req) + end + + unless res.is_a?(Net::HTTPSuccess) + error_message = "Failed to delete project: #{res.code}, body: #{res.body}" + raise RuntimeError, error_message + end + end +end diff --git a/app/jobs/metais/project_data_extraction_status_job.rb b/app/jobs/metais/project_data_extraction_status_job.rb new file mode 100644 index 0000000..0827951 --- /dev/null +++ b/app/jobs/metais/project_data_extraction_status_job.rb @@ -0,0 +1,17 @@ +class Metais::ProjectDataExtractionStatusJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid, location_header) + url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}/status" + response = Net::HTTP.get_response(URI(url)) + + if response.key?('Retry-After') + Metais::ProjectDataExtractionStatusJob.set(wait: response['Retry-After'].to_i.seconds).perform_later(project_uuid, location_header) + else + location = response['Location'] + raise "Location header missing in response" unless location + + Metais::ProjectDataExtractionResultJob.perform_later(project_uuid, location) + end + end +end diff --git a/app/jobs/metais/sync_project_documents_job.rb b/app/jobs/metais/sync_project_documents_job.rb index 409e52e..8b68a17 100644 --- a/app/jobs/metais/sync_project_documents_job.rb +++ b/app/jobs/metais/sync_project_documents_job.rb @@ -10,7 +10,7 @@ def perform(project_origin, metais_project) project_document.name = document.latest_version.nazov project_document.filename = document.latest_version.filename project_document.value = 'https://metais.vicepremier.gov.sk/dms/file/' + document.uuid - + project_document.description = "MetaIS" project_document.save! end end diff --git a/app/jobs/metais/sync_project_events_job.rb b/app/jobs/metais/sync_project_events_job.rb index 0397e25..0f49aa3 100644 --- a/app/jobs/metais/sync_project_events_job.rb +++ b/app/jobs/metais/sync_project_events_job.rb @@ -11,8 +11,8 @@ def perform(project_origin, metais_project) event_date = change.created_at event_name = Datahub::Metais::CodelistProjectState.find_by(code: change.new_value)&.nazov || change.new_value event_value = "Stav projektu bol zmenený z - #{Datahub::Metais::CodelistProjectState.find_by(code: change.old_value)&.nazov || change.new_value} na - #{Datahub::Metais::CodelistProjectState.find_by(code: change.new_value)&.nazov || change.new_value}" + #{Datahub::Metais::CodelistProjectState.find_by(code: change.old_value)&.nazov.downcase || change.new_value.downcase} na + #{Datahub::Metais::CodelistProjectState.find_by(code: change.new_value)&.nazov.downcase || change.new_value.downcase}" project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, origin_type: origin_type, @@ -26,8 +26,8 @@ def perform(project_origin, metais_project) event_date = change.created_at event_name = Datahub::Metais::CodelistProjectPhase.find_by(code: change.new_value)&.nazov || change.new_value event_value = "Fáza projektu bola zmenená z - #{Datahub::Metais::CodelistProjectPhase.find_by(code: change.old_value)&.nazov || change.old_value} na - #{Datahub::Metais::CodelistProjectPhase.find_by(code: change.new_value)&.nazov || change.new_value}" + #{Datahub::Metais::CodelistProjectPhase.find_by(code: change.old_value)&.nazov.downcase || change.old_value.downcase} na + #{Datahub::Metais::CodelistProjectPhase.find_by(code: change.new_value)&.nazov.downcase || change.new_value.downcase}" project_event = Metais::ProjectEvent.find_or_initialize_by(project_origin: project_origin, origin_type: origin_type, diff --git a/app/jobs/metais/sync_project_job.rb b/app/jobs/metais/sync_project_job.rb index c482079..eb28e60 100644 --- a/app/jobs/metais/sync_project_job.rb +++ b/app/jobs/metais/sync_project_job.rb @@ -24,6 +24,7 @@ def perform(project, metais_project) project_origin.save! + Metais::ProjectDataExtractionJob.set(wait: 5.minutes).perform_later(metais_project.uuid) Metais::SyncProjectSuppliersJob.perform_later(project_origin, metais_project) Metais::SyncProjectDocumentsJob.perform_later(project_origin, metais_project) Metais::SyncProjectEventsJob.perform_later(project_origin, metais_project) diff --git a/app/jobs/sync_all_topics_job.rb b/app/jobs/sync_all_topics_job.rb index a111851..1359abb 100644 --- a/app/jobs/sync_all_topics_job.rb +++ b/app/jobs/sync_all_topics_job.rb @@ -1,7 +1,7 @@ class SyncAllTopicsJob < ApplicationJob queue_as :default - COLUMN_NAMES = ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'].freeze + COLUMN_NAMES = ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'].freeze def perform(sync_all: true) sheets_service = GoogleApiService.get_sheets_service @@ -21,6 +21,7 @@ def find_indices(header_row) end def process_row(row, indices, sync_all) + project_metais_code = row[indices["MetaIS"]] project_name = row[indices["Projekt"]] project_id = row[indices["Projekt ID"]] platform_link = row[indices["Platforma"]] @@ -29,6 +30,10 @@ def process_row(row, indices, sync_all) product_document_id = row[indices["ID draft produktu"]] product_page_id = row[indices["ID produktu"]] + project = Project.find_by(id: project_id) + project.metais_code = project_metais_code + project.save! + if sync_all process_row_for_sync_all(project_name, project_id, platform_link, preparation_document_id, preparation_page_id, product_document_id, product_page_id) else diff --git a/db/migrate/20240822112026_add_benefits_to_metais_project_origins.rb b/db/migrate/20240822112026_add_benefits_to_metais_project_origins.rb new file mode 100644 index 0000000..6814422 --- /dev/null +++ b/db/migrate/20240822112026_add_benefits_to_metais_project_origins.rb @@ -0,0 +1,5 @@ +class AddBenefitsToMetaisProjectOrigins < ActiveRecord::Migration[5.1] + def change + add_column :'metais.project_origins', :benefits, :decimal, precision: 15, scale: 2 + end +end diff --git a/db/migrate/20240822143116_add_description_to_metais_project_documents.rb b/db/migrate/20240822143116_add_description_to_metais_project_documents.rb new file mode 100644 index 0000000..98562b2 --- /dev/null +++ b/db/migrate/20240822143116_add_description_to_metais_project_documents.rb @@ -0,0 +1,5 @@ +class AddDescriptionToMetaisProjectDocuments < ActiveRecord::Migration[5.1] + def change + add_column :'metais.project_documents', :description, :string + end +end diff --git a/db/structure.sql b/db/structure.sql index 8e41a20..96b173a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -64,7 +64,8 @@ CREATE TABLE metais.project_documents ( created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, filename character varying, - uuid character varying NOT NULL + uuid character varying NOT NULL, + description character varying ); @@ -218,7 +219,8 @@ CREATE TABLE metais.project_origins ( approved_operation numeric(15,2), metais_created_at timestamp without time zone, status_change_date timestamp without time zone, - supplier_cin bigint + supplier_cin bigint, + benefits numeric(15,2) ); @@ -1557,6 +1559,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240820080303'), ('20240820193320'), ('20240821210736'), -('20240821212620'); +('20240821212620'), +('20240822112026'), +('20240822143116'); From 0bb2f846ccc87e12a5f765ecce67f28719969635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Fri, 23 Aug 2024 02:03:39 +0200 Subject: [PATCH 06/39] Jobs rspec tests added --- app/jobs/metais/sync_project_suppliers_job.rb | 2 - app/jobs/sync_all_topics_job.rb | 9 +- spec/factories/metais/origin_type.rb | 5 + spec/factories/metais/project.rb | 7 + .../metais/daily_sync_projects_job_spec.rb | 49 ++++++ .../metais/initial_sync_projects_job_spec.rb | 22 +++ .../project_data_extraction_job_spec.rb | 59 ++++++++ ...project_data_extraction_result_job_spec.rb | 141 ++++++++++++++++++ ...project_data_extraction_status_job_spec.rb | 68 +++++++++ .../metais/sync_project_documents_job_spec.rb | 57 +++++++ .../metais/sync_project_events_job_spec.rb | 73 +++++++++ spec/jobs/metais/sync_project_job_spec.rb | 76 ++++++++++ .../metais/sync_project_suppliers_job_spec.rb | 58 +++++++ spec/jobs/sync_all_topics_job_spec.rb | 23 +-- 14 files changed, 634 insertions(+), 15 deletions(-) create mode 100644 spec/factories/metais/origin_type.rb create mode 100644 spec/factories/metais/project.rb create mode 100644 spec/jobs/metais/daily_sync_projects_job_spec.rb create mode 100644 spec/jobs/metais/initial_sync_projects_job_spec.rb create mode 100644 spec/jobs/metais/project_data_extraction_job_spec.rb create mode 100644 spec/jobs/metais/project_data_extraction_result_job_spec.rb create mode 100644 spec/jobs/metais/project_data_extraction_status_job_spec.rb create mode 100644 spec/jobs/metais/sync_project_documents_job_spec.rb create mode 100644 spec/jobs/metais/sync_project_events_job_spec.rb create mode 100644 spec/jobs/metais/sync_project_job_spec.rb create mode 100644 spec/jobs/metais/sync_project_suppliers_job_spec.rb diff --git a/app/jobs/metais/sync_project_suppliers_job.rb b/app/jobs/metais/sync_project_suppliers_job.rb index 172c2c1..6047c3b 100644 --- a/app/jobs/metais/sync_project_suppliers_job.rb +++ b/app/jobs/metais/sync_project_suppliers_job.rb @@ -7,8 +7,6 @@ class Metais::SyncProjectSuppliersJob < ApplicationJob ORIGIN_TYPE = Metais::OriginType.find_by(name: 'MetaIS') def perform(project_origin, metais_project) - puts "Start" - if metais_project.latest_version.link_nfp.present? supplier_type = Metais::SupplierType.find_by(name: "NFP") diff --git a/app/jobs/sync_all_topics_job.rb b/app/jobs/sync_all_topics_job.rb index 1359abb..26ea1a0 100644 --- a/app/jobs/sync_all_topics_job.rb +++ b/app/jobs/sync_all_topics_job.rb @@ -30,9 +30,12 @@ def process_row(row, indices, sync_all) product_document_id = row[indices["ID draft produktu"]] product_page_id = row[indices["ID produktu"]] - project = Project.find_by(id: project_id) - project.metais_code = project_metais_code - project.save! + + if project_metais_code.present? + project = Project.find_by(id: project_id) + project.metais_code = project_metais_code + project.save! + end if sync_all process_row_for_sync_all(project_name, project_id, platform_link, preparation_document_id, preparation_page_id, product_document_id, product_page_id) diff --git a/spec/factories/metais/origin_type.rb b/spec/factories/metais/origin_type.rb new file mode 100644 index 0000000..0882449 --- /dev/null +++ b/spec/factories/metais/origin_type.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :metais_origin_type, class: 'Metais::OriginType' do + name { "MetaIS" } + end +end \ No newline at end of file diff --git a/spec/factories/metais/project.rb b/spec/factories/metais/project.rb new file mode 100644 index 0000000..9ac9ce8 --- /dev/null +++ b/spec/factories/metais/project.rb @@ -0,0 +1,7 @@ +# spec/factories/metais_projects.rb +FactoryBot.define do + factory :metais_project, class: 'Metais::Project' do + code { "project_code" } + uuid { "project_uuid" } + end +end diff --git a/spec/jobs/metais/daily_sync_projects_job_spec.rb b/spec/jobs/metais/daily_sync_projects_job_spec.rb new file mode 100644 index 0000000..7ed2de2 --- /dev/null +++ b/spec/jobs/metais/daily_sync_projects_job_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +RSpec.describe Metais::SyncProjectSuppliersJob, type: :job do + let(:supplier_type_nfp) { Metais::SupplierType.create!(name: "NFP") } + let(:supplier_type_vo) { Metais::SupplierType.create!(name: "VO") } + let(:supplier_type_crz) { Metais::SupplierType.create!(name: "CRZ") } + + let(:project_origin) { Metais::ProjectOrigin.create! } + + let(:latest_version) { double('LatestVersion', link_nfp: 'http://example.com/nfp', vo: 'http://example.com/vo', zmluva_o_dielo_crz: 'http://example.com/crz', datum_nfp: Date.today, vyhlasenie_vo: Date.today, zmluva_o_dielo: Date.today) } + let(:metais_project) { instance_double(Metais::Project, latest_version: latest_version) } + + before do + allow(Metais::SupplierType).to receive(:find_by).with(name: "NFP").and_return(supplier_type_nfp) + allow(Metais::SupplierType).to receive(:find_by).with(name: "VO").and_return(supplier_type_vo) + allow(Metais::SupplierType).to receive(:find_by).with(name: "CRZ").and_return(supplier_type_crz) + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:extract_links).and_return([]) + end + + it 'creates ProjectSupplier records for NFP links' do + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:extract_links).with('http://example.com/nfp').and_return(['http://supplier1.com', 'http://supplier2.com']) + expect { described_class.perform_now(project_origin, metais_project) }.to change(Metais::ProjectSupplier, :count).by(2) + + suppliers = Metais::ProjectSupplier.where(supplier_type: supplier_type_nfp) + expect(suppliers.map(&:name)).to contain_exactly('http://supplier1.com', 'http://supplier2.com') + end + + it 'creates ProjectSupplier records for VO links' do + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:extract_links).with('http://example.com/vo').and_return(['http://supplier3.com']) + expect { described_class.perform_now(project_origin, metais_project) }.to change(Metais::ProjectSupplier, :count).by(1) + + supplier = Metais::ProjectSupplier.find_by(name: 'http://supplier3.com') + expect(supplier).to be_present + expect(supplier.supplier_type).to eq(supplier_type_vo) + end + + it 'updates project_origin with supplier info for CRZ links' do + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:extract_links).with('http://example.com/crz').and_return(['http://supplier4.com']) + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:valid_url?).and_return(true) + + # Mock the parsing to return specific values + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:parse_crz_document).and_return({ supplier: 'Supplier Info', cin: '12345678' }) + + expect { described_class.perform_now(project_origin, metais_project) }.to change(Metais::ProjectSupplier, :count).by(1) + + expect(project_origin.supplier).to eq('Supplier Info') + expect(project_origin.supplier_cin).to eq('12345678') + end +end diff --git a/spec/jobs/metais/initial_sync_projects_job_spec.rb b/spec/jobs/metais/initial_sync_projects_job_spec.rb new file mode 100644 index 0000000..dd675ee --- /dev/null +++ b/spec/jobs/metais/initial_sync_projects_job_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe Metais::InitialSyncProjectsJob, type: :job do + let(:latest_version) { double('LatestVersion', kod_metais: 'code1') } + let(:datahub_project) { instance_double(Datahub::Metais::Project, uuid: 'uuid1', latest_version: latest_version) } + let(:metais_project) { instance_double(Metais::Project, save!: true) } + + before do + allow(Datahub::Metais::Project).to receive(:find_each).and_yield(datahub_project) + allow(Metais::Project).to receive(:find_or_initialize_by).with(code: 'code1', uuid: 'uuid1').and_return(metais_project) + allow(Metais::SyncProjectJob).to receive(:perform_later) + end + + it 'processes each Datahub project and enqueues a SyncProjectJob' do + Metais::InitialSyncProjectsJob.perform_now + + expect(Datahub::Metais::Project).to have_received(:find_each) + expect(Metais::Project).to have_received(:find_or_initialize_by).with(code: 'code1', uuid: 'uuid1') + expect(metais_project).to have_received(:save!) + expect(Metais::SyncProjectJob).to have_received(:perform_later).with(metais_project, datahub_project) + end +end diff --git a/spec/jobs/metais/project_data_extraction_job_spec.rb b/spec/jobs/metais/project_data_extraction_job_spec.rb new file mode 100644 index 0000000..2f00510 --- /dev/null +++ b/spec/jobs/metais/project_data_extraction_job_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' +require 'net/http' + +RSpec.describe Metais::ProjectDataExtractionJob, type: :job do + let(:project_uuid) { 'sample-uuid' } + let(:api_url) { 'http://example.com/api' } + let(:url) { "#{api_url}/projects/#{project_uuid}" } + + before do + allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + end + + describe '#perform' do + let(:response) { instance_double(Net::HTTPResponse) } + + before do + allow(Net::HTTP).to receive(:post).and_return(response) + end + + context 'when the response status is 202 Accepted' do + before do + allow(response).to receive(:is_a?).with(Net::HTTPAccepted).and_return(true) + allow(response).to receive(:[]).with('Location').and_return('http://example.com/location') + end + + it 'enqueues the Metais::ProjectDataExtractionStatusJob with the correct parameters' do + expect(Metais::ProjectDataExtractionStatusJob).to receive(:perform_later).with(project_uuid, 'http://example.com/location') + subject.perform(project_uuid) + end + end + + context 'when the response status is not 202 Accepted' do + before do + allow(response).to receive(:is_a?).with(Net::HTTPAccepted).and_return(false) + allow(response).to receive(:code).and_return('400') + allow(response).to receive(:body).and_return('Bad Request') + end + + it 'raises a RuntimeError with the appropriate message' do + expect { + subject.perform(project_uuid) + }.to raise_error(RuntimeError, /Response status is 400. Message: Bad Request/) + end + end + + context 'when the Location header is missing' do + before do + allow(response).to receive(:is_a?).with(Net::HTTPAccepted).and_return(true) + allow(response).to receive(:[]).with('Location').and_return(nil) + end + + it 'raises a RuntimeError indicating the missing Location header' do + expect { + subject.perform(project_uuid) + }.to raise_error(RuntimeError, /Expected 'Location' header not found in the response./) + end + end + end +end diff --git a/spec/jobs/metais/project_data_extraction_result_job_spec.rb b/spec/jobs/metais/project_data_extraction_result_job_spec.rb new file mode 100644 index 0000000..f6b9c37 --- /dev/null +++ b/spec/jobs/metais/project_data_extraction_result_job_spec.rb @@ -0,0 +1,141 @@ +require 'rails_helper' +require 'net/http' + +RSpec.describe Metais::ProjectDataExtractionResultJob, type: :job do + include ActiveJob::TestHelper + + let(:project_uuid) { 'sample-uuid' } + let(:location_header) { 'http://example.com/location' } + let(:api_url) { 'http://example.com/api' } + let(:url) { "#{api_url}/projects/#{project_uuid}" } + + let(:response) { instance_double('Net::HTTPResponse') } + let(:body) { { 'status' => 'Done', 'result' => result_data }.to_json } + let(:result_data) do + { + 'harmonogram' => [], + 'responsible' => { 'first_name' => 'John', 'surname' => 'Doe' }, + 'capex' => 1000, + 'opex' => 500, + 'declared' => 200, + 'kpis' => ['KPI 1', 'KPI 2'], + 'goals' => ['Goal 1', 'Goal 2'] + } + end + let(:metais_project) { instance_double('Metais::Project') } + let(:project_origin) { instance_double('Metais::ProjectOrigin') } + let(:origin_type) { instance_double('Metais::OriginType') } + let(:event_type) { instance_double('Metais::ProjectEventType') } + + let(:project_origins_double) { double('ProjectOrigins') } + + before do + allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + end + + describe '#perform' do + before do + allow(Net::HTTP).to receive(:get_response).and_return(response) + end + + context 'when the response is successful and the project exists' do + before do + allow(response).to receive(:code).and_return('200') + allow(response).to receive(:body).and_return(body) + allow(JSON).to receive(:parse).and_return('status' => 'Done', 'result' => result_data) + + allow(Metais::Project).to receive(:find_by).with(uuid: project_uuid).and_return(metais_project) + + allow(metais_project).to receive(:project_origins).and_return(project_origins_double) + + allow(project_origins_double).to receive(:first).and_return(double('FirstProjectOrigin', title: 'Origin Title')) + allow(project_origins_double).to receive(:find_or_initialize_by).with( + title: 'Origin Title', + project: metais_project, + origin_type: origin_type + ).and_return(project_origin) + + allow(project_origin).to receive(:project_manager=) + allow(project_origin).to receive(:approved_investment=) + allow(project_origin).to receive(:approved_operation=) + allow(project_origin).to receive(:benefits=) + allow(project_origin).to receive(:targets_text=) + allow(project_origin).to receive(:save!) + + allow(Metais::OriginType).to receive(:find_by).with(name: 'AI').and_return(origin_type) + + allow(Metais::ProjectEventType).to receive(:find_by).with(name: 'Predpoklad').and_return(event_type) + + allow(Metais::ProjectEvent).to receive(:find_or_initialize_by).and_return(double('Metais::ProjectEvent', save!: true)) + + allow(Net::HTTP).to receive(:start).and_return(double('Net::HTTPResponse', is_a?: true, code: '200')) + end + + it 'successfully processes the result and sends a delete request' do + expect { + described_class.perform_now(project_uuid, location_header) + }.not_to have_enqueued_job + + expect(Net::HTTP).to have_received(:start) + end + end + + context 'when the response status is not 200 or 202' do + before do + allow(response).to receive(:code).and_return('500') + allow(response).to receive(:body).and_return('Internal Server Error') + end + + it 'raises a RuntimeError with the correct message' do + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error(RuntimeError, /Unexpected response status: 500, body: Internal Server Error/) + end + end + + context 'when JSON parsing fails' do + before do + allow(response).to receive(:code).and_return('200') + allow(response).to receive(:body).and_return('invalid json') + allow(JSON).to receive(:parse).and_raise(JSON::ParserError.new('error')) + end + + it 'raises a RuntimeError with the correct message' do + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error(RuntimeError, /Failed to parse JSON response: error/) + end + end + + context 'when project cannot be found' do + before do + allow(response).to receive(:code).and_return('200') + allow(response).to receive(:body).and_return(body) + allow(JSON).to receive(:parse).and_return('status' => 'Done', 'result' => {}) + allow(Metais::Project).to receive(:find_by).with(uuid: project_uuid).and_return(nil) + end + + it 'raises a RuntimeError indicating the project was not found' do + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error(RuntimeError, /Couldn't find MetaIS project with 'uuid'=sample-uuid/) + end + end + + context 'when project origin cannot be found or saved' do + before do + allow(response).to receive(:code).and_return('200') + allow(response).to receive(:body).and_return(body) + allow(JSON).to receive(:parse).and_return('status' => 'Done', 'result' => result_data) + + allow(project_origins_double).to receive(:find_or_initialize_by).and_return(nil) + end + + it 'raises an error when the project origin cannot be found or initialized' do + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error(RuntimeError, /Couldn't find MetaIS project with 'uuid'=sample-uuid/) + end + end + end +end diff --git a/spec/jobs/metais/project_data_extraction_status_job_spec.rb b/spec/jobs/metais/project_data_extraction_status_job_spec.rb new file mode 100644 index 0000000..1483f1f --- /dev/null +++ b/spec/jobs/metais/project_data_extraction_status_job_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' +require 'net/http' + +RSpec.describe Metais::ProjectDataExtractionStatusJob, type: :job do + include ActiveJob::TestHelper + + let(:project_uuid) { 'sample-uuid' } + let(:location_header) { 'http://example.com/location' } + let(:api_url) { 'http://example.com/api' } + let(:url) { "#{api_url}/projects/#{project_uuid}/status" } + + before do + allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + end + + describe '#perform' do + let(:response) { instance_double(Net::HTTPResponse) } + + before do + allow(Net::HTTP).to receive(:get_response).and_return(response) + end + + context 'when Retry-After header is present' do + before do + allow(response).to receive(:key?).with('Retry-After').and_return(true) + allow(response).to receive(:[]).with('Retry-After').and_return('10') + end + + it 're-enqueues the job with a delay' do + Metais::ProjectDataExtractionStatusJob.perform_now(project_uuid, location_header) + + expect(enqueued_jobs).to include( + a_hash_including( + job: Metais::ProjectDataExtractionStatusJob, + args: [project_uuid, location_header], + queue: 'metais_data_extraction' + ) + ) + end + end + + context 'when Retry-After header is not present' do + before do + allow(response).to receive(:key?).with('Retry-After').and_return(false) + allow(response).to receive(:[]).with('Location').and_return('http://example.com/new_location') + end + + it 'enqueues the Metais::ProjectDataExtractionResultJob with the correct parameters' do + expect { + subject.perform(project_uuid, location_header) + }.to have_enqueued_job(Metais::ProjectDataExtractionResultJob).with(project_uuid, 'http://example.com/new_location') + end + + context 'when Location header is missing' do + before do + allow(response).to receive(:key?).with('Retry-After').and_return(false) + allow(response).to receive(:[]).with('Location').and_return(nil) + end + + it 'raises an error indicating the missing Location header' do + expect { + subject.perform(project_uuid, location_header) + }.to raise_error(RuntimeError, /Location header missing in response/) + end + end + end + end +end diff --git a/spec/jobs/metais/sync_project_documents_job_spec.rb b/spec/jobs/metais/sync_project_documents_job_spec.rb new file mode 100644 index 0000000..32e0d9c --- /dev/null +++ b/spec/jobs/metais/sync_project_documents_job_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Metais::SyncProjectDocumentsJob, type: :job do + include ActiveJob::TestHelper + + let(:latest_version) do + double('LatestVersion', + nazov: 'Document Title', + filename: 'document.pdf') + end + + let(:document) do + double('Document', + uuid: 'doc-uuid', + latest_version: latest_version) + end + + let(:metais_project) do + double('MetaisProject', documents: [document]) + end + + let(:project_origin) { double('ProjectOrigin') } + let(:project_document) { instance_double(Metais::ProjectDocument, save!: true) } + + before do + allow(Metais::ProjectDocument).to receive(:find_or_initialize_by).and_return(project_document) + + allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(double('OriginType')) + + allow(project_document).to receive(:name=) + allow(project_document).to receive(:filename=) + allow(project_document).to receive(:value=) + allow(project_document).to receive(:description=) + end + + after do + clear_enqueued_jobs + clear_performed_jobs + end + + it 'processes each document and creates or updates ProjectDocument records' do + Metais::SyncProjectDocumentsJob.perform_now(project_origin, metais_project) + + expect(Metais::ProjectDocument).to have_received(:find_or_initialize_by).with( + uuid: document.uuid, + project_origin: project_origin, + origin_type: anything + ) + + expect(project_document).to have_received(:name=).with('Document Title') + expect(project_document).to have_received(:filename=).with('document.pdf') + expect(project_document).to have_received(:value=).with('https://metais.vicepremier.gov.sk/dms/file/doc-uuid') + expect(project_document).to have_received(:description=).with('MetaIS') + + expect(project_document).to have_received(:save!) + end +end diff --git a/spec/jobs/metais/sync_project_events_job_spec.rb b/spec/jobs/metais/sync_project_events_job_spec.rb new file mode 100644 index 0000000..9568b09 --- /dev/null +++ b/spec/jobs/metais/sync_project_events_job_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +RSpec.describe Metais::SyncProjectEventsJob, type: :job do + include ActiveJob::TestHelper + + let(:project_origin) { instance_double('Metais::ProjectOrigin') } + let(:metais_project) { instance_double('Metais::Project', latest_version: 'v1') } + let(:origin_type) { instance_double('Metais::OriginType') } + let(:event_type) { instance_double('Metais::ProjectEventType') } + + let(:status_change) do + instance_double('Datahub::Metais::ProjectChange', + field: 'status', + created_at: Time.now, + new_value: 'new_status', + old_value: 'old_status') + end + + let(:phase_change) do + instance_double('Datahub::Metais::ProjectChange', + field: 'faza_projektu', + created_at: Time.now, + new_value: 'new_phase', + old_value: 'old_phase') + end + + let(:codelist_project_state_old) { instance_double('Datahub::Metais::CodelistProjectState', nazov: 'Old Status') } + let(:codelist_project_state_new) { instance_double('Datahub::Metais::CodelistProjectState', nazov: 'New Status') } + let(:codelist_project_phase_old) { instance_double('Datahub::Metais::CodelistProjectPhase', nazov: 'Old Phase') } + let(:codelist_project_phase_new) { instance_double('Datahub::Metais::CodelistProjectPhase', nazov: 'New Phase') } + + before do + allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(origin_type) + allow(Metais::ProjectEventType).to receive(:find_by).with(name: 'Realita').and_return(event_type) + + allow(Datahub::Metais::ProjectChange).to receive(:where).with(project_version: metais_project.latest_version).and_return([status_change, phase_change]) + + allow(Datahub::Metais::CodelistProjectState).to receive(:find_by).with(code: 'old_status').and_return(codelist_project_state_old) + allow(Datahub::Metais::CodelistProjectState).to receive(:find_by).with(code: 'new_status').and_return(codelist_project_state_new) + allow(Datahub::Metais::CodelistProjectPhase).to receive(:find_by).with(code: 'old_phase').and_return(codelist_project_phase_old) + allow(Datahub::Metais::CodelistProjectPhase).to receive(:find_by).with(code: 'new_phase').and_return(codelist_project_phase_new) + + @project_event_double = instance_double('Metais::ProjectEvent') + allow(Metais::ProjectEvent).to receive(:find_or_initialize_by).and_return(@project_event_double) + allow(@project_event_double).to receive(:save!) + end + + describe '#perform' do + it 'processes all changes and creates events correctly' do + described_class.perform_now(project_origin, metais_project) + + expect(Metais::ProjectEvent).to have_received(:find_or_initialize_by).with( + project_origin: project_origin, + origin_type: origin_type, + event_type: event_type, + name: 'New Status', + value: "Stav projektu bol zmenený z\n old status na\n new status", + date: status_change.created_at + ).once + + expect(Metais::ProjectEvent).to have_received(:find_or_initialize_by).with( + project_origin: project_origin, + origin_type: origin_type, + event_type: event_type, + name: 'New Phase', + value: "Fáza projektu bola zmenená z\n old phase na\n new phase", + date: phase_change.created_at + ).once + + expect(@project_event_double).to have_received(:save!).twice + end + end +end diff --git a/spec/jobs/metais/sync_project_job_spec.rb b/spec/jobs/metais/sync_project_job_spec.rb new file mode 100644 index 0000000..b11f18f --- /dev/null +++ b/spec/jobs/metais/sync_project_job_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +RSpec.describe Metais::SyncProjectJob, type: :job do + include ActiveJob::TestHelper + + let(:latest_version) do + double('LatestVersion', + nazov: 'Project Title', + popis: 'Project Description', + status: 'Active', + faza_projektu: 'Phase 1', + prijimatel: 'Guarantor Name', + metais_created_at: Time.now, + datum_zacatia: Date.today, + termin_ukoncenia: Date.today + 1.year, + zmena_stavu: Date.today, + program: 'Program UUID', + suma_vydavkov: 100_000, + rocne_naklady: 20_000, + schvaleny_rozpocet: 120_000, + schvalene_rocne_naklady: 25_000) + end + + let(:metais_project) { double(Datahub::Metais::Project, latest_version: latest_version, uuid: 'uuid1') } + let(:project) { double(Metais::Project) } + let(:project_origin) { instance_double(Metais::ProjectOrigin, save!: true) } + + before do + allow(Metais::ProjectOrigin).to receive(:find_or_initialize_by).and_return(project_origin) + + allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(double('OriginType')) + allow(Datahub::Metais::CodelistProjectState).to receive(:find_by).with(code: 'Active').and_return(double('ProjectState', nazov: 'Active State')) + allow(Datahub::Metais::CodelistProjectPhase).to receive(:find_by).with(code: 'Phase 1').and_return(double('ProjectPhase', nazov: 'Phase 1')) + allow(Datahub::Metais::CodelistProgram).to receive(:find_by).with(uuid: 'Program UUID').and_return(double('Program', nazov: 'Program Name')) + + allow(project_origin).to receive(:title=) + allow(project_origin).to receive(:description=) + allow(project_origin).to receive(:status=) + allow(project_origin).to receive(:phase=) + allow(project_origin).to receive(:guarantor=) + allow(project_origin).to receive(:metais_created_at=) + allow(project_origin).to receive(:start_date=) + allow(project_origin).to receive(:end_date=) + allow(project_origin).to receive(:status_change_date=) + allow(project_origin).to receive(:finance_source=) + allow(project_origin).to receive(:investment=) + allow(project_origin).to receive(:operation=) + allow(project_origin).to receive(:approved_investment=) + allow(project_origin).to receive(:approved_operation=) + + allow(Metais::ProjectDataExtractionJob).to receive(:perform_later) + allow(Metais::SyncProjectSuppliersJob).to receive(:perform_later) + allow(Metais::SyncProjectDocumentsJob).to receive(:perform_later) + allow(Metais::SyncProjectEventsJob).to receive(:perform_later) + end + + after do + clear_enqueued_jobs + clear_performed_jobs + end + + it 'updates or creates a ProjectOrigin and enqueues subsequent jobs' do + Metais::SyncProjectJob.perform_now(project, metais_project) + + expect(enqueued_jobs).to include( + a_hash_including( + job: Metais::ProjectDataExtractionJob, + args: [metais_project.uuid], + queue: 'metais_data_extraction' + ) + ) + expect(Metais::SyncProjectSuppliersJob).to have_received(:perform_later).with(project_origin, metais_project) + expect(Metais::SyncProjectDocumentsJob).to have_received(:perform_later).with(project_origin, metais_project) + expect(Metais::SyncProjectEventsJob).to have_received(:perform_later).with(project_origin, metais_project) + end +end diff --git a/spec/jobs/metais/sync_project_suppliers_job_spec.rb b/spec/jobs/metais/sync_project_suppliers_job_spec.rb new file mode 100644 index 0000000..6ef6d3c --- /dev/null +++ b/spec/jobs/metais/sync_project_suppliers_job_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' +require 'webmock/rspec' + +RSpec.describe Metais::SyncProjectSuppliersJob, type: :job do + include ActiveJob::TestHelper + + let(:latest_version) do + double('LatestVersion', + link_nfp: 'http://example.com/nfp', + datum_nfp: Date.today, + vo: 'http://example.com/vo', + vyhlasenie_vo: Date.today, + zmluva_o_dielo_crz: 'http://example.com/crz', + zmluva_o_dielo: Date.today) + end + + let(:metais_project) do + double('MetaisProject', latest_version: latest_version) + end + + let(:project_origin) { double('ProjectOrigin') } + let(:project_supplier) { instance_double(Metais::ProjectSupplier, save!: true) } + + before do + # Stub HTTP requests + stub_request(:get, 'http://example.com/crz').to_return(status: 200, body: "response body", headers: {}) + + allow(Metais::ProjectSupplier).to receive(:find_or_initialize_by).and_return(project_supplier) + allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(double('OriginType')) + + allow(project_supplier).to receive(:name=) + allow(project_supplier).to receive(:value=) + allow(project_supplier).to receive(:date=) + end + + after do + clear_enqueued_jobs + clear_performed_jobs + end + + it 'processes each supplier and creates or updates ProjectSuppliers records' do + Metais::SyncProjectSuppliersJob.perform_now(project_origin, metais_project) + + expect(Metais::ProjectSupplier).to have_received(:find_or_initialize_by).with( + name: 'http://example.com/nfp', + value: 'http://example.com/nfp', + date: Date.today, + project_origin: project_origin, + origin_type: anything, + supplier_type: anything + ) + + expect(project_supplier).to have_received(:name=).with('http://example.com/nfp') + expect(project_supplier).to have_received(:value=).with('http://example.com/nfp') + expect(project_supplier).to have_received(:date=).with(Date.today) + expect(project_supplier).to have_received(:save!) + end +end diff --git a/spec/jobs/sync_all_topics_job_spec.rb b/spec/jobs/sync_all_topics_job_spec.rb index 9f6d4a9..3521e56 100644 --- a/spec/jobs/sync_all_topics_job_spec.rb +++ b/spec/jobs/sync_all_topics_job_spec.rb @@ -10,15 +10,18 @@ [ [], [], - ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], - ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], - ['Projekt2', 'ABC2', '', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 1, '', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 2, 'projekt_2741', '', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] ] end - let(:indices) { { 'Projekt' => 0, 'Projekt ID' => 1, 'Platforma' => 2, 'ID draft prípravy' => 3, 'ID prípravy' => 4, 'ID draft produktu' => 5, 'ID produktu' => 6 } } + let(:indices) { { 'Projekt' => 0, 'Projekt ID' => 1, 'MetaIS' => 2,'Platforma' => 3, 'ID draft prípravy' => 4, 'ID prípravy' => 5, 'ID draft produktu' => 6, 'ID produktu' => 7 } } before do + Project.create(id: 1) + Project.create(id: 2) + mock_document = instance_double("Google::Apis::DocsV1::Document") allow(mock_document).to receive(:title).and_return("Dokument RF-priprava-template") allow(GoogleApiService).to receive(:get_document).and_return(mock_document) @@ -45,8 +48,8 @@ [], [], ['Projekt'], - ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], - ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ['Projekt1', 1, '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 2, 'projekt_2741', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] ] end @@ -61,7 +64,7 @@ [ [], [], - ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'] + ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'] ] end @@ -76,9 +79,9 @@ [ [], [], - ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], - ['Projekt1', 'ABC1', '', 'ABC1', '', 'ABC1', ''], - ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 1, '', '', 'ABC1', '', 'ABC1', ''], + ['Projekt2', 2, 'projekt_2741', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] ] end From 9ca24b395bf993c301513e3a4c70744db2bd21d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Fri, 23 Aug 2024 11:48:43 +0200 Subject: [PATCH 07/39] Added documents to edit, tailwind style status badges, project index FE improvments --- app/assets/stylesheets/application.scss | 2 + app/assets/stylesheets/statuses.scss | 86 ++++++++++--------- .../metais/project_origins_controller.rb | 16 ++++ app/controllers/metais/projects_controller.rb | 8 ++ .../metais/project_origins/edit.html.erb | 11 +++ app/views/metais/projects/index.html.erb | 18 ++-- app/views/metais/projects/show.html.erb | 22 +++-- config/routes.rb | 1 + ...ull_on_uuid_in_metais_project_documents.rb | 5 ++ db/structure.sql | 5 +- 10 files changed, 111 insertions(+), 63 deletions(-) create mode 100644 db/migrate/20240823082322_allow_null_on_uuid_in_metais_project_documents.rb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 38c8be4..dcb5fd9 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -24,6 +24,8 @@ h1, h2, h3, h4, h5, h6 { .table-title-item, .table-price { font-family: $headings-font-family; + text-decoration: underline; + color: $black; } #filterForm.collapsing { diff --git a/app/assets/stylesheets/statuses.scss b/app/assets/stylesheets/statuses.scss index 6ead29f..5bb3566 100644 --- a/app/assets/stylesheets/statuses.scss +++ b/app/assets/stylesheets/statuses.scss @@ -1,83 +1,85 @@ .state-badge { - border: 2px solid; - border-radius: 10px; - padding: 5px 15px; + border: 1px solid; + border-radius: 5px; + padding: 2px 10px; + font-size: 0.75rem; + font-weight: 500; } .status-1 { - background-color: rgba(255, 0, 0, 0.25); - color: #FF0000; - border-color: #FF0000; + background-color: rgb(254, 242, 242); + color: rgb(185, 28, 28); + border-color: rgba(220, 38, 38, 0.25); } .status-2 { - background-color: rgba(255, 127, 127, 0.25); - color: #FF7F7F; - border-color: #FF7F7F; + background-color: rgb(240, 253, 244); + color: rgb(21, 128, 61); + border-color: rgba(22, 163, 74, 0.25); } .status-3 { - background-color: rgba(255, 153, 153, 0.25); - color: #FF9999; - border-color: #FF9999; + background-color: rgb(239, 246, 255); + color: rgb(29, 78, 216); + border-color: rgba(29, 78, 216, 0.25); } .status-4 { - background-color: rgba(255, 178, 178, 0.25); - color: #FFB2B2; - border-color: #FFB2B2; + background-color: rgb(254, 252, 232); + color: rgb(133, 77, 14); + border-color: rgba(202, 138, 4, 0.25); } .status-5 { - background-color: rgba(255, 218, 185, 0.25); - color: #FFDAB9; - border-color: #FFDAB9; + background-color: rgb(238, 242, 255); + color: rgb(67, 56, 202); + border-color: rgba(67, 56, 202, 0.25); } .status-6 { - background-color: rgba(240, 230, 140, 0.25); - color: #F0E68C; - border-color: #F0E68C; + background-color: rgb(250, 245, 255); + color: rgb(126, 34, 206); + border-color: rgba(126, 34, 206, 0.25); } .status-7 { - background-color: rgba(255, 255, 224, 0.25); - color: #FFFFE0; - border-color: #FFFFE0; + background-color: rgb(253, 242, 248); + color: rgb(190, 24, 93); + border-color: rgba(190, 24, 93, 0.25); } .status-8 { - background-color: rgba(144, 238, 144, 0.25); - color: #90EE90; - border-color: #90EE90; + background-color: rgb(255, 247, 237); + color: rgb(194, 65, 12); + border-color: rgba(194, 65, 12, 0.25); } .status-9 { - background-color: rgba(60, 179, 113, 0.25); - color: #3CB371; - border-color: #3CB371; + background-color: rgb(240, 253, 250); + color: rgb(20, 184, 166); + border-color: rgba(20, 184, 166, 0.25); } .status-10 { - background-color: rgba(0, 255, 255, 0.25); - color: #00FFFF; - border-color: #00FFFF; + background-color: rgb(236, 254, 255); + color: rgb(6, 182, 212); + border-color: rgba(6, 182, 212, 0.25); } .status-11 { - background-color: rgba(135, 206, 250, 0.25); - color: #87CEFA; - border-color: #87CEFA; + background-color: rgb(253, 244, 255); + color: rgb(162, 28, 175); + border-color: rgba(162, 28, 175, 0.25); } .status-12 { - background-color: rgba(65, 105, 225, 0.25); - color: #4169E1; - border-color: #4169E1; + background-color: rgb(255, 241, 242); + color: rgb(225, 29, 72); + border-color: rgba(225, 29, 72, 0.25); } .status-13 { - background-color: rgba(0, 0, 255, 0.25); - color: #0000FF; - border-color: #0000FF; + background-color: rgb(247, 254, 231); + color: rgb(132, 204, 22); + border-color: rgba(132, 204, 22, 0.25); } \ No newline at end of file diff --git a/app/controllers/admin/metais/project_origins_controller.rb b/app/controllers/admin/metais/project_origins_controller.rb index 1e57464..4269c48 100644 --- a/app/controllers/admin/metais/project_origins_controller.rb +++ b/app/controllers/admin/metais/project_origins_controller.rb @@ -113,6 +113,22 @@ def add_link end end + def add_document + @project = Metais::Project.find(params[:project_id]) + @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) + + origin_type = Metais::OriginType.find_by(name: params[:document][:origin_type]) + + document_params = params.require(:document).permit(:name, :value, :description).merge(origin_type: origin_type) + @document = @project_origin.documents.create(document_params) + + if @document.save + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Dokument bol úspešné pridaný.' + else + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), alert: 'Error encountered while creating an document: ' + @event.errors.full_messages.to_sentence + end + end + def remove_event @project = Metais::Project.find(params[:project_id]) @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) diff --git a/app/controllers/metais/projects_controller.rb b/app/controllers/metais/projects_controller.rb index 2a1ff55..46dcfc0 100644 --- a/app/controllers/metais/projects_controller.rb +++ b/app/controllers/metais/projects_controller.rb @@ -86,6 +86,14 @@ def show end @all_suppliers.sort_by! { |event| event.date || Time.zone.parse('2999-12-31')} + @all_links = [] + @project_origins.each do |project_origin| + @all_links.concat(project_origin.links) + end + + @all_documents = @project_origins.flat_map(&:documents) + @documents_grouped_by_description = @all_documents.group_by(&:description) + @project_origin = @project.project_origins.first @project_origin = Metais::ProjectOrigin.includes(:documents, :suppliers).find(@project_origin.id) end diff --git a/app/views/admin/metais/project_origins/edit.html.erb b/app/views/admin/metais/project_origins/edit.html.erb index a28a309..8fafcc7 100644 --- a/app/views/admin/metais/project_origins/edit.html.erb +++ b/app/views/admin/metais/project_origins/edit.html.erb @@ -211,6 +211,7 @@ Názov Link + Popis Akcia @@ -219,11 +220,21 @@ <%= document.name %> <%= link_to document.value, document.value %> + <%= document.description %> <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_document_path(project_id: @project.id, project_origin_id: @project_origin.id, document_id: document.id), method: :delete, data: { confirm: 'Naozaj chcete item odstrániť?' } %> <% end %> + + <%= form_with(url: admin_metais_project_project_origin_add_document_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> + <%= f.text_field 'document[name]', class: "form-control" %> + <%= f.text_field 'document[value]', class: "form-control" %> + <%= f.text_field 'document[description]', class: "form-control" %> + <%= hidden_field_tag 'document[origin_type]', 'Human' %> + <%= f.submit 'Pridať', class: 'btn btn-primary' %> + <% end %> +
    diff --git a/app/views/metais/projects/index.html.erb b/app/views/metais/projects/index.html.erb index 1a9b072..698011d 100644 --- a/app/views/metais/projects/index.html.erb +++ b/app/views/metais/projects/index.html.erb @@ -186,8 +186,8 @@ - - + + @@ -195,7 +195,7 @@ -
    ProjektCenaProjektCena
    - <%= link_to project.project_origins.first.title, metais_project_path(project), class: 'table-title-item'%> + <%= link_to project.project_origins.first.title, metais_project_path(project), class: 'table-title-item hover:text-primary' %>

    Dátum poslednej zmeny:

    <%= @@ -210,18 +210,16 @@
    -

    Gestor:

    <%= if project.get_project_origin_info.guarantor.present? - project.get_project_origin_info.guarantor + content_tag(:span, project.get_project_origin_info.guarantor, class: "tag-tooltip", title: "Gestor štátného IT projektu") else - content_tag(:span, "Neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Gestor projektu nie je známy") + content_tag(:span, "Gestor neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Gestor projektu nie je známy") end %>
    -

    Stav:

    <% status = project.get_project_origin_info.status&.strip status_class = case status @@ -257,9 +255,9 @@ %> <%= if status.present? - content_tag(:span, status, class: "state-badge #{status_class}") + content_tag(:span, status, class: "state-badge #{status_class} tag-tooltip", title: "Stav, v ktorom sa projekt nachádza") else - content_tag(:span, "Neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Stav a fáza projektu nie sú známe") + content_tag(:span, "Stav neuvedený", style: "color: grey;", class: "font-italic tag-tooltip", title: "Stav a fáza projektu nie sú známe") end %>
    @@ -278,7 +276,7 @@
    + <%= if project.get_project_origin_info.approved_investment.present? && project.get_project_origin_info.approved_investment > 0 number_to_currency(project.get_project_origin_info.approved_investment, unit: "€", separator: ",", delimiter: " ", format: "%n %u") diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb index aa678c3..96e7181 100644 --- a/app/views/metais/projects/show.html.erb +++ b/app/views/metais/projects/show.html.erb @@ -214,17 +214,21 @@ <% if @project_info.documents_text.present? %>

    <%= @project_info.documents_text %>

    <% end %> -
      - <% @project_origin.documents.each do |doc| %> -
    • - <%= image_tag origin_type_logo(doc.origin_type), style: "width: 12px; height: 12px;" if doc.origin_type.present? %> - <%= link_to doc.name, doc.value %> -
    • - <% end %> -
    + <% @documents_grouped_by_description.each do |description, documents| %> +

    <%= description %>

    +
      + <% documents.each do |doc| %> +
    • + <%= image_tag origin_type_logo(doc.origin_type), style: "width: 12px; height: 12px;" if doc.origin_type.present? %> + <%= link_to doc.name, doc.value %> +
    • + <% end %> +
    + <% end %> +
    Linky
    @@ -232,7 +236,7 @@

    <%= @project_info.links_text %>

    <% end %>
      - <% @project_origin.links.each do |link| %> + <% @all_links.each do |link| %>
    • <%= image_tag origin_type_logo(link.origin_type), style: "width: 12px; height: 12px;" if link.origin_type.present? %> <%= link_to link.name, link.value %> diff --git a/config/routes.rb b/config/routes.rb index e050594..b8869d5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,7 @@ post 'add_event', to: 'project_origins#add_event', as: 'add_event' post 'add_supplier', to: 'project_origins#add_supplier', as: 'add_supplier' post 'add_link', to: 'project_origins#add_link', as: 'add_link' + post 'add_document', to: 'project_origins#add_document', as: 'add_document' end end end diff --git a/db/migrate/20240823082322_allow_null_on_uuid_in_metais_project_documents.rb b/db/migrate/20240823082322_allow_null_on_uuid_in_metais_project_documents.rb new file mode 100644 index 0000000..b4a5e0c --- /dev/null +++ b/db/migrate/20240823082322_allow_null_on_uuid_in_metais_project_documents.rb @@ -0,0 +1,5 @@ +class AllowNullOnUuidInMetaisProjectDocuments < ActiveRecord::Migration[5.1] + def change + change_column :'metais.project_documents', :uuid, :string, null: true + end +end diff --git a/db/structure.sql b/db/structure.sql index 96b173a..348834f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -64,7 +64,7 @@ CREATE TABLE metais.project_documents ( created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, filename character varying, - uuid character varying NOT NULL, + uuid character varying, description character varying ); @@ -1561,6 +1561,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240821210736'), ('20240821212620'), ('20240822112026'), -('20240822143116'); +('20240822143116'), +('20240823082322'); From 74fa0a1b0578d8ff84f4cb8f790f91978dc62807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Sun, 25 Aug 2024 15:47:53 +0200 Subject: [PATCH 08/39] Change the model structure between projects and metais_projects --- app/controllers/metais/projects_controller.rb | 16 +-- app/controllers/phase_revision_controller.rb | 1 - ...ink_metais_projects_and_evaluations_job.rb | 16 ++- app/models/combined_project.rb | 15 --- app/models/metais/project.rb | 10 +- app/models/project.rb | 5 +- app/views/metais/projects/index.html.erb | 32 ++--- app/views/metais/projects/show.html.erb | 58 ++++----- app/views/phase_revision/show.html.erb | 16 +-- app/views/projects/index.html.erb | 4 +- ...20240820193320_create_combined_projects.rb | 10 -- ...ate_join_table_projects_metais_projects.rb | 8 ++ db/schema.rb | 11 -- db/structure.sql | 113 ++++++------------ 14 files changed, 130 insertions(+), 185 deletions(-) delete mode 100644 app/models/combined_project.rb delete mode 100644 db/migrate/20240820193320_create_combined_projects.rb create mode 100644 db/migrate/20240825111806_create_join_table_projects_metais_projects.rb diff --git a/app/controllers/metais/projects_controller.rb b/app/controllers/metais/projects_controller.rb index 46dcfc0..214015a 100644 --- a/app/controllers/metais/projects_controller.rb +++ b/app/controllers/metais/projects_controller.rb @@ -35,13 +35,14 @@ def index end if params[:has_evaluation].present? - @projects = @projects.joins(:combined_projects) - - if params[:has_evaluation] == 'yes' - @projects = @projects.where.not(combined_projects: { evaluation_id: nil }) - elsif params[:has_evaluation] == 'no' - @projects = @projects.where(combined_projects: { evaluation_id: nil }) - end + @projects = Metais::Project.where(id: @projects.select { |project| + evaluations = project.evaluations + if params[:has_evaluation] == 'yes' + evaluations.exists? + else + evaluations.empty? + end + }.map(&:id)) end case params[:sort] @@ -67,7 +68,6 @@ def index def show @project = Metais::Project.find(params[:id]) - @combined_project = CombinedProject.find_by(metais_project_id: @project.id) @project_info = @project.get_project_origin_info @project_origins = @project.project_origins diff --git a/app/controllers/phase_revision_controller.rb b/app/controllers/phase_revision_controller.rb index c46be23..82a4b62 100644 --- a/app/controllers/phase_revision_controller.rb +++ b/app/controllers/phase_revision_controller.rb @@ -2,7 +2,6 @@ class PhaseRevisionController < ApplicationController def show @project = Project.find_by!(id: params[:project_id]) @phase_revision = PhaseRevision.find_published_revision(@project.id, params[:revision_type]) - @combined_project = CombinedProject.find_by(evaluation_id: @project.id) if @phase_revision @revision = @phase_revision.revision diff --git a/app/jobs/link_metais_projects_and_evaluations_job.rb b/app/jobs/link_metais_projects_and_evaluations_job.rb index a2035d4..0d46caa 100644 --- a/app/jobs/link_metais_projects_and_evaluations_job.rb +++ b/app/jobs/link_metais_projects_and_evaluations_job.rb @@ -2,15 +2,13 @@ class LinkMetaisProjectsAndEvaluationsJob < ApplicationJob queue_as :default def perform - Metais::Project.find_each do |metais_project| - code = metais_project.code + Project.find_each do |project| + code = project.metais_code + metais_project = Metais::Project.find_by(code: code) - evaluation = Project.find_by(metais_code: code) - - CombinedProject.create!( - evaluation: evaluation, - metais_project: metais_project - ) + if metais_project.present? + project.metais_projects << metais_project + end end end -end +end \ No newline at end of file diff --git a/app/models/combined_project.rb b/app/models/combined_project.rb deleted file mode 100644 index 40ce1c1..0000000 --- a/app/models/combined_project.rb +++ /dev/null @@ -1,15 +0,0 @@ -# == Schema Information -# -# Table name: combined_projects -# -# id :integer not null, primary key -# metais_project_id :integer not null -# evaluation_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# - -class CombinedProject < ApplicationRecord - belongs_to :metais_project, class_name: 'Metais::Project', foreign_key: 'metais_project_id' - belongs_to :evaluation, class_name: 'Project', foreign_key: 'evaluation_id', optional: true -end diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index 0f36fce..5396314 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -10,8 +10,16 @@ class Metais::Project < ApplicationRecord self.table_name = "metais.projects" - has_many :combined_projects, class_name: 'CombinedProject', foreign_key: 'metais_project_id' has_many :project_origins, :class_name => 'Metais::ProjectOrigin' + has_and_belongs_to_many :projects, class_name: 'Project', + join_table: 'public.projects_metais_projects', + foreign_key: 'metais_project_id', + association_foreign_key: 'project_id' + + def evaluations + ::Project.joins(:metais_projects) + .where('projects_metais_projects.metais_project_id = ?', self.id) + end def get_project_origin_info fields = %w[title status description guarantor project_manager start_date end_date diff --git a/app/models/project.rb b/app/models/project.rb index f2e58a6..2c9bb13 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -9,7 +9,10 @@ class Project < ApplicationRecord has_many :phases - has_many :combined_projects, class_name: 'CombinedProject', foreign_key: 'evaluation_id' + has_and_belongs_to_many :metais_projects, class_name: 'Metais::Project', + join_table: 'public.projects_metais_projects', + foreign_key: 'project_id', + association_foreign_key: 'metais_project_id' def has_published_phases? phases.any? { |phase| phase.published_revision.present? } diff --git a/app/views/metais/projects/index.html.erb b/app/views/metais/projects/index.html.erb index 698011d..902e845 100644 --- a/app/views/metais/projects/index.html.erb +++ b/app/views/metais/projects/index.html.erb @@ -159,22 +159,22 @@ <%= 'Hodnotenie?' %>
    diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb index 96e7181..8d48d7e 100644 --- a/app/views/metais/projects/show.html.erb +++ b/app/views/metais/projects/show.html.erb @@ -80,36 +80,40 @@ - <% if @combined_project&.evaluation&.phases.present? %> - <% @combined_project.evaluation.phases.each do |phase| %> - <% if phase.published_revision.present? %> -
    - <% if phase.phase_type.name == 'Prípravná fáza' %> -
    <%= link_to 'Hodnotenie prípravnej fázy', project_show_revision_type_path(phase.project.id, 'hodnotenie-pripravy')%>
    - <% else %> -
    <%= link_to 'Hodnotenie fázy produkt', project_show_revision_type_path(phase.project.id, 'hodnotenie-produkt')%>
    - <% end %> -
    -
    - <% if phase.published_revision.summary.present? %> -
    -
    -
    Zhrnutie hodnotenia Red Flags
    -

    <%= phase.published_revision.summary %>

    -
    -
    + <% if @project.evaluations.present? %> + <% @project.evaluations.each do |evaluation| %> + <% if evaluation.phases.present? %> + <% evaluation.phases.each do |phase| %> + <% if phase.published_revision.present? %> +
    + <% if phase.phase_type.name == 'Prípravná fáza' %> +
    <%= link_to 'Hodnotenie prípravnej fázy', project_show_revision_type_path(phase.project.id, 'hodnotenie-pripravy')%>
    + <% else %> +
    <%= link_to 'Hodnotenie fázy produkt', project_show_revision_type_path(phase.project.id, 'hodnotenie-produkt')%>
    <% end %> +
    +
    + <% if phase.published_revision.summary.present? %> +
    +
    +
    Zhrnutie hodnotenia Red Flags
    +

    <%= phase.published_revision.summary %>

    +
    +
    + <% end %> - <% if phase.published_revision.recommendation.present? %> -
    -
    -
    Stanovisko Slovensko.Digital
    -

    <%= phase.published_revision.recommendation %>

    -
    + <% if phase.published_revision.recommendation.present? %> +
    +
    +
    Stanovisko Slovensko.Digital
    +

    <%= phase.published_revision.recommendation %>

    +
    +
    + <% end %>
    - <% end %> -
    -
    +
    + <% end %> + <% end %> <% end %> <% end %> <% end %> diff --git a/app/views/phase_revision/show.html.erb b/app/views/phase_revision/show.html.erb index 1ad6ccf..13150eb 100644 --- a/app/views/phase_revision/show.html.erb +++ b/app/views/phase_revision/show.html.erb @@ -74,15 +74,15 @@
    - <% if @combined_project.present? %> -
    -
    -
    Viac o projekte
    - <% if @combined_project&.metais_project.present? %> - <%= link_to @combined_project.metais_project.get_project_origin_info.title, metais_project_path(@combined_project.metais_project) %> - <% end %> + <% if @project.metais_projects.present? %> + <% @project.metais_projects.each do |metais_project| %> +
    +
    +
    Viac o projekte
    + <%= link_to metais_project.get_project_origin_info.title, metais_project_path(metais_project) %> +
    -
    + <% end %> <% end %>
    diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 269dc61..f3c10d1 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -61,8 +61,8 @@
    <% if project.has_published_phases? %> <% latest_published_revision = project.phases.select { |phase| phase.published_revision.present? }&.max_by { |phase| phase.published_revision.published_at }&.published_revision %> - <% if project.combined_projects.present? %> - <%= link_to latest_published_revision&.title, metais_project_path(id: project.combined_projects.first.metais_project.id), class: 'table-title-item' %> + <% if project.metais_projects.present? %> + <%= link_to latest_published_revision&.title, metais_project_path(id: project.metais_projects.first.id), class: 'table-title-item' %> <% else %> <%= content_tag(:p, latest_published_revision&.title, class: "table-price") %> <% end %> diff --git a/db/migrate/20240820193320_create_combined_projects.rb b/db/migrate/20240820193320_create_combined_projects.rb deleted file mode 100644 index 1237ac7..0000000 --- a/db/migrate/20240820193320_create_combined_projects.rb +++ /dev/null @@ -1,10 +0,0 @@ -class CreateCombinedProjects < ActiveRecord::Migration[5.1] - def change - create_table :combined_projects do |t| - t.references :metais_project, null: false, index: true, foreign_key: { to_table: 'metais.projects' } - t.references :evaluation, null: true, index: true, foreign_key: { to_table: 'projects' } - - t.timestamps - end - end -end diff --git a/db/migrate/20240825111806_create_join_table_projects_metais_projects.rb b/db/migrate/20240825111806_create_join_table_projects_metais_projects.rb new file mode 100644 index 0000000..db4f8fb --- /dev/null +++ b/db/migrate/20240825111806_create_join_table_projects_metais_projects.rb @@ -0,0 +1,8 @@ +class CreateJoinTableProjectsMetaisProjects < ActiveRecord::Migration[5.1] + def change + create_table :projects_metais_projects, id: false do |t| + t.references :project, index: true, foreign_key: {to_table: 'projects'} + t.references :metais_project, index: true, foreign_key: {to_table: 'metais.projects'} + end + end +end diff --git a/db/schema.rb b/db/schema.rb index eb97423..ac2fea5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -15,15 +15,6 @@ # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - create_table "combined_projects", force: :cascade do |t| - t.bigint "metais_project_id", null: false - t.bigint "evaluation_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["evaluation_id"], name: "index_combined_projects_on_evaluation_id" - t.index ["metais_project_id"], name: "index_combined_projects_on_metais_project_id" - end - create_table "pages", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -127,8 +118,6 @@ t.index ["tags"], name: "index_revisions_on_tags", using: :gin end - add_foreign_key "combined_projects", "metais.projects", column: "metais_project_id" - add_foreign_key "combined_projects", "projects", column: "evaluation_id" add_foreign_key "pages", "phases" add_foreign_key "pages", "revisions", column: "latest_revision_id" add_foreign_key "pages", "revisions", column: "published_revision_id" diff --git a/db/structure.sql b/db/structure.sql index 348834f..8b4176f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -354,38 +354,6 @@ CREATE TABLE public.ar_internal_metadata ( ); --- --- Name: combined_projects; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.combined_projects ( - id bigint NOT NULL, - metais_project_id bigint NOT NULL, - evaluation_id bigint, - created_at timestamp without time zone NOT NULL, - updated_at timestamp without time zone NOT NULL -); - - --- --- Name: combined_projects_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.combined_projects_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: combined_projects_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.combined_projects_id_seq OWNED BY public.combined_projects.id; - - -- -- Name: pages; Type: TABLE; Schema: public; Owner: - -- @@ -658,6 +626,16 @@ CREATE SEQUENCE public.projects_id_seq ALTER SEQUENCE public.projects_id_seq OWNED BY public.projects.id; +-- +-- Name: projects_metais_projects; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projects_metais_projects ( + project_id bigint, + metais_project_id bigint +); + + -- -- Name: que_jobs; Type: TABLE; Schema: public; Owner: - -- @@ -838,13 +816,6 @@ ALTER TABLE ONLY metais.projects ALTER COLUMN id SET DEFAULT nextval('metais.pro ALTER TABLE ONLY metais.supplier_types ALTER COLUMN id SET DEFAULT nextval('metais.supplier_types_id_seq'::regclass); --- --- Name: combined_projects id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.combined_projects ALTER COLUMN id SET DEFAULT nextval('public.combined_projects_id_seq'::regclass); - - -- -- Name: pages id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1002,14 +973,6 @@ ALTER TABLE ONLY public.ar_internal_metadata ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); --- --- Name: combined_projects combined_projects_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.combined_projects - ADD CONSTRAINT combined_projects_pkey PRIMARY KEY (id); - - -- -- Name: pages pages_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1190,20 +1153,6 @@ CREATE INDEX "index_metais.project_suppliers_on_project_origin_id" ON metais.pro CREATE INDEX "index_metais.project_suppliers_on_supplier_type_id" ON metais.project_suppliers USING btree (supplier_type_id); --- --- Name: index_combined_projects_on_evaluation_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_combined_projects_on_evaluation_id ON public.combined_projects USING btree (evaluation_id); - - --- --- Name: index_combined_projects_on_metais_project_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_combined_projects_on_metais_project_id ON public.combined_projects USING btree (metais_project_id); - - -- -- Name: index_pages_on_phase_id; Type: INDEX; Schema: public; Owner: - -- @@ -1274,6 +1223,20 @@ CREATE INDEX index_projects2_on_evaluation_id ON public.projects2 USING btree (e CREATE INDEX index_projects2_on_metais_project_id ON public.projects2 USING btree (metais_project_id); +-- +-- Name: index_projects_metais_projects_on_metais_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_projects_metais_projects_on_metais_project_id ON public.projects_metais_projects USING btree (metais_project_id); + + +-- +-- Name: index_projects_metais_projects_on_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_projects_metais_projects_on_project_id ON public.projects_metais_projects USING btree (project_id); + + -- -- Name: index_revisions_on_page_id; Type: INDEX; Schema: public; Owner: - -- @@ -1391,14 +1354,6 @@ ALTER TABLE ONLY metais.project_events ADD CONSTRAINT fk_rails_e243232959 FOREIGN KEY (project_origin_id) REFERENCES metais.project_origins(id); --- --- Name: combined_projects fk_rails_0d08d8ab66; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.combined_projects - ADD CONSTRAINT fk_rails_0d08d8ab66 FOREIGN KEY (evaluation_id) REFERENCES public.projects(id); - - -- -- Name: projects2 fk_rails_167b3161dd; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1408,11 +1363,11 @@ ALTER TABLE ONLY public.projects2 -- --- Name: combined_projects fk_rails_18e52e7275; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: projects_metais_projects fk_rails_747f5e41f3; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.combined_projects - ADD CONSTRAINT fk_rails_18e52e7275 FOREIGN KEY (metais_project_id) REFERENCES metais.projects(id); +ALTER TABLE ONLY public.projects_metais_projects + ADD CONSTRAINT fk_rails_747f5e41f3 FOREIGN KEY (project_id) REFERENCES public.projects(id); -- @@ -1423,6 +1378,14 @@ ALTER TABLE ONLY public.phases ADD CONSTRAINT fk_rails_7768cfc98c FOREIGN KEY (phase_type_id) REFERENCES public.phase_types(id); +-- +-- Name: projects_metais_projects fk_rails_7d4b01aec6; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects_metais_projects + ADD CONSTRAINT fk_rails_7d4b01aec6 FOREIGN KEY (metais_project_id) REFERENCES metais.projects(id); + + -- -- Name: phase_revision_ratings fk_rails_89f94d4743; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1557,11 +1520,9 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240801081124'), ('20240820080110'), ('20240820080303'), -('20240820193320'), ('20240821210736'), ('20240821212620'), ('20240822112026'), ('20240822143116'), -('20240823082322'); - - +('20240823082322'), +('20240825111806'); \ No newline at end of file From b7ee240330ad77d25cec85d1acd221aa91d9d5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Sun, 25 Aug 2024 15:53:17 +0200 Subject: [PATCH 09/39] Added prefix file for metais module --- app/models/metais.rb | 5 +++++ app/models/metais/origin_type.rb | 1 - app/models/metais/project.rb | 2 -- app/models/metais/project_document.rb | 2 -- app/models/metais/project_event.rb | 2 -- app/models/metais/project_event_type.rb | 2 -- app/models/metais/project_link.rb | 2 -- app/models/metais/project_origin.rb | 2 -- app/models/metais/project_supplier.rb | 2 -- app/models/metais/supplier_type.rb | 1 - 10 files changed, 5 insertions(+), 16 deletions(-) create mode 100644 app/models/metais.rb diff --git a/app/models/metais.rb b/app/models/metais.rb new file mode 100644 index 0000000..ccdd2d7 --- /dev/null +++ b/app/models/metais.rb @@ -0,0 +1,5 @@ +module Metais + def self.table_name_prefix + "metais." + end +end \ No newline at end of file diff --git a/app/models/metais/origin_type.rb b/app/models/metais/origin_type.rb index 0d8332f..bb910ac 100644 --- a/app/models/metais/origin_type.rb +++ b/app/models/metais/origin_type.rb @@ -8,5 +8,4 @@ # updated_at :datetime not null class Metais::OriginType < ApplicationRecord - self.table_name = "metais.origin_types" end diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index 5396314..0479419 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -8,8 +8,6 @@ # updated_at :datetime not null class Metais::Project < ApplicationRecord - self.table_name = "metais.projects" - has_many :project_origins, :class_name => 'Metais::ProjectOrigin' has_and_belongs_to_many :projects, class_name: 'Project', join_table: 'public.projects_metais_projects', diff --git a/app/models/metais/project_document.rb b/app/models/metais/project_document.rb index 2f3c360..effbf3e 100644 --- a/app/models/metais/project_document.rb +++ b/app/models/metais/project_document.rb @@ -9,8 +9,6 @@ # updated_at :datetime not null class Metais::ProjectDocument < ApplicationRecord - self.table_name = "metais.project_documents" - belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' belongs_to :origin_type, class_name: 'Metais::OriginType' end diff --git a/app/models/metais/project_event.rb b/app/models/metais/project_event.rb index acc9f8d..1eb3311 100644 --- a/app/models/metais/project_event.rb +++ b/app/models/metais/project_event.rb @@ -10,8 +10,6 @@ # updated_at :datetime not null class Metais::ProjectEvent < ApplicationRecord - self.table_name = "metais.project_events" - belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' belongs_to :origin_type, class_name: 'Metais::OriginType' belongs_to :event_type, class_name: 'Metais::ProjectEventType' diff --git a/app/models/metais/project_event_type.rb b/app/models/metais/project_event_type.rb index 27af73b..ea4a15b 100644 --- a/app/models/metais/project_event_type.rb +++ b/app/models/metais/project_event_type.rb @@ -1,5 +1,3 @@ class Metais::ProjectEventType < ApplicationRecord - self.table_name = "metais.project_event_types" - has_many :project_events, class_name: 'Metais::ProjectEvent' end diff --git a/app/models/metais/project_link.rb b/app/models/metais/project_link.rb index abe3ed7..771a260 100644 --- a/app/models/metais/project_link.rb +++ b/app/models/metais/project_link.rb @@ -9,8 +9,6 @@ # updated_at :datetime not null class Metais::ProjectLink < ApplicationRecord - self.table_name = "metais.project_links" - belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' belongs_to :origin_type, class_name: 'Metais::OriginType' end diff --git a/app/models/metais/project_origin.rb b/app/models/metais/project_origin.rb index 82fd51f..fff45f3 100644 --- a/app/models/metais/project_origin.rb +++ b/app/models/metais/project_origin.rb @@ -39,8 +39,6 @@ # fk_metais_project_origins_origin_type_id (origin_type_id => metais.origin_types.id) class Metais::ProjectOrigin < ApplicationRecord - self.table_name = "metais.project_origins" - belongs_to :project, :class_name => 'Metais::Project' belongs_to :origin_type, :class_name => 'Metais::OriginType' diff --git a/app/models/metais/project_supplier.rb b/app/models/metais/project_supplier.rb index 398eab2..5742e89 100644 --- a/app/models/metais/project_supplier.rb +++ b/app/models/metais/project_supplier.rb @@ -9,8 +9,6 @@ # updated_at :datetime not null class Metais::ProjectSupplier < ApplicationRecord - self.table_name = "metais.project_suppliers" - belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' belongs_to :origin_type, class_name: 'Metais::OriginType' belongs_to :supplier_type, class_name: 'Metais::SupplierType' diff --git a/app/models/metais/supplier_type.rb b/app/models/metais/supplier_type.rb index b2e6d62..cda5380 100644 --- a/app/models/metais/supplier_type.rb +++ b/app/models/metais/supplier_type.rb @@ -8,5 +8,4 @@ # updated_at :datetime not null class Metais::SupplierType < ApplicationRecord - self.table_name = "metais.supplier_types" end From 8ac77512722c877ed084313df6f57d68dd9faa01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Sun, 25 Aug 2024 15:58:53 +0200 Subject: [PATCH 10/39] Metais in separate rake task --- lib/tasks/metais.rake | 9 +++++++++ lib/tasks/redflags.rake | 10 ---------- 2 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 lib/tasks/metais.rake diff --git a/lib/tasks/metais.rake b/lib/tasks/metais.rake new file mode 100644 index 0000000..99d456b --- /dev/null +++ b/lib/tasks/metais.rake @@ -0,0 +1,9 @@ +namespace :metais do + task daily_sync: :environment do + Metais::DailySyncProjectsJob.perform_later + end + + task daily_sync_evaluations: :environment do + LinkMetaisProjectsAndEvaluationsJob.perform_later + end +end \ No newline at end of file diff --git a/lib/tasks/redflags.rake b/lib/tasks/redflags.rake index 26198bc..a70a345 100644 --- a/lib/tasks/redflags.rake +++ b/lib/tasks/redflags.rake @@ -3,13 +3,3 @@ namespace :redflags do SyncCategoryTopicsJob.perform_later(ENV.fetch('REDFLAGS_CATEGORY_SLUG')) end end - -namespace :metais do - task daily_sync: :environment do - Metais::DailySyncProjectsJob.perform_later - end - - task daily_sync_evaluations: :environment do - LinkMetaisProjectsAndEvaluationsJob.perform_later - end -end From 0b18cb7c62d1a4213f1229af76f8e10155a9fa7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Mon, 26 Aug 2024 15:55:20 +0200 Subject: [PATCH 11/39] multiple fixes --- .ruby-version | 2 +- Dockerfile | 2 +- Gemfile | 2 +- app/assets/stylesheets/application.scss | 2 +- app/assets/stylesheets/projects.scss | 6 ++--- app/controllers/static_controller.rb | 4 ++-- app/models/datahub_record.rb | 2 +- app/models/metais/project.rb | 8 ++++--- app/models/project.rb | 20 ++++------------ app/views/metais/projects/index.html.erb | 29 ++++++++++++------------ app/views/projects/index.html.erb | 11 ++++----- config/database.yml | 4 ++-- lib/tasks/metais.rake | 10 +++++++- 13 files changed, 49 insertions(+), 53 deletions(-) diff --git a/.ruby-version b/.ruby-version index 37c2961..6a81b4c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.2 +2.7.8 diff --git a/Dockerfile b/Dockerfile index eca10a4..7070182 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.7.2 +FROM ruby:2.7.8 # Install packages RUN apt-get update && apt-get install -y build-essential nodejs libpq-dev diff --git a/Gemfile b/Gemfile index d1e5e00..fb0abbc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '2.7.2' +ruby '2.7.8' git_source(:github) do |repo_name| repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index dcb5fd9..1d98e9c 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -24,7 +24,7 @@ h1, h2, h3, h4, h5, h6 { .table-title-item, .table-price { font-family: $headings-font-family; - text-decoration: underline; + font-size: large; color: $black; } diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss index cbfbf11..d50998c 100644 --- a/app/assets/stylesheets/projects.scss +++ b/app/assets/stylesheets/projects.scss @@ -109,12 +109,12 @@ img.emoji { } .accordion:after { - color: $blue; + color: $black; font-size: medium; text-decoration: underline; - content: "Schovať"; + content: "Schovať hodnotené kritériá"; } .accordion.collapsed:after { - content: "Zobraziť"; + content: "Zobraziť hodnotené kritériá"; } diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 4cd8d44..b540e4f 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -2,8 +2,8 @@ class StaticController < ApplicationController def index top_project_revisions = PhaseRevision.where(published: true).joins(:revision).limit(5) - @good_projects = top_project_revisions.where('phase_revisions.redflags_count = ?', 0).order('phase_revisions.total_score::float / phase_revisions.maximum_score DESC') - @bad_projects = top_project_revisions.order('phase_revisions.redflags_count DESC, phase_revisions.total_score::float / phase_revisions.maximum_score ASC') + @good_projects = top_project_revisions.where('phase_revisions.redflags_count = ? AND phase_revisions.maximum_score > 0', 0).order('phase_revisions.total_score::float / phase_revisions.maximum_score DESC') + @bad_projects = top_project_revisions.where('phase_revisions.maximum_score > 0').order('phase_revisions.redflags_count DESC, phase_revisions.total_score::float / phase_revisions.maximum_score ASC') end def about diff --git a/app/models/datahub_record.rb b/app/models/datahub_record.rb index 07d5642..d96032a 100644 --- a/app/models/datahub_record.rb +++ b/app/models/datahub_record.rb @@ -1,4 +1,4 @@ class DatahubRecord < ActiveRecord::Base self.abstract_class = true establish_connection :"#{Rails.env}_datahub" -end \ No newline at end of file +end diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index 0479419..7bd6821 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -15,12 +15,14 @@ class Metais::Project < ApplicationRecord association_foreign_key: 'project_id' def evaluations - ::Project.joins(:metais_projects) - .where('projects_metais_projects.metais_project_id = ?', self.id) + ::Project.joins(''' + INNER JOIN "public"."projects_metais_projects" ON "public"."projects_metais_projects"."project_id" = "projects"."id" + INNER JOIN "metais"."projects" AS "metais_projects" ON "metais_projects"."id" = "public"."projects_metais_projects"."metais_project_id" + ''').where('projects_metais_projects.metais_project_id = ?', self.id) end def get_project_origin_info - fields = %w[title status description guarantor project_manager start_date end_date + fields = %w[title status phase description guarantor project_manager start_date end_date finance_source investment operation approved_investment approved_operation supplier supplier_cin targets_text events_text documents_text links_text] diff --git a/app/models/project.rb b/app/models/project.rb index 2c9bb13..7654dd5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -21,11 +21,6 @@ def has_published_phases? def self.filtered_projects(selected_tag, sort_param) case sort_param - when 'newest' - projects = Project.joins(phases: :published_revision) - .select('projects.*, MAX(phase_revisions.published_at) AS newest_published_at') - .group('projects.id') - .order('newest_published_at DESC') when 'oldest' projects = Project.joins(phases: :published_revision) .select('projects.*, MIN(phase_revisions.published_at) AS oldest_published_at') @@ -70,18 +65,11 @@ def self.filtered_projects(selected_tag, sort_param) phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 end else + # when 'newest' projects = Project.joins(phases: :published_revision) - .distinct - - if ProjectsHelper::ALLOWED_TAGS.keys.include?(selected_tag) - projects = Project.joins(phases: :published_revision) - .where(phase_revisions: { tags: selected_tag }) - end - - projects = projects.sort_by do |project| - max_rating = project.phases.map { |p| p.published_revision&.aggregated_rating }.compact.max - max_rating - end + .select('projects.*, MAX(phase_revisions.published_at) AS newest_published_at') + .group('projects.id') + .order('newest_published_at DESC') end projects diff --git a/app/views/metais/projects/index.html.erb b/app/views/metais/projects/index.html.erb index 902e845..e8742c0 100644 --- a/app/views/metais/projects/index.html.erb +++ b/app/views/metais/projects/index.html.erb @@ -185,7 +185,7 @@ - + @@ -195,19 +195,18 @@ - + <%= form_with(url: admin_metais_project_project_origin_add_link_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> + + + <%= hidden_field_tag 'link[origin_type]', 'Human' %> + + <% end %> + + +
    Projekt Cena
    - <%= link_to project.project_origins.first.title, metais_project_path(project), class: 'table-title-item hover:text-primary' %> -
    -

    Dátum poslednej zmeny:

    - <%= - content_tag(:p) do - if project.updated_at.present? - content_tag(:small, l(project.updated_at.in_time_zone('Europe/Bratislava'), format: "%d.%m.%Y"), class: "font-italic") - else - content_tag(:small, "Neuvedená", style: "color: grey;", class: "font-italic tag-tooltip", title: "Dátum poslednej zmeny projektu nie je známy") - end - end - %> -
    + <%= link_to project.project_origins.first.title, metais_project_path(project), class: 'table-title-item mb-4 hover:text-primary' %> +
    +

    Dátum poslednej zmeny: + <%= + if project.updated_at.present? + content_tag(:small, l(project.updated_at.in_time_zone('Europe/Bratislava'), format: "%d.%m.%Y"), class: "font-italic") + else + content_tag(:small, "Neuvedená", style: "color: grey;", class: "font-italic tag-tooltip", title: "Dátum poslednej zmeny projektu nie je známy") + end + %> +

    +
    <%= @@ -276,7 +275,7 @@
    + <%= if project.get_project_origin_info.approved_investment.present? && project.get_project_origin_info.approved_investment > 0 number_to_currency(project.get_project_origin_info.approved_investment, unit: "€", separator: ",", delimiter: " ", format: "%n %u") diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index f3c10d1..e708922 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -13,12 +13,11 @@
    <% sort_options = { - '' => 'Odporúčané', 'alpha' => 'Abecedne', 'date' => 'Podľa dátumu', 'price' => 'Podľa ceny'} %> - <% current_sort = params[:sort] || '' %> + <% current_sort = params[:sort] || 'date' %> <% current_sort_label = sort_options[current_sort] %>
    -
    -
    Linky
    - - - - - - - - - - <% @all_links.each do |link| %> +
    +
    +
    +
    Linky
    +
    NázovLinkAkcia
    + - - - + + + - <% end %> - - <%= form_with(url: admin_metais_project_project_origin_add_link_path(project_id: @project.id, project_origin_id: @project_origin.id), method: :post, local: true) do |f| %> - - - <%= hidden_field_tag 'link[origin_type]', 'Human' %> - + + + <% @all_links.each do |link| %> + + + + + <% end %> - - -
    <%= link.name %><%= link_to link.value, link.value %> - <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_link_path(project_id: @project.id, project_origin_id: @project_origin.id, link_id: link.id), method: :delete, data: { confirm: 'Naozaj chcete item odstrániť?' } %> - NázovLinkAkcia
    <%= f.text_field 'link[name]', class: "w-100 form-control" %><%= f.text_field 'link[value]', class: "w-100 form-control" %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    + <%= image_tag origin_type_logo(link.origin_type), style: "width: 16px; height: 16px;" %> + <%= link.name %> + <%= link_to link.value, link.value %> + <%= link_to 'Odstrániť', admin_metais_project_project_origin_remove_link_path(project_id: @project.id, project_origin_id: @project_origin.id, link_id: link.id), method: :delete, data: { confirm: 'Naozaj chcete item odstrániť?' } %> +
    +
    <%= f.text_field 'link[name]', class: "w-100 form-control" %><%= f.text_field 'link[value]', class: "w-100 form-control" %><%= f.submit 'Pridať', class: 'btn btn-primary' %>
    +
    \ No newline at end of file diff --git a/app/views/admin/metais/projects/show.html.erb b/app/views/admin/metais/projects/show.html.erb deleted file mode 100644 index 597dc8f..0000000 --- a/app/views/admin/metais/projects/show.html.erb +++ /dev/null @@ -1,66 +0,0 @@ -

    Admin

    - -
    -
    -
    Metais Project Origins
    -
    -
    - -

    - <%= @project.get_project_origin_info.title %> -

    - -
    - <% human_origin = @project.project_origins.find_by(origin_type: Metais::OriginType.find_by(name: 'Human')) %> - <% if human_origin %> - <%= link_to 'Edit Human Origin', edit_admin_metais_project_project_origin_path(@project, human_origin), class: 'btn btn-primary' %> - <% else %> - <%= button_to 'Create Human Origin', create_human_origin_admin_metais_project_path(@project), method: :post, class: 'btn btn-primary' %> - <% end %> -
    -
    - <%= button_to 'Perform AI Extraction', run_ai_extraction_admin_metais_project_path(@project), method: :post, class: 'btn btn-secondary', data: { confirm: 'Naozaj chcete dať dokument spracovať AI? Ak ste už pred chvíľou raz spustili spracovanie, počkajte na výsledok, nespúšťajte ďalší pokus.' } %> -
    - - - - - - - - - - - - - - - <% @project.project_origins.each do |project_origin| %> - - - - - - - - <% if project_origin.origin_type.name == 'Human'%> - - <% else %> - - <% end %> - <% end %> - -
    IDTypDokumentyDodávateliaAktivityLinkyAkcie
    <%= project_origin.id %> - <%= link_to project_origin.origin_type.name, metais_project_path(project_origin.project) %> - - <%= project_origin.documents.count %> - - <%= project_origin.suppliers.count %> - - <%= project_origin.events.count %> - - <%= project_origin.links.count %> - - <%= link_to 'Edit', edit_admin_metais_project_project_origin_path(project_origin.project, project_origin) %> - -
    \ No newline at end of file diff --git a/app/views/metais/projects/index.html.erb b/app/views/metais/projects/index.html.erb index ff48bc9..d6cdc5f 100644 --- a/app/views/metais/projects/index.html.erb +++ b/app/views/metais/projects/index.html.erb @@ -6,10 +6,6 @@ <%= form_with url: metais_projects_path, method: :get, class: 'form my-2 my-lg-0', local: true, id: "form" do |form| %> <%= hidden_field_tag :sort, params[:sort] %> <%= hidden_field_tag :sort_direction, params[:sort_direction] %> - <%= hidden_field_tag :status, params[:status] %> - <%= hidden_field_tag :guarantor, params[:guarantor] %> - <%= hidden_field_tag :has_evaluation, params[:has_evaluation] %> -
    @@ -52,7 +48,7 @@
    diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb index 44c07d6..15f4ee7 100644 --- a/app/views/metais/projects/show.html.erb +++ b/app/views/metais/projects/show.html.erb @@ -1,9 +1,8 @@
    -

    <%= @project_info.title %>

    -

    <%= @project_info.description %>

    +
    <%= simple_format(@project_info.description) %>
    @@ -283,9 +282,17 @@
    Dodávateľ projektu
    - <% if @project_info.supplier.present? %> -

    Dodávateľ: <%= @project_info.supplier %> <%= image_tag origin_type_logo(@project_info.supplier.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %>

    - <% end %> +

    Dodávateľ: + <% if @project_info.supplier.present? && @project_info.supplier_cin.present? %> + + <%= @project_info.supplier %> + + <%= image_tag origin_type_logo(@project_info.supplier&.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% else %> + Neznámy + <% end %> +

    + <% if @all_suppliers.any? %>
      <% @all_suppliers.each do |supplier| %> @@ -301,7 +308,12 @@ end %>
    • <%= image_tag origin_type_logo(supplier.origin_type), style: "width: 12px; height: 12px;" if supplier.origin_type.present? %> - <%= link_to supplier.name, supplier.value %> + + <% if supplier.value.present? && supplier.value =~ /\Ahttps?:\/\/[\S]+\z/ %> + <%= link_to supplier.name, supplier.value %> + <% else %> + <%= supplier.name %> + <% end %>
    • <% end %>
    diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 8f97d3b..35498fc 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -3,47 +3,47 @@

    Chýba Vám tu nejaký projekt? Nezdá sa Vám hodnotenie? Toto hodnotenie je možné <%= link_to 'dopĺňať a upravovať', contribute_path %>.

    - <%= form_with url: projects_path, method: :get, class: 'form my-2 my-lg-0', local: true do |form| %> -
    -
    -
    - <%= text_field_tag :title, params[:title], class: 'form-control' %> + <%= form_with url: projects_path, method: :get, class: 'form my-2 my-lg-0', id: "form", local: true do |form| %> + <%= hidden_field_tag :sort, params[:sort] %> + <%= hidden_field_tag :sort_direction, params[:sort_direction] %> + +
    +
    +
    + <%= text_field_tag :title, params[:title], class: 'form-control mr-1', placeholder: "Meno projektu" %> + +
    -
    -
    -
    - <% sort_options = { - '' => 'Odporúčané', - 'alpha' => 'Abecedne', - 'date' => 'Podľa dátumu', - 'preparation' => 'Hodnotenie prípravy', - 'product' => 'Hodnotenie produktu'} %> - - <% current_sort = params[:sort] || '' %> - <% current_sort_label = sort_options[current_sort] %> - - <% end %>
    @@ -212,4 +212,45 @@ <% end %>
    +
    diff --git a/db/migrate/20240825115911_add_null_to_project_in_projects_metais_projects.rb b/db/migrate/20240825115911_add_null_to_project_in_projects_metais_projects.rb new file mode 100644 index 0000000..503839b --- /dev/null +++ b/db/migrate/20240825115911_add_null_to_project_in_projects_metais_projects.rb @@ -0,0 +1,5 @@ +class AddNullToProjectInProjectsMetaisProjects < ActiveRecord::Migration[5.1] + def change + change_column_null :projects_metais_projects, :project_id, true + end +end diff --git a/db/structure.sql b/db/structure.sql index 8b4176f..1598900 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -575,38 +575,6 @@ CREATE TABLE public.projects ( ); --- --- Name: projects2; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.projects2 ( - id bigint NOT NULL, - metais_project_id bigint NOT NULL, - evaluation_id bigint, - created_at timestamp without time zone NOT NULL, - updated_at timestamp without time zone NOT NULL -); - - --- --- Name: projects2_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.projects2_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: projects2_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.projects2_id_seq OWNED BY public.projects2.id; - - -- -- Name: projects_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -865,13 +833,6 @@ ALTER TABLE ONLY public.project_stages ALTER COLUMN id SET DEFAULT nextval('publ ALTER TABLE ONLY public.projects ALTER COLUMN id SET DEFAULT nextval('public.projects_id_seq'::regclass); --- --- Name: projects2 id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.projects2 ALTER COLUMN id SET DEFAULT nextval('public.projects2_id_seq'::regclass); - - -- -- Name: que_jobs job_id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1020,15 +981,7 @@ ALTER TABLE ONLY public.phases ALTER TABLE ONLY public.project_stages ADD CONSTRAINT project_stages_pkey PRIMARY KEY (id); - --- --- Name: projects2 projects2_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.projects2 - ADD CONSTRAINT projects2_pkey PRIMARY KEY (id); - - + -- -- Name: projects projects_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1209,20 +1162,6 @@ CREATE INDEX index_phases_on_phase_type_id ON public.phases USING btree (phase_t CREATE INDEX index_phases_on_project_id ON public.phases USING btree (project_id); --- --- Name: index_projects2_on_evaluation_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_projects2_on_evaluation_id ON public.projects2 USING btree (evaluation_id); - - --- --- Name: index_projects2_on_metais_project_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_projects2_on_metais_project_id ON public.projects2 USING btree (metais_project_id); - - -- -- Name: index_projects_metais_projects_on_metais_project_id; Type: INDEX; Schema: public; Owner: - -- @@ -1354,14 +1293,6 @@ ALTER TABLE ONLY metais.project_events ADD CONSTRAINT fk_rails_e243232959 FOREIGN KEY (project_origin_id) REFERENCES metais.project_origins(id); --- --- Name: projects2 fk_rails_167b3161dd; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.projects2 - ADD CONSTRAINT fk_rails_167b3161dd FOREIGN KEY (evaluation_id) REFERENCES public.projects(id); - - -- -- Name: projects_metais_projects fk_rails_747f5e41f3; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1525,4 +1456,5 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240822112026'), ('20240822143116'), ('20240823082322'), -('20240825111806'); \ No newline at end of file +('20240825111806'), +('20240825115911'); From 747defa845bdb8f767efc97d2bc414f0448448db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Wed, 25 Sep 2024 14:29:30 +0200 Subject: [PATCH 17/39] Ordering of project documents, js errors fixed --- app/assets/javascripts/projects.js | 10 +++--- .../metais/project_origins_controller.rb | 16 ++++++++++ app/controllers/metais/projects_controller.rb | 3 +- .../metais/project_origins/edit.html.erb | 31 ++++++++++++++++--- app/views/metais/projects/show.html.erb | 12 ++++--- config/routes.rb | 1 + ...648_add_group_order_to_metais_documents.rb | 5 +++ db/structure.sql | 6 ++-- 8 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20240924210648_add_group_order_to_metais_documents.rb diff --git a/app/assets/javascripts/projects.js b/app/assets/javascripts/projects.js index 62a42ea..9a8b634 100644 --- a/app/assets/javascripts/projects.js +++ b/app/assets/javascripts/projects.js @@ -15,12 +15,12 @@ document.addEventListener("turbolinks:before-render", function() { }); document.addEventListener('DOMContentLoaded', function() { - let sortDirectionField = document.getElementById('sort_direction'); - let sortButton = document.getElementById('ascdesctoggle'); - let upIcon = document.getElementById('up-icon'); - let downIcon = document.getElementById('down-icon'); + var sortDirectionField = document.getElementById('sort_direction'); + var sortButton = document.getElementById('ascdesctoggle'); + var upIcon = document.getElementById('up-icon'); + var downIcon = document.getElementById('down-icon'); - let toggleState = sortDirectionField.value || 'desc'; + var toggleState = sortDirectionField.value || 'desc'; if (toggleState === 'desc') { upIcon.style.fill = 'rgb(100, 100, 100, 0.4)'; diff --git a/app/controllers/admin/metais/project_origins_controller.rb b/app/controllers/admin/metais/project_origins_controller.rb index 6fb7924..b5f8aff 100644 --- a/app/controllers/admin/metais/project_origins_controller.rb +++ b/app/controllers/admin/metais/project_origins_controller.rb @@ -41,6 +41,9 @@ def edit @project_origins.each do |project_origin| @all_documents.concat(project_origin.documents) end + + @grouped_documents = @all_documents.group_by(&:description) + @grouped_documents = @grouped_documents.sort_by { |description, docs| docs.first.group_order || Float::INFINITY } @project_origin = Metais::ProjectOrigin.includes(:documents, :suppliers).find(@project_origin.id) end @@ -184,6 +187,19 @@ def remove_document @project_document.destroy redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Dokument bol úspešne odstránený.' end + + def update_group_order + @project = Metais::Project.find(params[:project_id]) + @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) + + description = params[:description] + group_order = params[:group_order].to_i + + Metais::ProjectDocument.where(description: description).update_all(group_order: group_order) + + flash[:notice] = "Group order updated successfully." + redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id) + end private diff --git a/app/controllers/metais/projects_controller.rb b/app/controllers/metais/projects_controller.rb index ac99f8c..269e6b6 100644 --- a/app/controllers/metais/projects_controller.rb +++ b/app/controllers/metais/projects_controller.rb @@ -36,7 +36,8 @@ def show end @all_documents = @project_origins.flat_map(&:documents) - @documents_grouped_by_description = @all_documents.group_by(&:description) + @grouped_documents = @all_documents.group_by(&:description) + @grouped_documents = @grouped_documents.sort_by { |description, docs| docs.first.group_order || Float::INFINITY } @project_origin = @project.project_origins.first @project_origin = Metais::ProjectOrigin.includes(:documents, :suppliers).find(@project_origin.id) diff --git a/app/views/admin/metais/project_origins/edit.html.erb b/app/views/admin/metais/project_origins/edit.html.erb index a195c17..41d967d 100644 --- a/app/views/admin/metais/project_origins/edit.html.erb +++ b/app/views/admin/metais/project_origins/edit.html.erb @@ -178,13 +178,13 @@ document.getElementById('supplier-input').value = ''; }); - const form = document.querySelector('form'); + var form = document.querySelector('form'); form.addEventListener('submit', function(event) { - const supplier = document.getElementById('supplier-input').value.trim(); - const supplierCin = document.getElementById('supplier-cin-input').value.trim(); + var supplier = document.getElementById('supplier-input').value.trim(); + var supplierCin = document.getElementById('supplier-cin-input').value.trim(); - const cinPattern = /^\d{6,8}$/; + var cinPattern = /^\d{6,8}$/; if (!cinPattern.test(supplierCin) || !supplier) { event.preventDefault(); @@ -325,7 +325,28 @@
    Dokumenty
    - +
    + + + + + + + + + <% @grouped_documents.each do |description, documents| %> + <%= form_with(url: admin_metais_project_project_origin_update_group_order_path(project_id: @project.id, project_origin_id: @project_origin.id, description: description), method: :patch, local: true) do |f| %> + + + + + + <% end %> + <% end %> + +
    Názov skupinyPrioritaAkcia
    <%= description %>
    <%= f.number_field 'group_order', value: documents.first.group_order, class: "form-control", min: 1 %><%= f.submit 'Aktualizovať poradie', class: 'btn btn-primary' %>
    + + diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb index 15f4ee7..74aff8f 100644 --- a/app/views/metais/projects/show.html.erb +++ b/app/views/metais/projects/show.html.erb @@ -322,15 +322,17 @@ <% end %> - <% if @project_info.documents_text.present? || @documents_grouped_by_description.any? %> + <% if @project_info.documents_text.present? || @grouped_documents.any? %>
    Dokumenty
    + <% if @project_info.documents_text.present? %>

    <%= @project_info.documents_text %>

    <% end %> - <% if @documents_grouped_by_description.any? %> - <% @documents_grouped_by_description.each do |description, documents| %> + + <% if @grouped_documents.any? %> + <% @grouped_documents.each do |description, documents| %>

    <%= description %>

      <% documents.each do |doc| %> @@ -342,10 +344,12 @@
    <% end %> <% end %> + +
    <% end %> - + <% if @project_info.links_text.present? || @all_links.any? %>
    diff --git a/config/routes.rb b/config/routes.rb index 9a388b6..402e3c4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,6 +43,7 @@ post 'add_supplier', to: 'project_origins#add_supplier', as: 'add_supplier' post 'add_link', to: 'project_origins#add_link', as: 'add_link' post 'add_document', to: 'project_origins#add_document', as: 'add_document' + patch 'update_group_order', to: 'project_origins#update_group_order', as: 'update_group_order' end end end diff --git a/db/migrate/20240924210648_add_group_order_to_metais_documents.rb b/db/migrate/20240924210648_add_group_order_to_metais_documents.rb new file mode 100644 index 0000000..cc645fc --- /dev/null +++ b/db/migrate/20240924210648_add_group_order_to_metais_documents.rb @@ -0,0 +1,5 @@ +class AddGroupOrderToMetaisDocuments < ActiveRecord::Migration[5.1] + def change + add_column :'metais.project_documents', :group_order, :integer + end +end diff --git a/db/structure.sql b/db/structure.sql index 1598900..ffd6bdc 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -65,7 +65,8 @@ CREATE TABLE metais.project_documents ( updated_at timestamp without time zone NOT NULL, filename character varying, uuid character varying, - description character varying + description character varying, + group_order integer ); @@ -1457,4 +1458,5 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240822143116'), ('20240823082322'), ('20240825111806'), -('20240825115911'); +('20240825115911'), +('20240924210648'); From bc0503972414950cc0a8f3303cb94c5d30a055ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Thu, 26 Sep 2024 09:06:59 +0200 Subject: [PATCH 18/39] Modified the CI workflow to create datahub_test db --- .github/workflows/slovensko_digital_ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/slovensko_digital_ci.yml b/.github/workflows/slovensko_digital_ci.yml index db9dc72..2674d92 100644 --- a/.github/workflows/slovensko_digital_ci.yml +++ b/.github/workflows/slovensko_digital_ci.yml @@ -29,6 +29,7 @@ jobs: bundler-cache: true - run: bundle exec rails db:create db:structure:load --trace + - run: RAILS_ENV=test_datahub bundle exec rails db:create - run: bundle exec rspec gitlab-push: From 0ace20a7331728671668ca4171ebb932e685bcc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Mon, 30 Sep 2024 12:37:56 +0200 Subject: [PATCH 19/39] fixed failing rspec tests --- app/controllers/admin/pages_controller.rb | 1 + app/jobs/metais/sync_project_job.rb | 3 +- app/jobs/metais/sync_project_suppliers_job.rb | 58 ++++++---- app/views/admin/pages/preview.html.erb | 1 - app/views/metais/projects/index.html.erb | 5 +- .../metais/daily_sync_projects_job_spec.rb | 41 +++++-- .../metais/sync_project_events_job_spec.rb | 9 +- spec/jobs/metais/sync_project_job_spec.rb | 20 ++-- .../metais/sync_project_suppliers_job_spec.rb | 108 +++++++++++------- 9 files changed, 153 insertions(+), 93 deletions(-) diff --git a/app/controllers/admin/pages_controller.rb b/app/controllers/admin/pages_controller.rb index d6ae40c..8de3e6a 100644 --- a/app/controllers/admin/pages_controller.rb +++ b/app/controllers/admin/pages_controller.rb @@ -20,6 +20,7 @@ def preview @phase_revision = @phase.revisions.find_by(revision: @page.revisions.find_by(version: params['version'])) end @ratings_by_type = @phase_revision.ratings.index_by(&:rating_type) + @project = @phase.project else if params['version'] == 'latest' diff --git a/app/jobs/metais/sync_project_job.rb b/app/jobs/metais/sync_project_job.rb index 99bfe51..eb28e60 100644 --- a/app/jobs/metais/sync_project_job.rb +++ b/app/jobs/metais/sync_project_job.rb @@ -24,8 +24,7 @@ def perform(project, metais_project) project_origin.save! - # TODO uncomment later if needed but mind the request price - # Metais::ProjectDataExtractionJob.set(wait: 5.minutes).perform_later(metais_project.uuid) + Metais::ProjectDataExtractionJob.set(wait: 5.minutes).perform_later(metais_project.uuid) Metais::SyncProjectSuppliersJob.perform_later(project_origin, metais_project) Metais::SyncProjectDocumentsJob.perform_later(project_origin, metais_project) Metais::SyncProjectEventsJob.perform_later(project_origin, metais_project) diff --git a/app/jobs/metais/sync_project_suppliers_job.rb b/app/jobs/metais/sync_project_suppliers_job.rb index 6047c3b..d7d4fb5 100644 --- a/app/jobs/metais/sync_project_suppliers_job.rb +++ b/app/jobs/metais/sync_project_suppliers_job.rb @@ -4,9 +4,9 @@ class Metais::SyncProjectSuppliersJob < ApplicationJob queue_as :metais - ORIGIN_TYPE = Metais::OriginType.find_by(name: 'MetaIS') - def perform(project_origin, metais_project) + origin_type = Metais::OriginType.find_by(name: 'MetaIS') + if metais_project.latest_version.link_nfp.present? supplier_type = Metais::SupplierType.find_by(name: "NFP") @@ -20,7 +20,7 @@ def perform(project_origin, metais_project) value: url, date: metais_project.latest_version.datum_nfp, project_origin: project_origin, - origin_type: ORIGIN_TYPE, + origin_type: origin_type, supplier_type: supplier_type) project_supplier.save! @@ -41,7 +41,7 @@ def perform(project_origin, metais_project) value: url, date: metais_project.latest_version.vyhlasenie_vo, project_origin: project_origin, - origin_type: ORIGIN_TYPE, + origin_type: origin_type, supplier_type: supplier_type) project_supplier.save! @@ -54,24 +54,13 @@ def perform(project_origin, metais_project) link = metais_project.latest_version.zmluva_o_dielo_crz - document = Nokogiri::HTML(open(link)) - - li_elements = document.css('li.py-2.border-top') - - li_elements.each_with_index do |li_element, i| - supplier_label = li_element.at_css('strong.col-sm-3.text-sm-end')&.text&.strip - if supplier_label == 'Dodávateľ:' - supplier_info = li_element.at_css('span.col-sm-9')&.text&.strip - cin_label = li_elements[i + 1].at_css('strong.col-sm-3.text-sm-end')&.text&.strip - if cin_label == 'IČO:' - cin = li_elements[i + 1].at_css('span.col-sm-9')&.text&.strip - if supplier_info && cin - project_origin.supplier = supplier_info - project_origin.supplier_cin = cin - project_origin.save! - end - end - end + document = Nokogiri::HTML(open(link).read.force_encoding('UTF-8')) + crz_data = parse_crz_document(document) + + if crz_data[:supplier] && crz_data[:cin] + project_origin.supplier = crz_data[:supplier] + project_origin.supplier_cin = crz_data[:cin] + project_origin.save! end links = extract_links(link) @@ -82,7 +71,7 @@ def perform(project_origin, metais_project) value: url, date: metais_project.latest_version.zmluva_o_dielo, project_origin: project_origin, - origin_type: ORIGIN_TYPE, + origin_type: origin_type, supplier_type: supplier_type) project_supplier.save! @@ -102,6 +91,27 @@ def valid_url?(url) end def extract_links(str) - str.to_s.scan(URI.regexp(['http','https'])) + uri_regexp = /\b(?:https?|ftp):\/\/\S+\b/ + str.to_s.scan(uri_regexp) + end + + def parse_crz_document(document) + li_elements = document.css('li.py-2.border-top') + supplier_info = nil + cin = nil + + li_elements.each_with_index do |li_element, i| + supplier_label = li_element.at_css('strong.col-sm-3.text-sm-end')&.text&.strip + if supplier_label == 'Dodávateľ:' + supplier_info = li_element.at_css('span.col-sm-9')&.text&.strip + cin_label = li_elements[i + 1].at_css('strong.col-sm-3.text-sm-end')&.text&.strip + if cin_label == 'IČO:' + cin = li_elements[i + 1].at_css('span.col-sm-9')&.text&.strip + break if supplier_info && cin + end + end + end + + { supplier: supplier_info, cin: cin } end end diff --git a/app/views/admin/pages/preview.html.erb b/app/views/admin/pages/preview.html.erb index 3e8294c..b07710f 100644 --- a/app/views/admin/pages/preview.html.erb +++ b/app/views/admin/pages/preview.html.erb @@ -12,7 +12,6 @@
    <% if @phase.present? %> - <% @project = @phase_revision %> <%= render file: 'phase_revision/show' %> <% else %> <% @page = @phase_revision %> diff --git a/app/views/metais/projects/index.html.erb b/app/views/metais/projects/index.html.erb index d6cdc5f..90fa867 100644 --- a/app/views/metais/projects/index.html.erb +++ b/app/views/metais/projects/index.html.erb @@ -6,7 +6,10 @@ <%= form_with url: metais_projects_path, method: :get, class: 'form my-2 my-lg-0', local: true, id: "form" do |form| %> <%= hidden_field_tag :sort, params[:sort] %> <%= hidden_field_tag :sort_direction, params[:sort_direction] %> - + <%= hidden_field_tag :status, params[:status] %> + <%= hidden_field_tag :guarantor, params[:guarantor] %> + <%= hidden_field_tag :has_evaluation, params[:has_evaluation] %> +
    diff --git a/spec/jobs/metais/daily_sync_projects_job_spec.rb b/spec/jobs/metais/daily_sync_projects_job_spec.rb index 7ed2de2..42b66cd 100644 --- a/spec/jobs/metais/daily_sync_projects_job_spec.rb +++ b/spec/jobs/metais/daily_sync_projects_job_spec.rb @@ -1,25 +1,50 @@ require 'rails_helper' RSpec.describe Metais::SyncProjectSuppliersJob, type: :job do + let!(:origin_type) { Metais::OriginType.create!(name: 'MetaIS') } + let(:supplier_type_nfp) { Metais::SupplierType.create!(name: "NFP") } let(:supplier_type_vo) { Metais::SupplierType.create!(name: "VO") } let(:supplier_type_crz) { Metais::SupplierType.create!(name: "CRZ") } - let(:project_origin) { Metais::ProjectOrigin.create! } + let!(:metais_project) { create(:metais_project) } + let(:project_origin) { Metais::ProjectOrigin.create!(project: metais_project, origin_type: origin_type, title: "Test") } - let(:latest_version) { double('LatestVersion', link_nfp: 'http://example.com/nfp', vo: 'http://example.com/vo', zmluva_o_dielo_crz: 'http://example.com/crz', datum_nfp: Date.today, vyhlasenie_vo: Date.today, zmluva_o_dielo: Date.today) } - let(:metais_project) { instance_double(Metais::Project, latest_version: latest_version) } + let(:latest_version) { double('LatestVersion', kod_metais: 'code1', link_nfp: "http://example.com/nfp", datum_nfp: Date.today, vo: "http://example.com/vo", vyhlasenie_vo: Date.today, zmluva_o_dielo_crz: "http://example.com/crz", zmluva_o_dielo: Date.today) } + let(:datahub_project) { instance_double(Datahub::Metais::Project, uuid: 'uuid1', latest_version: latest_version) } before do + allow(datahub_project).to receive(:latest_version).and_return(latest_version) + allow(Metais::SupplierType).to receive(:find_by).with(name: "NFP").and_return(supplier_type_nfp) allow(Metais::SupplierType).to receive(:find_by).with(name: "VO").and_return(supplier_type_vo) allow(Metais::SupplierType).to receive(:find_by).with(name: "CRZ").and_return(supplier_type_crz) allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:extract_links).and_return([]) + + crz_html = <<-HTML + + +
      +
    • + Dodávateľ: + Supplier Name +
    • +
    • + IČO: + 12345678 +
    • +
    + + + HTML + + stub_request(:get, "http://example.com/crz").to_return(body: crz_html, headers: { 'Content-Type' => 'text/html' }) + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:open).with("http://example.com/crz").and_return(StringIO.new(crz_html)) end it 'creates ProjectSupplier records for NFP links' do allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:extract_links).with('http://example.com/nfp').and_return(['http://supplier1.com', 'http://supplier2.com']) - expect { described_class.perform_now(project_origin, metais_project) }.to change(Metais::ProjectSupplier, :count).by(2) + expect { described_class.perform_now(project_origin, datahub_project) }.to change(Metais::ProjectSupplier, :count).by(2) suppliers = Metais::ProjectSupplier.where(supplier_type: supplier_type_nfp) expect(suppliers.map(&:name)).to contain_exactly('http://supplier1.com', 'http://supplier2.com') @@ -27,7 +52,7 @@ it 'creates ProjectSupplier records for VO links' do allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:extract_links).with('http://example.com/vo').and_return(['http://supplier3.com']) - expect { described_class.perform_now(project_origin, metais_project) }.to change(Metais::ProjectSupplier, :count).by(1) + expect { described_class.perform_now(project_origin, datahub_project) }.to change(Metais::ProjectSupplier, :count).by(1) supplier = Metais::ProjectSupplier.find_by(name: 'http://supplier3.com') expect(supplier).to be_present @@ -39,11 +64,11 @@ allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:valid_url?).and_return(true) # Mock the parsing to return specific values - allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:parse_crz_document).and_return({ supplier: 'Supplier Info', cin: '12345678' }) + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:parse_crz_document).and_return({ supplier: 'Supplier Info', cin: 12345678 }) - expect { described_class.perform_now(project_origin, metais_project) }.to change(Metais::ProjectSupplier, :count).by(1) + expect { described_class.perform_now(project_origin, datahub_project) }.to change(Metais::ProjectSupplier, :count).by(1) expect(project_origin.supplier).to eq('Supplier Info') - expect(project_origin.supplier_cin).to eq('12345678') + expect(project_origin.supplier_cin).to eq(12345678) end end diff --git a/spec/jobs/metais/sync_project_events_job_spec.rb b/spec/jobs/metais/sync_project_events_job_spec.rb index 9568b09..4627a99 100644 --- a/spec/jobs/metais/sync_project_events_job_spec.rb +++ b/spec/jobs/metais/sync_project_events_job_spec.rb @@ -4,10 +4,13 @@ include ActiveJob::TestHelper let(:project_origin) { instance_double('Metais::ProjectOrigin') } - let(:metais_project) { instance_double('Metais::Project', latest_version: 'v1') } + let(:origin_type) { instance_double('Metais::OriginType') } let(:event_type) { instance_double('Metais::ProjectEventType') } + let(:latest_version) { double('LatestVersion', kod_metais: 'code1') } + let(:datahub_project) { instance_double(Datahub::Metais::Project, uuid: 'uuid1', latest_version: latest_version) } + let(:status_change) do instance_double('Datahub::Metais::ProjectChange', field: 'status', @@ -33,7 +36,7 @@ allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(origin_type) allow(Metais::ProjectEventType).to receive(:find_by).with(name: 'Realita').and_return(event_type) - allow(Datahub::Metais::ProjectChange).to receive(:where).with(project_version: metais_project.latest_version).and_return([status_change, phase_change]) + allow(Datahub::Metais::ProjectChange).to receive(:where).with(project_version: datahub_project.latest_version).and_return([status_change, phase_change]) allow(Datahub::Metais::CodelistProjectState).to receive(:find_by).with(code: 'old_status').and_return(codelist_project_state_old) allow(Datahub::Metais::CodelistProjectState).to receive(:find_by).with(code: 'new_status').and_return(codelist_project_state_new) @@ -47,7 +50,7 @@ describe '#perform' do it 'processes all changes and creates events correctly' do - described_class.perform_now(project_origin, metais_project) + described_class.perform_now(project_origin, datahub_project) expect(Metais::ProjectEvent).to have_received(:find_or_initialize_by).with( project_origin: project_origin, diff --git a/spec/jobs/metais/sync_project_job_spec.rb b/spec/jobs/metais/sync_project_job_spec.rb index b11f18f..dab9fc6 100644 --- a/spec/jobs/metais/sync_project_job_spec.rb +++ b/spec/jobs/metais/sync_project_job_spec.rb @@ -21,7 +21,7 @@ schvalene_rocne_naklady: 25_000) end - let(:metais_project) { double(Datahub::Metais::Project, latest_version: latest_version, uuid: 'uuid1') } + let(:datahub_project) { double(Datahub::Metais::Project, latest_version: latest_version, uuid: 'uuid1') } let(:project) { double(Metais::Project) } let(:project_origin) { instance_double(Metais::ProjectOrigin, save!: true) } @@ -60,17 +60,13 @@ end it 'updates or creates a ProjectOrigin and enqueues subsequent jobs' do - Metais::SyncProjectJob.perform_now(project, metais_project) + Metais::SyncProjectJob.perform_now(project, datahub_project) - expect(enqueued_jobs).to include( - a_hash_including( - job: Metais::ProjectDataExtractionJob, - args: [metais_project.uuid], - queue: 'metais_data_extraction' - ) - ) - expect(Metais::SyncProjectSuppliersJob).to have_received(:perform_later).with(project_origin, metais_project) - expect(Metais::SyncProjectDocumentsJob).to have_received(:perform_later).with(project_origin, metais_project) - expect(Metais::SyncProjectEventsJob).to have_received(:perform_later).with(project_origin, metais_project) + expect(enqueued_jobs).to include(a_hash_including(job: Metais::ProjectDataExtractionJob, + args: [datahub_project.uuid], + queue: 'metais_data_extraction')) + expect(Metais::SyncProjectSuppliersJob).to have_received(:perform_later).with(project_origin, datahub_project) + expect(Metais::SyncProjectDocumentsJob).to have_received(:perform_later).with(project_origin, datahub_project) + expect(Metais::SyncProjectEventsJob).to have_received(:perform_later).with(project_origin, datahub_project) end end diff --git a/spec/jobs/metais/sync_project_suppliers_job_spec.rb b/spec/jobs/metais/sync_project_suppliers_job_spec.rb index 6ef6d3c..da15772 100644 --- a/spec/jobs/metais/sync_project_suppliers_job_spec.rb +++ b/spec/jobs/metais/sync_project_suppliers_job_spec.rb @@ -4,55 +4,79 @@ RSpec.describe Metais::SyncProjectSuppliersJob, type: :job do include ActiveJob::TestHelper - let(:latest_version) do - double('LatestVersion', - link_nfp: 'http://example.com/nfp', - datum_nfp: Date.today, - vo: 'http://example.com/vo', - vyhlasenie_vo: Date.today, - zmluva_o_dielo_crz: 'http://example.com/crz', - zmluva_o_dielo: Date.today) - end + let!(:origin_type) { Metais::OriginType.create!(name: 'MetaIS') } - let(:metais_project) do - double('MetaisProject', latest_version: latest_version) - end + let!(:supplier_type_nfp) { Metais::SupplierType.create!(name: "NFP") } + let!(:supplier_type_vo) { Metais::SupplierType.create!(name: "VO") } + let!(:supplier_type_crz) { Metais::SupplierType.create!(name: "CRZ") } - let(:project_origin) { double('ProjectOrigin') } - let(:project_supplier) { instance_double(Metais::ProjectSupplier, save!: true) } + let!(:metais_project) { create(:metais_project) } + let(:project_origin) { Metais::ProjectOrigin.create!(project: metais_project, origin_type: origin_type, title: "Test") } + + let(:latest_version) { double('LatestVersion', kod_metais: 'code1', link_nfp: "http://example.com/nfp", datum_nfp: Date.today, vo: "http://example.com/vo", vyhlasenie_vo: Date.today, zmluva_o_dielo_crz: "http://example.com/crz", zmluva_o_dielo: Date.today) } + let(:datahub_project) { instance_double(Datahub::Metais::Project, uuid: 'uuid1', latest_version: latest_version) } before do - # Stub HTTP requests - stub_request(:get, 'http://example.com/crz').to_return(status: 200, body: "response body", headers: {}) + allow(datahub_project).to receive(:latest_version).and_return(latest_version) - allow(Metais::ProjectSupplier).to receive(:find_or_initialize_by).and_return(project_supplier) - allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(double('OriginType')) + allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(origin_type) + allow(Metais::SupplierType).to receive(:find_by).with(name: "NFP").and_return(supplier_type_nfp) + allow(Metais::SupplierType).to receive(:find_by).with(name: "VO").and_return(supplier_type_vo) + allow(Metais::SupplierType).to receive(:find_by).with(name: "CRZ").and_return(supplier_type_crz) - allow(project_supplier).to receive(:name=) - allow(project_supplier).to receive(:value=) - allow(project_supplier).to receive(:date=) - end + crz_html = <<-HTML + + +
      +
    • + Dodávateľ: + Supplier Name +
    • +
    • + IČO: + 12345678 +
    • +
    + + + HTML + stub_request(:get, "http://example.com/crz").to_return(body: crz_html, headers: { 'Content-Type' => 'text/html' }) + allow_any_instance_of(Metais::SyncProjectSuppliersJob).to receive(:open).with("http://example.com/crz").and_return(StringIO.new(crz_html)) - after do - clear_enqueued_jobs - clear_performed_jobs + allow(Metais::ProjectSupplier).to receive(:find_or_initialize_by).and_call_original + allow(project_origin).to receive(:supplier=).and_call_original + allow(project_origin).to receive(:supplier_cin=).and_call_original + allow(project_origin).to receive(:save!).and_call_original + + Metais::SyncProjectSuppliersJob.perform_now(project_origin, datahub_project) end - it 'processes each supplier and creates or updates ProjectSuppliers records' do - Metais::SyncProjectSuppliersJob.perform_now(project_origin, metais_project) - - expect(Metais::ProjectSupplier).to have_received(:find_or_initialize_by).with( - name: 'http://example.com/nfp', - value: 'http://example.com/nfp', - date: Date.today, - project_origin: project_origin, - origin_type: anything, - supplier_type: anything - ) - - expect(project_supplier).to have_received(:name=).with('http://example.com/nfp') - expect(project_supplier).to have_received(:value=).with('http://example.com/nfp') - expect(project_supplier).to have_received(:date=).with(Date.today) - expect(project_supplier).to have_received(:save!) + describe '#perform' do + context 'when processing NFP links' do + it 'creates project suppliers for valid NFP links' do + expect(Metais::ProjectSupplier).to have_received(:find_or_initialize_by) + .with(hash_including(name: 'http://example.com/nfp', value: 'http://example.com/nfp', date: Date.today, origin_type: origin_type, supplier_type: supplier_type_nfp)) + end + end + + context 'when processing VO links' do + it 'creates project suppliers for valid VO links' do + expect(Metais::ProjectSupplier).to have_received(:find_or_initialize_by) + .with(hash_including(name: 'http://example.com/vo', value: 'http://example.com/vo', date: Date.today, origin_type: origin_type, supplier_type: supplier_type_vo)) + end + end + + context 'when processing CRZ links' do + it 'scrapes supplier data from the CRZ page and updates the project_origin' do + expect(project_origin).to have_received(:supplier=).with('Supplier Name') + expect(project_origin).to have_received(:supplier_cin=).with('12345678') + expect(project_origin).to have_received(:save!) + end + + it 'creates project suppliers for valid CRZ links' do + expect(Metais::ProjectSupplier).to have_received(:find_or_initialize_by) + .with(hash_including(name: 'http://example.com/crz', value: 'http://example.com/crz', date: Date.today, origin_type: origin_type, supplier_type: supplier_type_crz)) + end + end end -end +end \ No newline at end of file From f87241ea0501f47b7681a212fa672bc04513741f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Mon, 30 Sep 2024 13:10:58 +0200 Subject: [PATCH 20/39] Added datahub db structure --- .github/workflows/slovensko_digital_ci.yml | 2 +- db/datahub_structure.sql | 1007 ++++++++++++++++++++ 2 files changed, 1008 insertions(+), 1 deletion(-) create mode 100644 db/datahub_structure.sql diff --git a/.github/workflows/slovensko_digital_ci.yml b/.github/workflows/slovensko_digital_ci.yml index 2674d92..3eadbbb 100644 --- a/.github/workflows/slovensko_digital_ci.yml +++ b/.github/workflows/slovensko_digital_ci.yml @@ -29,7 +29,7 @@ jobs: bundler-cache: true - run: bundle exec rails db:create db:structure:load --trace - - run: RAILS_ENV=test_datahub bundle exec rails db:create + - run: RAILS_ENV=test_datahub bundle exec rails db:create db:structure:load DB_STRUCTURE=db/datahub_structure.sql --trace - run: bundle exec rspec gitlab-push: diff --git a/db/datahub_structure.sql b/db/datahub_structure.sql new file mode 100644 index 0000000..c6d155b --- /dev/null +++ b/db/datahub_structure.sql @@ -0,0 +1,1007 @@ +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: metais; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA metais; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: codelist_investment_type; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.codelist_investment_type ( + id bigint NOT NULL, + code character varying NOT NULL, + nazov character varying NOT NULL, + order_list integer NOT NULL, + popis character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: codelist_investment_type_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.codelist_investment_type_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: codelist_investment_type_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.codelist_investment_type_id_seq OWNED BY metais.codelist_investment_type.id; + + +-- +-- Name: codelist_program; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.codelist_program ( + id bigint NOT NULL, + kod_metais character varying NOT NULL, + nazov character varying NOT NULL, + nazov_en character varying, + ref_id character varying, + zdroj character varying, + raw_data text, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + uuid character varying +); + + +-- +-- Name: codelist_program_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.codelist_program_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: codelist_program_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.codelist_program_id_seq OWNED BY metais.codelist_program.id; + + +-- +-- Name: codelist_project_phase; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.codelist_project_phase ( + id bigint NOT NULL, + code character varying NOT NULL, + nazov character varying NOT NULL, + order_list integer NOT NULL, + popis character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: codelist_project_phase_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.codelist_project_phase_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: codelist_project_phase_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.codelist_project_phase_id_seq OWNED BY metais.codelist_project_phase.id; + + +-- +-- Name: codelist_project_state; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.codelist_project_state ( + id bigint NOT NULL, + code character varying NOT NULL, + nazov character varying NOT NULL, + order_list integer NOT NULL, + popis character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: codelist_project_state_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.codelist_project_state_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: codelist_project_state_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.codelist_project_state_id_seq OWNED BY metais.codelist_project_state.id; + + +-- +-- Name: codelist_source; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.codelist_source ( + id bigint NOT NULL, + code character varying NOT NULL, + nazov character varying NOT NULL, + order_list integer NOT NULL, + popis character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: codelist_source_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.codelist_source_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: codelist_source_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.codelist_source_id_seq OWNED BY metais.codelist_source.id; + + +-- +-- Name: isvs; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.isvs ( + id bigint NOT NULL, + project_id bigint, + uuid character varying NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + latest_version_id bigint +); + + +-- +-- Name: isvs_document_versions; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.isvs_document_versions ( + id bigint NOT NULL, + document_id bigint NOT NULL, + nazov character varying NOT NULL, + kod_metais character varying NOT NULL, + ref_id character varying NOT NULL, + mime_type character varying, + content_length integer, + status character varying, + poznamka text, + typ_dokumentu character varying, + filename character varying, + metais_created_at timestamp without time zone, + metais_updated_at timestamp without time zone, + raw_data jsonb NOT NULL, + raw_meta jsonb NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: isvs_document_versions_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.isvs_document_versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: isvs_document_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.isvs_document_versions_id_seq OWNED BY metais.isvs_document_versions.id; + + +-- +-- Name: isvs_documents; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.isvs_documents ( + id bigint NOT NULL, + isvs_id bigint, + uuid character varying NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + latest_version_id bigint +); + + +-- +-- Name: isvs_documents_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.isvs_documents_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: isvs_documents_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.isvs_documents_id_seq OWNED BY metais.isvs_documents.id; + + +-- +-- Name: isvs_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.isvs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: isvs_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.isvs_id_seq OWNED BY metais.isvs.id; + + +-- +-- Name: isvs_versions; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.isvs_versions ( + id bigint NOT NULL, + isvs_id bigint NOT NULL, + nazov character varying NOT NULL, + kod_metais character varying NOT NULL, + ref_id character varying, + popis text, + popis_as_is text, + poznamka text, + zdroj character varying, + stav_isvs character varying, + typ_isvs character varying, + raw_data jsonb NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + metais_created_at timestamp without time zone +); + + +-- +-- Name: isvs_versions_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.isvs_versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: isvs_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.isvs_versions_id_seq OWNED BY metais.isvs_versions.id; + + +-- +-- Name: project_changes; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.project_changes ( + id bigint NOT NULL, + project_version_id bigint NOT NULL, + field character varying, + old_value character varying, + new_value character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: project_changes_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.project_changes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: project_changes_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.project_changes_id_seq OWNED BY metais.project_changes.id; + + +-- +-- Name: project_document_versions; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.project_document_versions ( + id bigint NOT NULL, + document_id bigint NOT NULL, + nazov character varying NOT NULL, + kod_metais character varying NOT NULL, + ref_id character varying NOT NULL, + mime_type character varying, + content_length integer, + status character varying, + poznamka text, + typ_dokumentu character varying, + filename character varying, + metais_created_at timestamp without time zone, + metais_updated_at timestamp without time zone, + raw_data jsonb NOT NULL, + raw_meta jsonb NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: project_document_versions_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.project_document_versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: project_document_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.project_document_versions_id_seq OWNED BY metais.project_document_versions.id; + + +-- +-- Name: project_documents; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.project_documents ( + id bigint NOT NULL, + project_id bigint, + uuid character varying NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + latest_version_id bigint +); + + +-- +-- Name: project_documents_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.project_documents_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: project_documents_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.project_documents_id_seq OWNED BY metais.project_documents.id; + + +-- +-- Name: project_versions; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.project_versions ( + id bigint NOT NULL, + project_id bigint NOT NULL, + nazov character varying NOT NULL, + kod_metais character varying NOT NULL, + typ_investicie character varying, + prijimatel character varying, + faza_projektu character varying, + program character varying, + popis text, + datum_zacatia timestamp without time zone, + termin_ukoncenia timestamp without time zone, + schvalovaci_proces character varying, + zdroj character varying, + financna_skupina character varying, + suma_vydavkov numeric(15,2), + rocne_naklady numeric(15,2), + ref_id character varying, + status character varying, + zmena_stavu timestamp without time zone, + schvalene_rocne_naklady numeric(15,2), + schvaleny_rozpocet numeric(15,2), + datum_nfp timestamp without time zone, + link_nfp character varying, + vyhlasenie_vo timestamp without time zone, + vo character varying, + zmluva_o_dielo timestamp without time zone, + zmluva_o_dielo_crz character varying, + raw_data jsonb NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + metais_created_at timestamp without time zone +); + + +-- +-- Name: project_versions_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.project_versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: project_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.project_versions_id_seq OWNED BY metais.project_versions.id; + + +-- +-- Name: projects; Type: TABLE; Schema: metais; Owner: - +-- + +CREATE TABLE metais.projects ( + id bigint NOT NULL, + uuid character varying NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + latest_version_id bigint +); + + +-- +-- Name: projects_id_seq; Type: SEQUENCE; Schema: metais; Owner: - +-- + +CREATE SEQUENCE metais.projects_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: projects_id_seq; Type: SEQUENCE OWNED BY; Schema: metais; Owner: - +-- + +ALTER SEQUENCE metais.projects_id_seq OWNED BY metais.projects.id; + + +-- +-- Name: codelist_investment_type id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_investment_type ALTER COLUMN id SET DEFAULT nextval('metais.codelist_investment_type_id_seq'::regclass); + + +-- +-- Name: codelist_program id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_program ALTER COLUMN id SET DEFAULT nextval('metais.codelist_program_id_seq'::regclass); + + +-- +-- Name: codelist_project_phase id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_project_phase ALTER COLUMN id SET DEFAULT nextval('metais.codelist_project_phase_id_seq'::regclass); + + +-- +-- Name: codelist_project_state id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_project_state ALTER COLUMN id SET DEFAULT nextval('metais.codelist_project_state_id_seq'::regclass); + + +-- +-- Name: codelist_source id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_source ALTER COLUMN id SET DEFAULT nextval('metais.codelist_source_id_seq'::regclass); + + +-- +-- Name: isvs id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs ALTER COLUMN id SET DEFAULT nextval('metais.isvs_id_seq'::regclass); + + +-- +-- Name: isvs_document_versions id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_document_versions ALTER COLUMN id SET DEFAULT nextval('metais.isvs_document_versions_id_seq'::regclass); + + +-- +-- Name: isvs_documents id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_documents ALTER COLUMN id SET DEFAULT nextval('metais.isvs_documents_id_seq'::regclass); + + +-- +-- Name: isvs_versions id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_versions ALTER COLUMN id SET DEFAULT nextval('metais.isvs_versions_id_seq'::regclass); + + +-- +-- Name: project_changes id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_changes ALTER COLUMN id SET DEFAULT nextval('metais.project_changes_id_seq'::regclass); + + +-- +-- Name: project_document_versions id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_document_versions ALTER COLUMN id SET DEFAULT nextval('metais.project_document_versions_id_seq'::regclass); + + +-- +-- Name: project_documents id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_documents ALTER COLUMN id SET DEFAULT nextval('metais.project_documents_id_seq'::regclass); + + +-- +-- Name: project_versions id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_versions ALTER COLUMN id SET DEFAULT nextval('metais.project_versions_id_seq'::regclass); + + +-- +-- Name: projects id; Type: DEFAULT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.projects ALTER COLUMN id SET DEFAULT nextval('metais.projects_id_seq'::regclass); + + +-- +-- Name: codelist_investment_type codelist_investment_type_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_investment_type + ADD CONSTRAINT codelist_investment_type_pkey PRIMARY KEY (id); + + +-- +-- Name: codelist_program codelist_program_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_program + ADD CONSTRAINT codelist_program_pkey PRIMARY KEY (id); + + +-- +-- Name: codelist_project_phase codelist_project_phase_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_project_phase + ADD CONSTRAINT codelist_project_phase_pkey PRIMARY KEY (id); + + +-- +-- Name: codelist_project_state codelist_project_state_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_project_state + ADD CONSTRAINT codelist_project_state_pkey PRIMARY KEY (id); + + +-- +-- Name: codelist_source codelist_source_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.codelist_source + ADD CONSTRAINT codelist_source_pkey PRIMARY KEY (id); + + +-- +-- Name: isvs_document_versions isvs_document_versions_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_document_versions + ADD CONSTRAINT isvs_document_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: isvs_documents isvs_documents_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_documents + ADD CONSTRAINT isvs_documents_pkey PRIMARY KEY (id); + + +-- +-- Name: isvs isvs_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs + ADD CONSTRAINT isvs_pkey PRIMARY KEY (id); + + +-- +-- Name: isvs_versions isvs_versions_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_versions + ADD CONSTRAINT isvs_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: project_changes project_changes_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_changes + ADD CONSTRAINT project_changes_pkey PRIMARY KEY (id); + + +-- +-- Name: project_document_versions project_document_versions_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_document_versions + ADD CONSTRAINT project_document_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: project_documents project_documents_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_documents + ADD CONSTRAINT project_documents_pkey PRIMARY KEY (id); + + +-- +-- Name: project_versions project_versions_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_versions + ADD CONSTRAINT project_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: projects projects_pkey; Type: CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.projects + ADD CONSTRAINT projects_pkey PRIMARY KEY (id); + + +-- +-- Name: index_metais.codelist_investment_type_on_code; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.codelist_investment_type_on_code" ON metais.codelist_investment_type USING btree (code); + + +-- +-- Name: index_metais.codelist_program_on_kod_metais; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.codelist_program_on_kod_metais" ON metais.codelist_program USING btree (kod_metais); + + +-- +-- Name: index_metais.codelist_project_phase_on_code; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.codelist_project_phase_on_code" ON metais.codelist_project_phase USING btree (code); + + +-- +-- Name: index_metais.codelist_project_state_on_code; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.codelist_project_state_on_code" ON metais.codelist_project_state USING btree (code); + + +-- +-- Name: index_metais.codelist_source_on_code; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.codelist_source_on_code" ON metais.codelist_source USING btree (code); + + +-- +-- Name: index_metais.isvs_document_versions_on_document_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_document_versions_on_document_id" ON metais.isvs_document_versions USING btree (document_id); + + +-- +-- Name: index_metais.isvs_documents_on_isvs_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_documents_on_isvs_id" ON metais.isvs_documents USING btree (isvs_id); + + +-- +-- Name: index_metais.isvs_documents_on_latest_version_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_documents_on_latest_version_id" ON metais.isvs_documents USING btree (latest_version_id); + + +-- +-- Name: index_metais.isvs_documents_on_uuid; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_documents_on_uuid" ON metais.isvs_documents USING btree (uuid); + + +-- +-- Name: index_metais.isvs_on_latest_version_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_on_latest_version_id" ON metais.isvs USING btree (latest_version_id); + + +-- +-- Name: index_metais.isvs_on_project_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_on_project_id" ON metais.isvs USING btree (project_id); + + +-- +-- Name: index_metais.isvs_on_uuid; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_on_uuid" ON metais.isvs USING btree (uuid); + + +-- +-- Name: index_metais.isvs_versions_on_isvs_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.isvs_versions_on_isvs_id" ON metais.isvs_versions USING btree (isvs_id); + + +-- +-- Name: index_metais.project_changes_on_project_version_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.project_changes_on_project_version_id" ON metais.project_changes USING btree (project_version_id); + + +-- +-- Name: index_metais.project_document_versions_on_document_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.project_document_versions_on_document_id" ON metais.project_document_versions USING btree (document_id); + + +-- +-- Name: index_metais.project_documents_on_latest_version_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.project_documents_on_latest_version_id" ON metais.project_documents USING btree (latest_version_id); + + +-- +-- Name: index_metais.project_documents_on_project_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.project_documents_on_project_id" ON metais.project_documents USING btree (project_id); + + +-- +-- Name: index_metais.project_documents_on_uuid; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.project_documents_on_uuid" ON metais.project_documents USING btree (uuid); + + +-- +-- Name: index_metais.project_versions_on_project_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.project_versions_on_project_id" ON metais.project_versions USING btree (project_id); + + +-- +-- Name: index_metais.projects_on_latest_version_id; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.projects_on_latest_version_id" ON metais.projects USING btree (latest_version_id); + + +-- +-- Name: index_metais.projects_on_uuid; Type: INDEX; Schema: metais; Owner: - +-- + +CREATE INDEX "index_metais.projects_on_uuid" ON metais.projects USING btree (uuid); + + +-- +-- Name: projects fk_rails_1925410d1c; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.projects + ADD CONSTRAINT fk_rails_1925410d1c FOREIGN KEY (latest_version_id) REFERENCES metais.project_versions(id); + + +-- +-- Name: isvs_documents fk_rails_2926a855af; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_documents + ADD CONSTRAINT fk_rails_2926a855af FOREIGN KEY (isvs_id) REFERENCES metais.isvs(id); + + +-- +-- Name: isvs_documents fk_rails_5d253a31fc; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_documents + ADD CONSTRAINT fk_rails_5d253a31fc FOREIGN KEY (latest_version_id) REFERENCES metais.isvs_document_versions(id); + + +-- +-- Name: project_versions fk_rails_819be9e3a6; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_versions + ADD CONSTRAINT fk_rails_819be9e3a6 FOREIGN KEY (project_id) REFERENCES metais.projects(id); + + +-- +-- Name: project_documents fk_rails_868923b92d; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_documents + ADD CONSTRAINT fk_rails_868923b92d FOREIGN KEY (project_id) REFERENCES metais.projects(id); + + +-- +-- Name: isvs fk_rails_8befaa8056; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs + ADD CONSTRAINT fk_rails_8befaa8056 FOREIGN KEY (project_id) REFERENCES metais.projects(id); + + +-- +-- Name: project_documents fk_rails_9352955d7e; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_documents + ADD CONSTRAINT fk_rails_9352955d7e FOREIGN KEY (latest_version_id) REFERENCES metais.project_document_versions(id); + + +-- +-- Name: project_changes fk_rails_a57192e308; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_changes + ADD CONSTRAINT fk_rails_a57192e308 FOREIGN KEY (project_version_id) REFERENCES metais.project_versions(id); + + +-- +-- Name: project_document_versions fk_rails_ae50626673; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.project_document_versions + ADD CONSTRAINT fk_rails_ae50626673 FOREIGN KEY (document_id) REFERENCES metais.project_documents(id); + + +-- +-- Name: isvs_document_versions fk_rails_affcff3ef1; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_document_versions + ADD CONSTRAINT fk_rails_affcff3ef1 FOREIGN KEY (document_id) REFERENCES metais.isvs_documents(id); + + +-- +-- Name: isvs fk_rails_be183a92d5; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs + ADD CONSTRAINT fk_rails_be183a92d5 FOREIGN KEY (latest_version_id) REFERENCES metais.isvs_versions(id); + + +-- +-- Name: isvs_versions fk_rails_ce75fc7d23; Type: FK CONSTRAINT; Schema: metais; Owner: - +-- + +ALTER TABLE ONLY metais.isvs_versions + ADD CONSTRAINT fk_rails_ce75fc7d23 FOREIGN KEY (isvs_id) REFERENCES metais.isvs(id); + + +-- +-- PostgreSQL database dump complete +-- + +SET search_path TO "$user", public; + From 21bd5b0e7e45ef561d0edb4eb741ef335bd31733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Mon, 30 Sep 2024 13:20:44 +0200 Subject: [PATCH 21/39] Test doubles in correct format --- .../metais/sync_project_events_job_spec.rb | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/spec/jobs/metais/sync_project_events_job_spec.rb b/spec/jobs/metais/sync_project_events_job_spec.rb index 4627a99..fa011ab 100644 --- a/spec/jobs/metais/sync_project_events_job_spec.rb +++ b/spec/jobs/metais/sync_project_events_job_spec.rb @@ -11,26 +11,13 @@ let(:latest_version) { double('LatestVersion', kod_metais: 'code1') } let(:datahub_project) { instance_double(Datahub::Metais::Project, uuid: 'uuid1', latest_version: latest_version) } - let(:status_change) do - instance_double('Datahub::Metais::ProjectChange', - field: 'status', - created_at: Time.now, - new_value: 'new_status', - old_value: 'old_status') - end - - let(:phase_change) do - instance_double('Datahub::Metais::ProjectChange', - field: 'faza_projektu', - created_at: Time.now, - new_value: 'new_phase', - old_value: 'old_phase') - end + let(:status_change) { instance_double(Datahub::Metais::ProjectChange, field: 'status', created_at: Time.now, new_value: 'new_status', old_value: 'old_status') } + let(:phase_change) { instance_double(Datahub::Metais::ProjectChange, field: 'faza_projektu', created_at: Time.now, new_value: 'new_phase', old_value: 'old_phase') } - let(:codelist_project_state_old) { instance_double('Datahub::Metais::CodelistProjectState', nazov: 'Old Status') } - let(:codelist_project_state_new) { instance_double('Datahub::Metais::CodelistProjectState', nazov: 'New Status') } - let(:codelist_project_phase_old) { instance_double('Datahub::Metais::CodelistProjectPhase', nazov: 'Old Phase') } - let(:codelist_project_phase_new) { instance_double('Datahub::Metais::CodelistProjectPhase', nazov: 'New Phase') } + let(:codelist_project_state_old) { instance_double(Datahub::Metais::CodelistProjectState, nazov: 'Old Status') } + let(:codelist_project_state_new) { instance_double(Datahub::Metais::CodelistProjectState, nazov: 'New Status') } + let(:codelist_project_phase_old) { instance_double(Datahub::Metais::CodelistProjectPhase, nazov: 'Old Phase') } + let(:codelist_project_phase_new) { instance_double(Datahub::Metais::CodelistProjectPhase, nazov: 'New Phase') } before do allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(origin_type) From 45de60860a254f18426908299210fcef182142ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Mon, 30 Sep 2024 13:29:00 +0200 Subject: [PATCH 22/39] Change from instance double to double --- spec/jobs/metais/sync_project_events_job_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/jobs/metais/sync_project_events_job_spec.rb b/spec/jobs/metais/sync_project_events_job_spec.rb index fa011ab..d19428b 100644 --- a/spec/jobs/metais/sync_project_events_job_spec.rb +++ b/spec/jobs/metais/sync_project_events_job_spec.rb @@ -11,8 +11,8 @@ let(:latest_version) { double('LatestVersion', kod_metais: 'code1') } let(:datahub_project) { instance_double(Datahub::Metais::Project, uuid: 'uuid1', latest_version: latest_version) } - let(:status_change) { instance_double(Datahub::Metais::ProjectChange, field: 'status', created_at: Time.now, new_value: 'new_status', old_value: 'old_status') } - let(:phase_change) { instance_double(Datahub::Metais::ProjectChange, field: 'faza_projektu', created_at: Time.now, new_value: 'new_phase', old_value: 'old_phase') } + let(:status_change) { double(Datahub::Metais::ProjectChange, field: 'status', created_at: Time.now, new_value: 'new_status', old_value: 'old_status') } + let(:phase_change) { double(Datahub::Metais::ProjectChange, field: 'faza_projektu', created_at: Time.now, new_value: 'new_phase', old_value: 'old_phase') } let(:codelist_project_state_old) { instance_double(Datahub::Metais::CodelistProjectState, nazov: 'Old Status') } let(:codelist_project_state_new) { instance_double(Datahub::Metais::CodelistProjectState, nazov: 'New Status') } From ee636798a89cce86e4553c96bab35015cf357592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Mon, 30 Sep 2024 13:32:13 +0200 Subject: [PATCH 23/39] SyncProjectEventsJobSpec further changes --- spec/jobs/metais/sync_project_events_job_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/jobs/metais/sync_project_events_job_spec.rb b/spec/jobs/metais/sync_project_events_job_spec.rb index d19428b..c14d4d0 100644 --- a/spec/jobs/metais/sync_project_events_job_spec.rb +++ b/spec/jobs/metais/sync_project_events_job_spec.rb @@ -14,10 +14,10 @@ let(:status_change) { double(Datahub::Metais::ProjectChange, field: 'status', created_at: Time.now, new_value: 'new_status', old_value: 'old_status') } let(:phase_change) { double(Datahub::Metais::ProjectChange, field: 'faza_projektu', created_at: Time.now, new_value: 'new_phase', old_value: 'old_phase') } - let(:codelist_project_state_old) { instance_double(Datahub::Metais::CodelistProjectState, nazov: 'Old Status') } - let(:codelist_project_state_new) { instance_double(Datahub::Metais::CodelistProjectState, nazov: 'New Status') } - let(:codelist_project_phase_old) { instance_double(Datahub::Metais::CodelistProjectPhase, nazov: 'Old Phase') } - let(:codelist_project_phase_new) { instance_double(Datahub::Metais::CodelistProjectPhase, nazov: 'New Phase') } + let(:codelist_project_state_old) { double(Datahub::Metais::CodelistProjectState, nazov: 'Old Status') } + let(:codelist_project_state_new) { double(Datahub::Metais::CodelistProjectState, nazov: 'New Status') } + let(:codelist_project_phase_old) { double(Datahub::Metais::CodelistProjectPhase, nazov: 'Old Phase') } + let(:codelist_project_phase_new) { double(Datahub::Metais::CodelistProjectPhase, nazov: 'New Phase') } before do allow(Metais::OriginType).to receive(:find_by).with(name: 'MetaIS').and_return(origin_type) From afeebfa29b1d855b7179bee86db4f5277809f2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Thu, 3 Oct 2024 14:09:38 +0200 Subject: [PATCH 24/39] BE changes to clean up the code, added tooltips to origin types --- .../metais/project_origins_controller.rb | 244 +++++++++--------- app/controllers/metais/projects_controller.rb | 30 +-- app/helpers/metais/projects_helper.rb | 34 +-- app/models/metais/project.rb | 4 +- app/models/metais/project_event.rb | 8 + .../metais/project_origins/edit.html.erb | 69 +++-- app/views/metais/projects/show.html.erb | 99 +++++-- 7 files changed, 269 insertions(+), 219 deletions(-) diff --git a/app/controllers/admin/metais/project_origins_controller.rb b/app/controllers/admin/metais/project_origins_controller.rb index b5f8aff..4ef55f3 100644 --- a/app/controllers/admin/metais/project_origins_controller.rb +++ b/app/controllers/admin/metais/project_origins_controller.rb @@ -1,7 +1,7 @@ class Admin::Metais::ProjectOriginsController < ApplicationController + before_action :get_project, only: [:craete, :edit, :update, :add_event, :add_supplier, :add_link, :add_document, :remove_event, :remove_supplier, :remove_link, :remove_document, :update_group_order] def create - @project = Metais::Project.find(params[:project_id]) @project_origin = @project.project_origins.build(project_origin_params) if @project_origin.save redirect_to admin_metais_project_path(@project), notice: 'Projekt bol úspešne vytvorený.' @@ -11,199 +11,193 @@ def create end def edit - @project = Metais::Project.find(params[:project_id]) @project_info = @project.get_project_origin_info + @project_origins = @project.project_origins - @project_origin = Metais::ProjectOrigin.find(params[:id]) + @project_origin = @project_origins.find(params[:id]) @ai_project_origin = @project_origins.joins(:origin_type).find_by(origin_types: { name: 'AI' }) - @assumption_events = [] - @real_events = [] - @project_origins.each do |project_origin| - @assumption_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Predpoklad'))) - @real_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Realita'))) - end - @assumption_events.sort_by! { |event| event.date } - @real_events.sort_by! { |event| event.date } - - @all_suppliers = [] - @project_origins.each do |project_origin| - @all_suppliers.concat(project_origin.suppliers) - end - @all_suppliers.sort_by! { |event| event.date || Time.zone.parse('2999-12-31') } + @assumption_events = @project_origins.flat_map { |project_origin| project_origin.events.assumpted } + @real_events = @project_origins.flat_map { |project_origin| project_origin.events.real } - @all_links = [] - @project_origins.each do |project_origin| - @all_links.concat(project_origin.links) - end + @combined_suppliers = @project_origins.flat_map(&:suppliers).sort_by { |supplier| supplier.date || Time.zone.parse('2999-12-31') } + @combined_links = @project_origins.flat_map(&:links) + @combined_documents = @project_origins.flat_map(&:documents) - @all_documents = [] - @project_origins.each do |project_origin| - @all_documents.concat(project_origin.documents) - end - - @grouped_documents = @all_documents.group_by(&:description) - @grouped_documents = @grouped_documents.sort_by { |description, docs| docs.first.group_order || Float::INFINITY } - - @project_origin = Metais::ProjectOrigin.includes(:documents, :suppliers).find(@project_origin.id) + @grouped_documents = @combined_documents.group_by(&:description).sort_by { |description, docs| docs.first.group_order || Float::INFINITY } end def update - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:id]) - - finance_source_mappings = {"Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet"} - + @project_origin = @project.project_origins.find(params[:id]) current_project_info = @project.get_project_origin_info - - new_params = project_origin_params.to_h - changed_params = {} - - new_params.each do |field, new_value| - current_value = current_project_info.send(field) - if field == 'finance_source' - current_value_mapped = finance_source_mappings[current_value] || current_value - new_value_mapped = finance_source_mappings[new_value] || new_value - - if new_value.present? && new_value_mapped.to_s.strip != current_value_mapped.to_s.strip - changed_params[field] = new_value - end - else - if field == 'end_date' || field == 'start_date' - new_value = new_value.present? ? Date.parse(new_value.to_s) : nil - current_value = current_value.present? ? current_value.to_date : nil - end - - if new_value.present? && new_value.to_s.strip != current_value.to_s.strip - changed_params[field] = new_value - end - end - end - + changed_params = detect_changes(current_project_info, project_origin_params.to_h) + if changed_params.any? && @project_origin.update(changed_params) redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Projekt bol úspešne aktualizovaný.' else - redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Žiadne nové informácie pre projekt.' + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Žiadne nové informácie v projekte.' end end def add_event - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - @project_info = @project.get_project_origin_info - - origin_type = Metais::OriginType.find_by(name: params[:event][:origin_type]) - event_type = Metais::ProjectEventType.find_by(name: params[:event][:event_type]) - event_params = params.require(:event).permit(:name, :value, :date) - .merge(origin_type: origin_type, event_type: event_type) - @event = @project_origin.events.create(event_params) + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @event = @project_origin.events.build(event_params) if @event.save - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Event bol úspešné pridaný.' + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Aktivita bola úspešné pridaná.' else - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), alert: 'Error encountered while creating an event: ' + @event.errors.full_messages.to_sentence + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: "Nastala chyba pri vytváraní aktivity: #{@event.errors.full_messages.to_sentence}" end end def add_supplier - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - - origin_type = Metais::OriginType.find_by(name: params[:supplier][:origin_type]) - supplier_type = Metais::SupplierType.find(params[:supplier][:supplier_type]) - - supplier_params = params.require(:supplier).permit(:name, :value) - .merge(name: params[:supplier][:value], origin_type: origin_type, supplier_type: supplier_type) - @supplier = @project_origin.suppliers.create(supplier_params) + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @supplier = @project_origin.suppliers.build(supplier_params) if @supplier.save - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Supplier bol úspešné pridaný.' + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Dodávateľ bol úspešné pridaný.' else - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), alert: 'Error encountered while creating an event: ' + @event.errors.full_messages.to_sentence + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: "Nastala chyba pri vytváraní dodávateľa: #{@event.errors.full_messages.to_sentence}" end end def add_link - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - - origin_type = Metais::OriginType.find_by(name: params[:link][:origin_type]) - - link_params = params.require(:link).permit(:name, :value).merge(origin_type: origin_type) - @link = @project_origin.links.create(link_params) + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @link = @project_origin.links.build(link_params) if @link.save - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Link bol úspešné pridaný.' + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Link bol úspešné pridaný.' else - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), alert: 'Error encountered while creating an link: ' + @event.errors.full_messages.to_sentence + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: "Nastala chyba pri vytváraní linku: #{@event.errors.full_messages.to_sentence}" end end def add_document - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - - origin_type = Metais::OriginType.find_by(name: params[:document][:origin_type]) - - document_params = params.require(:document).permit(:name, :value, :description).merge(origin_type: origin_type) - @document = @project_origin.documents.create(document_params) + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @document = @project_origin.documents.build(document_params) if @document.save - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Dokument bol úspešné pridaný.' + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Dokument bol úspešné pridaný.' else - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), alert: 'Error encountered while creating an document: ' + @event.errors.full_messages.to_sentence + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: "Nastala chyba pri vytváraní dokumentu: #{@event.errors.full_messages.to_sentence}" end end def remove_event - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - @project_event = Metais::ProjectEvent.find(params[:event_id]) - @project_event.destroy - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Event z harmonogramu bol úspešne odstránený.' + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @project_event = @project_origin.events.find(params[:event_id]) + + if @project_event.destroy + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Aktivita bola úspešne odstránená z harmonogramu.' + else + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: 'Nastala chyba pri odstraňovaní aktivity z harmonogramu.' + end end def remove_supplier - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - @project_supplier = Metais::ProjectSupplier.find(params[:supplier_id]) - @project_supplier.destroy - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Supplier bol úspešne odstránený.' + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @project_supplier = @project_origin.suppliers.find(params[:supplier_id]) + + if @project_supplier.destroy + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Dodávateľ bol úspešne odstránený.' + else + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: 'Nastala chyba pri odstraňovaní dodávateľa.' + end end def remove_link - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - @project_link = Metais::ProjectLink.find(params[:link_id]) - @project_link.destroy - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Link bol úspešne odstránený.' + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @project_link = @project_origin.links.find(params[:link_id]) + + if @project_link.destroy + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Link bol úspešne odstránený.' + else + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: 'Nastala chyba pri odstraňovaní linku.' + end end def remove_document - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) - @project_document = Metais::ProjectDocument.find(params[:document_id]) - @project_document.destroy - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id), notice: 'Dokument bol úspešne odstránený.' + @project_origin = @project.project_origins.find(params[:project_origin_id]) + @project_document = @project_origin.documents.find(params[:document_id]) + + if @project_document.destroy + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), notice: 'Dokument bol úspešne odstránený.' + else + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: 'Nastala chyba pri odstraňovaní dokumentu.' + end end def update_group_order - @project = Metais::Project.find(params[:project_id]) - @project_origin = Metais::ProjectOrigin.find(params[:project_origin_id]) + @project_origin = @project.project_origins.find(params[:project_origin_id]) description = params[:description] group_order = params[:group_order].to_i Metais::ProjectDocument.where(description: description).update_all(group_order: group_order) - flash[:notice] = "Group order updated successfully." - redirect_to edit_admin_metais_project_project_origin_path(project_id: @project.id, id: @project_origin.id) + flash[:notice] = "Zoskupenie dokumentov bolo úspešne updatované." + redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin) end private + def get_project + @project = Metais::Project.find(params[:project_id]) + end + def project_origin_params params.require(:metais_project_origin).permit(:title, :description, :guarantor, :project_manager, :start_date, :end_date, :status, :phase, :finance_source, :investment, :operation, :approved_investment, :approved_operation, :supplier, :supplier_cin, :targets_text, :documents_text, :links_text, project_events: [:name, :value, :date]) end + + def event_params + origin_type = Metais::OriginType.find_by(name: params[:event][:origin_type]) + event_type = Metais::ProjectEventType.find_by(name: params[:event][:event_type]) + params.require(:event).permit(:name, :value, :date).merge(origin_type: origin_type, event_type: event_type) + end + + def supplier_params + origin_type = Metais::OriginType.find_by(name: params[:supplier][:origin_type]) + supplier_type = Metais::SupplierType.find(params[:supplier][:supplier_type]) + params.require(:supplier).permit(:name, :value).merge(name: params[:supplier][:value], origin_type: origin_type, supplier_type: supplier_type) + end + + def link_params + origin_type = Metais::OriginType.find_by(name: params[:link][:origin_type]) + params.require(:link).permit(:name, :value).merge(origin_type: origin_type) + end + + def document_params + origin_type = Metais::OriginType.find_by(name: params[:document][:origin_type]) + params.require(:document).permit(:name, :value, :description).merge(origin_type: origin_type) + end + + def detect_changes(current_project_info, new_params) + finance_source_mappings = {"Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet"} + changed_params = {} + + new_params.each do |field, new_value| + current_value = current_project_info.send(field) + + if field == 'finance_source' + current_value_mapped = finance_source_mappings[current_value] || current_value + new_value_mapped = finance_source_mappings[new_value] || new_value + + if new_value.present? && new_value_mapped.to_s.strip != current_value_mapped.to_s.strip + changed_params[field] = new_value + end + else + if %w[end_date start_date].include?(field) + new_value = new_value.present? ? Date.parse(new_value.to_s) : nil + current_value = current_value.present? ? current_value.to_date : nil + end + + if new_value.present? && new_value.to_s.strip != current_value.to_s.strip + changed_params[field] = new_value + end + end + end + + changed_params + end end \ No newline at end of file diff --git a/app/controllers/metais/projects_controller.rb b/app/controllers/metais/projects_controller.rb index 269e6b6..9440d59 100644 --- a/app/controllers/metais/projects_controller.rb +++ b/app/controllers/metais/projects_controller.rb @@ -15,31 +15,13 @@ def show @project_info = @project.get_project_origin_info @project_origins = @project.project_origins - @assumption_events = [] - @real_events = [] - @project_origins.each do |project_origin| - @assumption_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Predpoklad'))) - @real_events.concat(project_origin.events.where(event_type: Metais::ProjectEventType.find_by(name: 'Realita'))) - end - @assumption_events.sort_by! { |event| event.date } - @real_events.sort_by! { |event| event.date } + @assumption_events = @project_origins.flat_map { |project_origin| project_origin.events.assumpted } + @real_events = @project_origins.flat_map { |project_origin| project_origin.events.real } - @all_suppliers = [] - @project_origins.each do |project_origin| - @all_suppliers.concat(project_origin.suppliers) - end - @all_suppliers.sort_by! { |event| event.date || Time.zone.parse('2999-12-31')} + @combined_suppliers = @project_origins.flat_map(&:suppliers).sort_by { |supplier| supplier.date || Time.zone.parse('2999-12-31') } + @combined_links = @project_origins.flat_map(&:links) + @combined_documents = @project_origins.flat_map(&:documents) - @all_links = [] - @project_origins.each do |project_origin| - @all_links.concat(project_origin.links) - end - - @all_documents = @project_origins.flat_map(&:documents) - @grouped_documents = @all_documents.group_by(&:description) - @grouped_documents = @grouped_documents.sort_by { |description, docs| docs.first.group_order || Float::INFINITY } - - @project_origin = @project.project_origins.first - @project_origin = Metais::ProjectOrigin.includes(:documents, :suppliers).find(@project_origin.id) + @grouped_documents = @combined_documents.group_by(&:description).sort_by { |description, docs| docs.first.group_order || Float::INFINITY } end end diff --git a/app/helpers/metais/projects_helper.rb b/app/helpers/metais/projects_helper.rb index ffd2f89..9993754 100644 --- a/app/helpers/metais/projects_helper.rb +++ b/app/helpers/metais/projects_helper.rb @@ -1,25 +1,25 @@ module Metais::ProjectsHelper def origin_type_logo(origin_type) - if origin_type.is_a?(Integer) - case origin_type - when 3 - 'icons/sd_logo.png' - when 2 - 'icons/ai_logo.png' - else - 'icons/metais_logo.png' - end + if origin_type.is_a?(Integer) + case origin_type + when 3 + ['icons/sd_logo.png', 'Slovensko.Digital'] + when 2 + ['icons/ai_logo.png', 'Umelá inteligencia'] else - case origin_type.name - when 'Human' - 'icons/sd_logo.png' - when 'AI' - 'icons/ai_logo.png' - else - 'icons/metais_logo.png' - end + ['icons/metais_logo.png', 'MetaIS'] + end + else + case origin_type.name + when 'Human' + ['icons/sd_logo.png', 'Slovensko.Digital'] + when 'AI' + ['icons/ai_logo.png', 'Umelá inteligencia'] + else + ['icons/metais_logo.png', 'MetaIS'] end end +end def convert_to_list(text) items = text.split('\n') diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index 6d65046..a05e579 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -22,6 +22,8 @@ def evaluations end def get_project_origin_info + return @project_origin_info if defined?(@project_origin_info) + finance_source_mappings = {"Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet"} fields = %w[title status phase description guarantor project_manager start_date end_date @@ -43,7 +45,7 @@ def get_project_origin_info end end - project_info + @project_origin_info = project_info end def self.evaluation_counts diff --git a/app/models/metais/project_event.rb b/app/models/metais/project_event.rb index 1eb3311..39ffc71 100644 --- a/app/models/metais/project_event.rb +++ b/app/models/metais/project_event.rb @@ -13,4 +13,12 @@ class Metais::ProjectEvent < ApplicationRecord belongs_to :project_origin, class_name: 'Metais::ProjectOrigin' belongs_to :origin_type, class_name: 'Metais::OriginType' belongs_to :event_type, class_name: 'Metais::ProjectEventType' + + scope :assumpted, -> { + where(event_type: Metais::ProjectEventType.find_by(name: 'Predpoklad')).order(:date) + } + + scope :real, -> { + where(event_type: Metais::ProjectEventType.find_by(name: 'Realita')).order(:date) + } end diff --git a/app/views/admin/metais/project_origins/edit.html.erb b/app/views/admin/metais/project_origins/edit.html.erb index 41d967d..3e664fc 100644 --- a/app/views/admin/metais/project_origins/edit.html.erb +++ b/app/views/admin/metais/project_origins/edit.html.erb @@ -14,7 +14,8 @@
    <%= form.label :title, "Názov projektu" %> <% if @project_info.title.present? %> - <%= image_tag origin_type_logo(@project_info.title.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.title.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.text_field :title, value: @project_info.title, class: "form-control" %> @@ -22,7 +23,8 @@
    <%= form.label :description, "Popis" %> <% if @project_info.description.present? %> - <%= image_tag origin_type_logo(@project_info.description.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.description.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.text_area :description, value: @project_info.description, class: "form-control", rows: 12 %> @@ -33,7 +35,8 @@
    <%= form.label :guarantor, "Garant" %> <% if @project_info.guarantor.present? %> - <%= image_tag origin_type_logo(@project_info.guarantor.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.guarantor.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.text_field :guarantor, value: @project_info.guarantor, class: "form-control" %> @@ -41,7 +44,8 @@
    <%= form.label :project_manager, "Projektový manažér" %> <% if @project_info.project_manager.present? %> - <%= image_tag origin_type_logo(@project_info.project_manager.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.project_manager.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.text_field :project_manager, value: @project_info.project_manager, class: "form-control" %> @@ -50,7 +54,8 @@
    <%= form.label :finance_source, "Zdroj financovania" %> <% if @project_info.finance_source.present? %> - <%= image_tag origin_type_logo(@project_info.finance_source.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.finance_source.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <% finance_source_mappings = {"Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet"} %> @@ -60,13 +65,15 @@
    <%= form.label :status, "Stav" %> <% if @project_info.status.present? %> - <%= image_tag origin_type_logo(@project_info.status.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.status.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.select :status, Metais::ProjectOrigin.pluck(:status).uniq.reject(&:blank?), { selected: @project_info.status }, { class: "form-control" } %>
    <%= form.label :phase, "Fáza" %> <% if @project_info.phase.present? %> - <%= image_tag origin_type_logo(@project_info.phase.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.phase.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.select :phase, Metais::ProjectOrigin.pluck(:phase).uniq.reject(&:blank?), { selected: @project_info.phase }, { class: "form-control" } %> @@ -75,7 +82,8 @@
    <%= form.label :start_date, "Dátum začiatku" %> <% if @project_info.start_date.present? %> - <%= image_tag origin_type_logo(@project_info.start_date.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.start_date.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.date_field :start_date, @@ -85,7 +93,8 @@
    <%= form.label :end_date, "Termín ukončenia " %> <% if @project_info.end_date.present? %> - <%= image_tag origin_type_logo(@project_info.end_date.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.end_date.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.date_field :end_date, @@ -99,13 +108,15 @@
    <%= form.label :investment, "Investícia" %> <% if @project_info.investment.present? %> - <%= image_tag origin_type_logo(@project_info.investment.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.investment.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.number_field :investment, value: @project_info.investment, step: 0.1, class: "form-control" %>
    <%= form.label :operation, "Ročné náklady" %> <% if @project_info.operation.present? %> - <%= image_tag origin_type_logo(@project_info.operation.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.operation.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.number_field :operation, value: @project_info.operation, step: 0.1, class: "form-control" %> @@ -113,13 +124,15 @@
    <%= form.label :approved_investment, "Schválená investícia" %> <% if @project_info.approved_investment.present? %> - <%= image_tag origin_type_logo(@project_info.approved_investment.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.approved_investment.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.number_field :approved_investment, value: @project_info.approved_investment, step: 0.1, class: "form-control" %>
    <%= form.label :approved_operation, "Schválené ročné náklady" %> <% if @project_info.approved_operation.present? %> - <%= image_tag origin_type_logo(@project_info.approved_operation.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.approved_operation.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.number_field :approved_operation, value: @project_info.approved_operation, step: 0.1, class: "form-control" %> @@ -134,7 +147,8 @@
    <%= form.label :targets_text, "Ciele a merateľné ukazovatele" %> <% if @project_info.targets_text.present? %> - <%= image_tag origin_type_logo(@project_info.targets_text.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.targets_text.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.text_area :targets_text, value: @project_info.targets_text, class: "form-control", rows: 5 %> @@ -145,7 +159,8 @@
    <%= form.label :documents_text, "Dokumenty" %> <% if @project_info.documents_text.present? %> - <%= image_tag origin_type_logo(@project_info.documents_text.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.documents_text.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.text_area :documents_text, value: @project_info.documents_text, class: "form-control", rows: 2 %> @@ -156,7 +171,8 @@
    <%= form.label :links_text, "Linky" %> <% if @project_info.links_text.present? %> - <%= image_tag origin_type_logo(@project_info.links_text.origin), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.links_text.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %>
    <%= form.text_area :links_text, value: @project_info.links_text, class: "form-control", rows: 2 %> @@ -221,7 +237,8 @@ <% @assumption_events.each do |event| %>
    @@ -259,7 +276,8 @@ <% @real_events.each do |event| %> @@ -297,10 +315,11 @@ - <% @all_suppliers.each do |supplier| %> + <% @combined_suppliers.each do |supplier| %> @@ -356,10 +375,11 @@ - <% @all_documents.each do |document| %> + <% @combined_documents.each do |document| %> @@ -395,10 +415,11 @@ - <% @all_links.each do |link| %> + <% @combined_links.each do |link| %> diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb index 74aff8f..7ea06ae 100644 --- a/app/views/metais/projects/show.html.erb +++ b/app/views/metais/projects/show.html.erb @@ -5,7 +5,23 @@
    <%= simple_format(@project_info.description) %>
    - + +
    + +
    +
    +
    Pôvod údajov o projekte
    +
      + <% [['icons/sd_logo.png', 'Ručne vyplnený údaj od Slovensko.Digital'], ['icons/ai_logo.png', 'Údaj spracovaný našim AI extraktorom'], ['icons/metais_logo.png', 'Údaj pochádzajúci z MetaIS portálu']].each do |logo_path, tooltip_text| %> +
    • + <%= image_tag logo_path, style: "width: 16px; height: 16px; margin-right: 8px;" %> + <%= tooltip_text %> +
    • + <% end %> +
    +
    +
    +
    @@ -14,7 +30,8 @@
    Garant <% if @project_info.guarantor.present? %> - <%= image_tag origin_type_logo(@project_info.guarantor.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.guarantor.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.guarantor.present? %> @@ -26,7 +43,8 @@
    Projektový manažér <% if @project_info.project_manager.present? %> - <%= image_tag origin_type_logo(@project_info.project_manager.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.project_manager.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.project_manager.present? %> @@ -38,7 +56,8 @@
    Investičné náklady <% if @project_info.investment.present? %> - <%= image_tag origin_type_logo(@project_info.investment.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.investment.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.investment.present? %> @@ -53,7 +72,8 @@
    Zdroj financovania <% if @project_info.finance_source.present? %> - <%= image_tag origin_type_logo(@project_info.finance_source.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.finance_source.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.finance_source.present? %> @@ -65,9 +85,11 @@
    Aktuálny stav <% if @project_info.status.present? %> - <%= image_tag origin_type_logo(@project_info.status.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.status.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% elsif @project_info.phase.present? %> - <%= image_tag origin_type_logo(@project_info.phase.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.phase.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.finance_source.present? %> @@ -79,7 +101,8 @@
    Ročné náklady <% if @project_info.operation.present? %> - <%= image_tag origin_type_logo(@project_info.operation.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.operation.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.operation.present? %> @@ -94,7 +117,8 @@
    Dátum začiatku <% if @project_info.start_date.present? %> - <%= image_tag origin_type_logo(@project_info.start_date.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.start_date.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.start_date.present? %> @@ -106,7 +130,8 @@
    Termín ukončenia <% if @project_info.end_date.present? %> - <%= image_tag origin_type_logo(@project_info.end_date.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.end_date.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.end_date.present? %> @@ -118,7 +143,8 @@
    Schválené investičné náklady <% if @project_info.approved_investment.present? %> - <%= image_tag origin_type_logo(@project_info.approved_investment.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.approved_investment.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.approved_investment.present? %> @@ -137,7 +163,8 @@
    Posledná zmena <% if @project_info.updated_at.present? %> - <%= image_tag origin_type_logo(@project_info.updated_at.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.updated_at.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project.updated_at.present? %> @@ -149,7 +176,8 @@
    Schválené ročné náklady <% if @project_info.approved_operation.present? %> - <%= image_tag origin_type_logo(@project_info.approved_operation.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.approved_operation.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> <% end %>
    <% if @project_info.approved_operation.present? %> @@ -206,7 +234,8 @@
    Ciele a merateľné ukazovatele - <%= image_tag origin_type_logo(@project_info.targets_text.origin), style: "width: 18px; height: 18px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.targets_text.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %>
    <%= convert_to_list(@project_info.targets_text) %>
    @@ -231,9 +260,12 @@
    + <% logo_path, tooltip_text = origin_type_logo(event.origin_type) %> +

    - <%= event.name %> <%= image_tag origin_type_logo(event.origin_type), style: "width: 12px; height: 12px;", class: 'mr-2' if event.origin_type.present? %> + <%= event.name %> <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 12px; height: 12px;", class:"mr-2 tag-tooltip" if event.origin_type.present? %>

    +

    <%= event.value %>

    <%= event.date.in_time_zone('Europe/Bratislava').strftime('%m/%Y') %>
    @@ -259,9 +291,12 @@
    + <% logo_path, tooltip_text = origin_type_logo(event.origin_type) %> +

    - <%= event.name %> <%= image_tag origin_type_logo(event.origin_type), style: "width: 12px; height: 12px;", class: 'mr-2' if event.origin_type.present? %> + <%= event.name %> <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 12px; height: 12px;", class:"mr-2 tag-tooltip" if event.origin_type.present? %>

    +

    <%= event.value %>

    <%= event.date.in_time_zone('Europe/Bratislava').strftime('%m/%Y') %>
    @@ -278,7 +313,7 @@
    <% end %> - <% if @project_info.supplier.present? || @all_suppliers.any? %> + <% if @project_info.supplier.present? || @combined_suppliers.any? %>
    Dodávateľ projektu
    @@ -287,15 +322,18 @@ <%= @project_info.supplier %> - <%= image_tag origin_type_logo(@project_info.supplier&.origin), style: "width: 16px; height: 16px; margin-left: 6px;" %> + <% logo_path, tooltip_text = origin_type_logo(@project_info.supplier&.origin) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px; margin-left: 6px;", class:"tag-tooltip" %> + <% else %> Neznámy <% end %>

    - <% if @all_suppliers.any? %> + <% if @combined_suppliers.any? %>
      - <% @all_suppliers.each do |supplier| %> + <% @combined_suppliers.each do |supplier| %> + <% logo_path, tooltip_text = origin_type_logo(supplier.origin_type) %> <%= case supplier.supplier_type&.name when "CRZ" "Dodávateľská zmlúva" @@ -307,8 +345,7 @@ supplier.supplier_type&.name end %>
    • - <%= image_tag origin_type_logo(supplier.origin_type), style: "width: 12px; height: 12px;" if supplier.origin_type.present? %> - + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 12px; height: 12px;", class:"tag-tooltip" if supplier.origin_type.present? %> <% if supplier.value.present? && supplier.value =~ /\Ahttps?:\/\/[\S]+\z/ %> <%= link_to supplier.name, supplier.value %> <% else %> @@ -336,8 +373,10 @@

      <%= description %>

        <% documents.each do |doc| %> + <% logo_path, tooltip_text = origin_type_logo(doc.origin_type) %> +
      • - <%= image_tag origin_type_logo(doc.origin_type), style: "width: 12px; height: 12px;" if doc.origin_type.present? %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 12px; height: 12px;", class:"tag-tooltip" if doc.origin_type.present? %> <%= link_to doc.name, doc.value %>
      • <% end %> @@ -345,28 +384,32 @@ <% end %> <% end %> -
    <% end %> - <% if @project_info.links_text.present? || @all_links.any? %> + <% if @project_info.links_text.present? || @combined_links.any? %>
    Linky
    + <% if @project_info.links_text.present? %>

    <%= @project_info.links_text %>

    <% end %> - <% if @all_links.any? %> + + <% if @combined_links.any? %>
      - <% @all_links.each do |link| %> + <% @combined_links.each do |link| %> + <% logo_path, tooltip_text = origin_type_logo(link.origin_type) %> +
    • - <%= image_tag origin_type_logo(link.origin_type), style: "width: 12px; height: 12px;" if link.origin_type.present? %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 12px; height: 12px;", class:"tag-tooltip" if link.origin_type.present? %> <%= link_to link.name, link.value %>
    • <% end %>
    <% end %> +
    <% end %> From e46e9f93f4a5cba13b0c20e1e90d966178bfcb61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Thu, 3 Oct 2024 23:41:40 +0200 Subject: [PATCH 25/39] Added legend for project origin types, be fixes --- .../metais/project_origins_controller.rb | 6 +-- app/helpers/metais/projects_helper.rb | 12 ++--- app/models/metais/project.rb | 52 ++++++++++--------- .../metais/project_origins/edit.html.erb | 2 +- app/views/metais/projects/show.html.erb | 35 +++++++------ app/views/static/index.html.erb | 3 -- 6 files changed, 57 insertions(+), 53 deletions(-) diff --git a/app/controllers/admin/metais/project_origins_controller.rb b/app/controllers/admin/metais/project_origins_controller.rb index 4ef55f3..312f42f 100644 --- a/app/controllers/admin/metais/project_origins_controller.rb +++ b/app/controllers/admin/metais/project_origins_controller.rb @@ -1,5 +1,5 @@ class Admin::Metais::ProjectOriginsController < ApplicationController - before_action :get_project, only: [:craete, :edit, :update, :add_event, :add_supplier, :add_link, :add_document, :remove_event, :remove_supplier, :remove_link, :remove_document, :update_group_order] + before_action :set_project, only: [:craete, :edit, :update, :add_event, :add_supplier, :add_link, :add_document, :remove_event, :remove_supplier, :remove_link, :remove_document, :update_group_order] def create @project_origin = @project.project_origins.build(project_origin_params) @@ -142,7 +142,7 @@ def update_group_order private - def get_project + def set_project @project = Metais::Project.find(params[:project_id]) end @@ -173,7 +173,7 @@ def document_params end def detect_changes(current_project_info, new_params) - finance_source_mappings = {"Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet"} + finance_source_mappings = Metais::Project::FINANCE_SOURCE_MAPPINGS changed_params = {} new_params.each do |field, new_value| diff --git a/app/helpers/metais/projects_helper.rb b/app/helpers/metais/projects_helper.rb index 9993754..2beba6e 100644 --- a/app/helpers/metais/projects_helper.rb +++ b/app/helpers/metais/projects_helper.rb @@ -3,20 +3,20 @@ def origin_type_logo(origin_type) if origin_type.is_a?(Integer) case origin_type when 3 - ['icons/sd_logo.png', 'Slovensko.Digital'] + ['icons/sd_logo.png', 'Ručne vyplnený údaj od Slovensko.Digital'] when 2 - ['icons/ai_logo.png', 'Umelá inteligencia'] + ['icons/ai_logo.png', 'Údaj spracovaný našim AI extraktorom'] else - ['icons/metais_logo.png', 'MetaIS'] + ['icons/metais_logo.png', 'Údaj pochádzajúci z MetaIS portálu'] end else case origin_type.name when 'Human' - ['icons/sd_logo.png', 'Slovensko.Digital'] + ['icons/sd_logo.png', 'Ručne vyplnený údaj od Slovensko.Digital'] when 'AI' - ['icons/ai_logo.png', 'Umelá inteligencia'] + ['icons/ai_logo.png', 'Údaj spracovaný našim AI extraktorom'] else - ['icons/metais_logo.png', 'MetaIS'] + ['icons/metais_logo.png', 'Údaj pochádzajúci z MetaIS portálu'] end end end diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index a05e579..6f9cc45 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -14,6 +14,8 @@ class Metais::Project < ApplicationRecord foreign_key: 'metais_project_id', association_foreign_key: 'project_id' + FINANCE_SOURCE_MAPPINGS = { "Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet" } + def evaluations ::Project.joins(''' INNER JOIN "public"."projects_metais_projects" ON "public"."projects_metais_projects"."project_id" = "projects"."id" @@ -22,30 +24,7 @@ def evaluations end def get_project_origin_info - return @project_origin_info if defined?(@project_origin_info) - - finance_source_mappings = {"Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet"} - - fields = %w[title status phase description guarantor project_manager start_date end_date - finance_source investment operation approved_investment approved_operation - supplier supplier_cin targets_text events_text documents_text links_text updated_at] - - origins = self.project_origins.sort_by { |origin| -origin.origin_type_id } - - project_info = OpenStruct.new - fields.each do |field| - origin = origins.detect { |origin| !origin.send(field).nil? } - value = origin&.send(field) - - if field == 'finance_source' && value - value = finance_source_mappings[value] || value - end - if value - project_info.send("#{field}=", Metais::ValueWithOrigin.new(value, origin.origin_type_id)) - end - end - - @project_origin_info = project_info + @project_origin_info ||= load_project_origin_info end def self.evaluation_counts @@ -107,4 +86,29 @@ def self.filtered_and_sorted_projects(params) projects.page(page).per(per_page) end + + private + + def load_project_origin_info + fields = %w[title status phase description guarantor project_manager start_date end_date + finance_source investment operation approved_investment approved_operation + supplier supplier_cin targets_text events_text documents_text links_text updated_at] + + origins = self.project_origins.sort_by { |origin| -origin.origin_type_id } + + project_info = OpenStruct.new + fields.each do |field| + origin = origins.detect { |origin| !origin.send(field).nil? } + value = origin&.send(field) + + if field == 'finance_source' && value + value = FINANCE_SOURCE_MAPPINGS[value] || value + end + if value + project_info.send("#{field}=", Metais::ValueWithOrigin.new(value, origin.origin_type_id)) + end + end + + project_info + end end diff --git a/app/views/admin/metais/project_origins/edit.html.erb b/app/views/admin/metais/project_origins/edit.html.erb index 3e664fc..2cf511a 100644 --- a/app/views/admin/metais/project_origins/edit.html.erb +++ b/app/views/admin/metais/project_origins/edit.html.erb @@ -58,7 +58,7 @@ <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <% end %> - <% finance_source_mappings = {"Medzirezortný program 0EK Informačné technológie financované zo štátneho rozpočtu" => "Štátny rozpočet"} %> + <% finance_source_mappings = Metais::Project::FINANCE_SOURCE_MAPPINGS %> <%= form.select :finance_source, Metais::ProjectOrigin.pluck(:finance_source).uniq.reject(&:blank?).map { |fs| [finance_source_mappings[fs] || fs, fs] }, { selected: @project_info.finance_source }, class: "form-control" %>
    diff --git a/app/views/metais/projects/show.html.erb b/app/views/metais/projects/show.html.erb index 7ea06ae..06d744e 100644 --- a/app/views/metais/projects/show.html.erb +++ b/app/views/metais/projects/show.html.erb @@ -2,28 +2,31 @@

    <%= @project_info.title %>

    + +
    +
    +
      + <% [['icons/sd_logo.png', 'Údaj bol manuálne zadaný členmi tímu Slovensko.Digital. Tento proces zaručuje vysokú presnosť, keďže experti starostlivo vyplnili údaje na základe overených informácií.'], + ['icons/ai_logo.png', 'Údaj bol automaticky získaný pomocou nášho AI nástroja na spracovanie dát. Nástroj prehľadáva veľké množstvo dokumentov a extrahuje z nich relevantné informácie.'], + ['icons/metais_logo.png', 'Údaj pochádza z verejne dostupného informačného systému MetaIS, ktorý zhromažďuje informácie o štátnych IT projektoch verejnej správy. Poskytuje oficiálne informácie o projekte.']].each_with_index do |(logo_path, tooltip_text), index| %> +
    • +
      + <%= image_tag logo_path, style:"height: 28px; width:28px;" %> +
      + <%= raw tooltip_text %> +
    • + <% end %> +
    +
    +
    <%= simple_format(@project_info.description) %>

    -
    -
    -
    Pôvod údajov o projekte
    -
      - <% [['icons/sd_logo.png', 'Ručne vyplnený údaj od Slovensko.Digital'], ['icons/ai_logo.png', 'Údaj spracovaný našim AI extraktorom'], ['icons/metais_logo.png', 'Údaj pochádzajúci z MetaIS portálu']].each do |logo_path, tooltip_text| %> -
    • - <%= image_tag logo_path, style: "width: 16px; height: 16px; margin-right: 8px;" %> - <%= tooltip_text %> -
    • - <% end %> -
    -
    -
    - -
    -
    diff --git a/app/views/static/index.html.erb b/app/views/static/index.html.erb index a062291..36f4756 100644 --- a/app/views/static/index.html.erb +++ b/app/views/static/index.html.erb @@ -16,9 +16,6 @@ Štátne IT projekty
    nemusia byť len zlé
    -

    - Novinka! -

    Moderný a jednoduchý prehľad k štátnym IT projektom je tu. Zabudnite na hodiny strávené nad zložitou a neprehľadnou dokumentáciou.

    From e33840492022a268b00a49b86057b47a28608a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Fri, 4 Oct 2024 12:20:26 +0200 Subject: [PATCH 26/39] Rake tasks execution time moved, linking of projects through metais code fixed --- .../link_metais_projects_and_evaluations_job.rb | 16 +++++++++++----- app/jobs/sync_all_topics_job.rb | 2 +- config/clock.rb | 7 ++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/jobs/link_metais_projects_and_evaluations_job.rb b/app/jobs/link_metais_projects_and_evaluations_job.rb index 0d46caa..77a13fe 100644 --- a/app/jobs/link_metais_projects_and_evaluations_job.rb +++ b/app/jobs/link_metais_projects_and_evaluations_job.rb @@ -3,12 +3,18 @@ class LinkMetaisProjectsAndEvaluationsJob < ApplicationJob def perform Project.find_each do |project| - code = project.metais_code - metais_project = Metais::Project.find_by(code: code) + link_metais_project(project) + end + end + + private + + def link_metais_project(project) + code = project.metais_code + metais_project = Metais::Project.find_by(code: code) - if metais_project.present? - project.metais_projects << metais_project - end + if metais_project.present? && !project.metais_projects.exists?(metais_project.id) + project.metais_projects << metais_project end end end \ No newline at end of file diff --git a/app/jobs/sync_all_topics_job.rb b/app/jobs/sync_all_topics_job.rb index 26ea1a0..895806b 100644 --- a/app/jobs/sync_all_topics_job.rb +++ b/app/jobs/sync_all_topics_job.rb @@ -21,9 +21,9 @@ def find_indices(header_row) end def process_row(row, indices, sync_all) - project_metais_code = row[indices["MetaIS"]] project_name = row[indices["Projekt"]] project_id = row[indices["Projekt ID"]] + project_metais_code = row[indices["MetaIS"]] platform_link = row[indices["Platforma"]] preparation_document_id = row[indices["ID draft prípravy"]] preparation_page_id = row[indices["ID prípravy"]] diff --git a/config/clock.rb b/config/clock.rb index c050ec5..1b83ce9 100644 --- a/config/clock.rb +++ b/config/clock.rb @@ -12,8 +12,9 @@ module Clockwork Rake::Task[job].invoke end - every(1.day, 'redflags:sync', at: '9:00') + every(1.day, 'redflags:sync_google_drafts', at: '3:00') + every(1.day, 'redflags:sync_sheets', at: '4:00') - every(1.day, 'metais:daily_sync', at: '10:00') - every(1.day, 'metais:daily_sync_evaluations', at: '11:00') + every(1.day, 'metais:daily_sync', at: '5:00') + every(1.day, 'metais:daily_sync_evaluations', at: '6:00') end From 7e0473a6509a17cd22206e62600c91cf3fee211a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Fri, 4 Oct 2024 14:08:13 +0200 Subject: [PATCH 27/39] Separated the delete logic from data extraction result job, modified tests --- .../project_data_extraction_delete_job.rb | 21 +++++++++ .../project_data_extraction_result_job.rb | 14 +----- ...project_data_extraction_delete_job_spec.rb | 47 +++++++++++++++++++ ...project_data_extraction_result_job_spec.rb | 6 +-- 4 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 app/jobs/metais/project_data_extraction_delete_job.rb create mode 100644 spec/jobs/metais/project_data_extraction_delete_job_spec.rb diff --git a/app/jobs/metais/project_data_extraction_delete_job.rb b/app/jobs/metais/project_data_extraction_delete_job.rb new file mode 100644 index 0000000..3461df8 --- /dev/null +++ b/app/jobs/metais/project_data_extraction_delete_job.rb @@ -0,0 +1,21 @@ +require 'net/http' +require 'uri' + +class Metais::ProjectDataExtractionDeleteJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid) + url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}" + uri = URI(url) + + req = Net::HTTP::Delete.new(uri) + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| + http.request(req) + end + + unless res.is_a?(Net::HTTPSuccess) + error_message = "Failed to delete project: #{res.code}, body: #{res.body}" + raise RuntimeError, error_message + end + end +end diff --git a/app/jobs/metais/project_data_extraction_result_job.rb b/app/jobs/metais/project_data_extraction_result_job.rb index d1dec96..a8f8455 100644 --- a/app/jobs/metais/project_data_extraction_result_job.rb +++ b/app/jobs/metais/project_data_extraction_result_job.rb @@ -21,7 +21,7 @@ def perform(project_uuid, location_header) update_project_origin(project_origin, result) process_harmonogram(result['harmonogram'], project_origin) - send_delete_request(URI(url)) + Metais::ProjectDataExtractionDeleteJob.perform_later(project_uuid) end private @@ -105,16 +105,4 @@ def parse_event_date(start_date) rescue ArgumentError => e raise RuntimeError, e end - - def send_delete_request(uri) - req = Net::HTTP::Delete.new(uri) - res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| - http.request(req) - end - - unless res.is_a?(Net::HTTPSuccess) - error_message = "Failed to delete project: #{res.code}, body: #{res.body}" - raise RuntimeError, error_message - end - end end diff --git a/spec/jobs/metais/project_data_extraction_delete_job_spec.rb b/spec/jobs/metais/project_data_extraction_delete_job_spec.rb new file mode 100644 index 0000000..d3e8f81 --- /dev/null +++ b/spec/jobs/metais/project_data_extraction_delete_job_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' +require 'net/http' + +RSpec.describe Metais::ProjectDataExtractionDeleteJob, type: :job do + include ActiveJob::TestHelper + + let(:project_uuid) { 'sample-uuid' } + let(:api_url) { 'http://example.com/api' } + let(:url) { "#{api_url}/projects/#{project_uuid}" } + let(:uri) { URI(url) } + let(:response) { instance_double('Net::HTTPResponse') } + + before do + allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + allow(Net::HTTP).to receive(:start).and_return(response) + end + + describe '#perform' do + context 'when the delete request is successful' do + before do + allow(response).to receive(:is_a?).with(Net::HTTPSuccess).and_return(true) + end + + it 'sends a delete request' do + expect { + described_class.perform_now(project_uuid) + }.not_to raise_error + + expect(Net::HTTP).to have_received(:start).with(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') + end + end + + context 'when the delete request fails' do + before do + allow(response).to receive(:is_a?).with(Net::HTTPSuccess).and_return(false) + allow(response).to receive(:code).and_return('500') + allow(response).to receive(:body).and_return('Internal Server Error') + end + + it 'raises a RuntimeError with the correct message' do + expect { + described_class.perform_now(project_uuid) + }.to raise_error(RuntimeError, /Failed to delete project: 500, body: Internal Server Error/) + end + end + end +end diff --git a/spec/jobs/metais/project_data_extraction_result_job_spec.rb b/spec/jobs/metais/project_data_extraction_result_job_spec.rb index f6b9c37..5b83902 100644 --- a/spec/jobs/metais/project_data_extraction_result_job_spec.rb +++ b/spec/jobs/metais/project_data_extraction_result_job_spec.rb @@ -67,16 +67,12 @@ allow(Metais::ProjectEventType).to receive(:find_by).with(name: 'Predpoklad').and_return(event_type) allow(Metais::ProjectEvent).to receive(:find_or_initialize_by).and_return(double('Metais::ProjectEvent', save!: true)) - - allow(Net::HTTP).to receive(:start).and_return(double('Net::HTTPResponse', is_a?: true, code: '200')) end it 'successfully processes the result and sends a delete request' do expect { described_class.perform_now(project_uuid, location_header) - }.not_to have_enqueued_job - - expect(Net::HTTP).to have_received(:start) + }.to have_enqueued_job(Metais::ProjectDataExtractionDeleteJob).with(project_uuid) end end From b2e270e07fc193a2931cbd19da055d1f1598d0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Mon, 7 Oct 2024 10:57:47 +0200 Subject: [PATCH 28/39] Update app/jobs/link_metais_projects_and_evaluations_job.rb --- app/jobs/link_metais_projects_and_evaluations_job.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/jobs/link_metais_projects_and_evaluations_job.rb b/app/jobs/link_metais_projects_and_evaluations_job.rb index 77a13fe..4d1e355 100644 --- a/app/jobs/link_metais_projects_and_evaluations_job.rb +++ b/app/jobs/link_metais_projects_and_evaluations_job.rb @@ -12,8 +12,9 @@ def perform def link_metais_project(project) code = project.metais_code metais_project = Metais::Project.find_by(code: code) + return unless metais_project - if metais_project.present? && !project.metais_projects.exists?(metais_project.id) + unless project.metais_projects.exists?(metais_project.id) project.metais_projects << metais_project end end From c2cbf41e102cd018cf693e17a43036e56614373f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Mon, 7 Oct 2024 10:59:49 +0200 Subject: [PATCH 29/39] grammar fix --- app/helpers/metais/projects_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/metais/projects_helper.rb b/app/helpers/metais/projects_helper.rb index 2beba6e..280499c 100644 --- a/app/helpers/metais/projects_helper.rb +++ b/app/helpers/metais/projects_helper.rb @@ -5,7 +5,7 @@ def origin_type_logo(origin_type) when 3 ['icons/sd_logo.png', 'Ručne vyplnený údaj od Slovensko.Digital'] when 2 - ['icons/ai_logo.png', 'Údaj spracovaný našim AI extraktorom'] + ['icons/ai_logo.png', 'Údaj spracovaný naším AI extraktorom'] else ['icons/metais_logo.png', 'Údaj pochádzajúci z MetaIS portálu'] end @@ -14,7 +14,7 @@ def origin_type_logo(origin_type) when 'Human' ['icons/sd_logo.png', 'Ručne vyplnený údaj od Slovensko.Digital'] when 'AI' - ['icons/ai_logo.png', 'Údaj spracovaný našim AI extraktorom'] + ['icons/ai_logo.png', 'Údaj spracovaný naším AI extraktorom'] else ['icons/metais_logo.png', 'Údaj pochádzajúci z MetaIS portálu'] end From 56ff094a448fc3a460ce00f124c5f460b9734ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Mon, 7 Oct 2024 11:17:20 +0200 Subject: [PATCH 30/39] simplify sorting --- app/models/metais/project.rb | 35 ++++---- app/models/project.rb | 109 ++++++++++------------- app/views/metais/projects/index.html.erb | 2 +- app/views/projects/index.html.erb | 2 +- 4 files changed, 64 insertions(+), 84 deletions(-) diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index 6f9cc45..5c6c3c1 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -26,7 +26,7 @@ def evaluations def get_project_origin_info @project_origin_info ||= load_project_origin_info end - + def self.evaluation_counts total_count = Metais::Project.count yes_count = Metais::Project @@ -34,17 +34,17 @@ def self.evaluation_counts .distinct .count('metais.projects.id') no_count = total_count - yes_count - + { yes: yes_count, no: no_count } end - + def self.filtered_and_sorted_projects(params) per_page = 25 page = params[:page] || 1 ordered_project_origins = Metais::ProjectOrigin .select('project_id, - COALESCE(NULLIF(max(approved_investment) FILTER (WHERE approved_investment IS NOT NULL), 0), + COALESCE(NULLIF(max(approved_investment) FILTER (WHERE approved_investment IS NOT NULL), 0), NULLIF(max(investment) FILTER (WHERE investment IS NOT NULL), 0)) AS final_investment, max(title) FILTER (WHERE title IS NOT NULL) AS final_title, max(status) FILTER (WHERE status IS NOT NULL) AS final_status, @@ -68,21 +68,16 @@ def self.filtered_and_sorted_projects(params) projects = Metais::Project.where(id: projects) end - if params[:sort].present? - sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' - - projects = case params[:sort] - when 'alpha' - projects.order("project_origins.final_title #{sort_direction}") - when 'date' - projects.order("metais.projects.updated_at #{sort_direction}") - when 'price' - projects.order("project_origins.final_investment #{sort_direction} NULLS #{sort_direction == 'ASC' ? 'FIRST' : 'LAST'}") - end - else - sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' - projects.order("metais.projects.updated_at #{sort_direction}") - end + params[:sort] = 'date' unless params[:sort].present? + sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' + projects = case params[:sort] + when 'alpha' + projects.order("project_origins.final_title #{sort_direction}") + when 'date' + projects.order("metais.projects.updated_at #{sort_direction}") + when 'price' + projects.order("project_origins.final_investment #{sort_direction} NULLS #{sort_direction == 'ASC' ? 'FIRST' : 'LAST'}") + end projects.page(page).per(per_page) end @@ -100,7 +95,7 @@ def load_project_origin_info fields.each do |field| origin = origins.detect { |origin| !origin.send(field).nil? } value = origin&.send(field) - + if field == 'finance_source' && value value = FINANCE_SOURCE_MAPPINGS[value] || value end diff --git a/app/models/project.rb b/app/models/project.rb index 3d9d509..b35e2c4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -17,80 +17,65 @@ class Project < ApplicationRecord def has_published_phases? phases.any? { |phase| phase.published_revision.present? } end - + def self.filtered_and_sorted_projects(params) per_page = 25 page = params[:page] || 1 - + projects = Project.joins(phases: :published_revision) if params[:title].present? projects = projects.where('phase_revisions.title ILIKE ?', "%#{params[:title]}%") end - - if params[:sort].present? - sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' - projects = case params[:sort] - when 'alpha' - projects = projects.select('DISTINCT ON (projects.id) projects.*, LOWER(phase_revisions.title) AS alpha_title') - .order("projects.id, alpha_title") - projects = projects.sort_by(&:alpha_title) - projects = projects.reverse if sort_direction == 'DESC' - projects - when 'date' - if sort_direction == 'ASC' - projects = projects.select("projects.*, MIN(phase_revisions.published_at) AS oldest_published_at").group("projects.id") - .order("oldest_published_at") - elsif sort_direction == 'DESC' - projects = projects.select('projects.*, MAX(phase_revisions.published_at) AS newest_published_at').group('projects.id') - .order('newest_published_at DESC') - end - when 'preparation' - if sort_direction == 'ASC' - projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Prípravná fáza') }).distinct - projects = projects.sort_by do |project| - phase_prep = project.phases.find { |p| p.phase_type.name == 'Prípravná fáza' } - phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 - end.reverse - elsif sort_direction == 'DESC' - projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Prípravná fáza') }).distinct - projects = projects.sort_by do |project| - phase_prep = project.phases.find { |p| p.phase_type.name == 'Prípravná fáza' } - phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 - end + params[:sort] = 'date' unless params[:sort].present? + sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' + projects = case params[:sort] + when 'alpha' + projects = projects.select('DISTINCT ON (projects.id) projects.*, LOWER(phase_revisions.title) AS alpha_title') + .order("projects.id, alpha_title") + projects = projects.sort_by(&:alpha_title) + projects = projects.reverse if sort_direction == 'DESC' + projects + when 'date' + if sort_direction == 'ASC' + projects = projects.select("projects.*, MIN(phase_revisions.published_at) AS oldest_published_at").group("projects.id") + .order("oldest_published_at") + elsif sort_direction == 'DESC' + projects = projects.select('projects.*, MAX(phase_revisions.published_at) AS newest_published_at').group('projects.id') + .order('newest_published_at DESC') + end + when 'preparation' + if sort_direction == 'ASC' + projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Prípravná fáza') }).distinct + projects = projects.sort_by do |project| + phase_prep = project.phases.find { |p| p.phase_type.name == 'Prípravná fáza' } + phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 + end.reverse + elsif sort_direction == 'DESC' + projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Prípravná fáza') }).distinct + projects = projects.sort_by do |project| + phase_prep = project.phases.find { |p| p.phase_type.name == 'Prípravná fáza' } + phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 end - when 'product' - if sort_direction == 'ASC' - projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Fáza produkt') }).distinct - projects = projects.sort_by do |project| - phase_prep = project.phases.find { |p| p.phase_type.name == 'Fáza produkt' } - phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 - end.reverse - - elsif sort_direction == 'DESC' - projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Fáza produkt') }).distinct - projects = projects.sort_by do |project| - phase_prep = project.phases.find { |p| p.phase_type.name == 'Fáza produkt' } - phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 - end + end + when 'product' + if sort_direction == 'ASC' + projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Fáza produkt') }).distinct + projects = projects.sort_by do |project| + phase_prep = project.phases.find { |p| p.phase_type.name == 'Fáza produkt' } + phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 + end.reverse + + elsif sort_direction == 'DESC' + projects = projects.where(phases: { phase_type: PhaseType.find_by(name: 'Fáza produkt') }).distinct + projects = projects.sort_by do |project| + phase_prep = project.phases.find { |p| p.phase_type.name == 'Fáza produkt' } + phase_prep ? (phase_prep.published_revision.aggregated_rating) : 0 end end - else - sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' - if sort_direction == 'DESC' - projects = projects.sort_by do |project| - max_rating = project.phases.map { |p| p.published_revision&.aggregated_rating }.compact.max - max_rating - end - else - projects = projects.sort_by do |project| - max_rating = project.phases.map { |p| p.published_revision&.aggregated_rating }.compact.max - max_rating - end.reverse - end - end - + end + projects = Kaminari.paginate_array(projects).page(page).per(per_page) projects end diff --git a/app/views/metais/projects/index.html.erb b/app/views/metais/projects/index.html.erb index 90fa867..4ccf8b2 100644 --- a/app/views/metais/projects/index.html.erb +++ b/app/views/metais/projects/index.html.erb @@ -30,7 +30,7 @@
    <% sort_options = { - '' => 'Odporúčané', + '' => 'Podľa dátumu', 'alpha' => 'Abecedne', 'date' => 'Podľa dátumu', 'price' => 'Podľa ceny'} %> diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 35498fc..05a0d85 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -17,7 +17,7 @@
    <% sort_options = { - '' => 'Odporúčané', + '' => 'Podľa dátumu', 'alpha' => 'Abecedne', 'date' => 'Podľa dátumu', 'preparation' => 'Hodnotenie prípravy', From 74ace443a2df7b34c5435a57fdad34d04b9e6028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Wed, 9 Oct 2024 18:25:11 +0200 Subject: [PATCH 31/39] add AI button and fix AI result handle empty --- app/controllers/admin/metais/project_origins_controller.rb | 7 +++---- app/controllers/admin/metais/projects_controller.rb | 4 ++-- app/jobs/metais/project_data_extraction_result_job.rb | 2 ++ app/models/metais/project.rb | 4 ++++ app/views/admin/metais/project_origins/edit.html.erb | 2 +- app/views/admin/metais/projects/index.html.erb | 5 +++++ 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/controllers/admin/metais/project_origins_controller.rb b/app/controllers/admin/metais/project_origins_controller.rb index 312f42f..fc6351e 100644 --- a/app/controllers/admin/metais/project_origins_controller.rb +++ b/app/controllers/admin/metais/project_origins_controller.rb @@ -15,7 +15,6 @@ def edit @project_origins = @project.project_origins @project_origin = @project_origins.find(params[:id]) - @ai_project_origin = @project_origins.joins(:origin_type).find_by(origin_types: { name: 'AI' }) @assumption_events = @project_origins.flat_map { |project_origin| project_origin.events.assumpted } @real_events = @project_origins.flat_map { |project_origin| project_origin.events.real } @@ -127,10 +126,10 @@ def remove_document redirect_to edit_admin_metais_project_project_origin_path(@project, @project_origin), alert: 'Nastala chyba pri odstraňovaní dokumentu.' end end - + def update_group_order @project_origin = @project.project_origins.find(params[:project_origin_id]) - + description = params[:description] group_order = params[:group_order].to_i @@ -200,4 +199,4 @@ def detect_changes(current_project_info, new_params) changed_params end -end \ No newline at end of file +end diff --git a/app/controllers/admin/metais/projects_controller.rb b/app/controllers/admin/metais/projects_controller.rb index aa4647b..db8968c 100644 --- a/app/controllers/admin/metais/projects_controller.rb +++ b/app/controllers/admin/metais/projects_controller.rb @@ -5,7 +5,7 @@ def index @status_counts = Metais::ProjectOrigin.status_counts @unique_statuses = Metais::ProjectOrigin.unique_statuses @evaluation_counts = Metais::Project.evaluation_counts - + @projects = Metais::Project.filtered_and_sorted_projects(params) end @@ -26,6 +26,6 @@ def run_ai_extraction @project = Metais::Project.find(params[:id]) Metais::ProjectDataExtractionJob.perform_later(@project.uuid) - redirect_to admin_metais_project_path @project + redirect_to admin_metais_project_path @project, notice: 'Projekt bol zaradený na spracovanie.' end end diff --git a/app/jobs/metais/project_data_extraction_result_job.rb b/app/jobs/metais/project_data_extraction_result_job.rb index a8f8455..5120899 100644 --- a/app/jobs/metais/project_data_extraction_result_job.rb +++ b/app/jobs/metais/project_data_extraction_result_job.rb @@ -15,6 +15,8 @@ def perform(project_uuid, location_header) raise RuntimeError.new('Result status is not "Done"') unless body['status'] == 'Done' result = body['result'] + return if result['detail'] == 'No documents' + metais_project = find_metais_project(project_uuid) project_origin = find_or_initialize_project_origin(metais_project) diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index 5c6c3c1..58425bb 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -27,6 +27,10 @@ def get_project_origin_info @project_origin_info ||= load_project_origin_info end + def get_ai_project_origin + @ai_project_origin ||= project_origins.joins(:origin_type).find_by(origin_types: { name: 'AI' }) + end + def self.evaluation_counts total_count = Metais::Project.count yes_count = Metais::Project diff --git a/app/views/admin/metais/project_origins/edit.html.erb b/app/views/admin/metais/project_origins/edit.html.erb index 2cf511a..a1c9c97 100644 --- a/app/views/admin/metais/project_origins/edit.html.erb +++ b/app/views/admin/metais/project_origins/edit.html.erb @@ -3,7 +3,7 @@
    + @@ -233,8 +234,12 @@ + <% end %> From f59eadbd64d160aebd283805b1a1462c76f7b323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Wed, 9 Oct 2024 18:33:43 +0200 Subject: [PATCH 32/39] handle no project plan in ai --- app/jobs/metais/project_data_extraction_result_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/metais/project_data_extraction_result_job.rb b/app/jobs/metais/project_data_extraction_result_job.rb index 5120899..d04935f 100644 --- a/app/jobs/metais/project_data_extraction_result_job.rb +++ b/app/jobs/metais/project_data_extraction_result_job.rb @@ -15,7 +15,7 @@ def perform(project_uuid, location_header) raise RuntimeError.new('Result status is not "Done"') unless body['status'] == 'Done' result = body['result'] - return if result['detail'] == 'No documents' + return if ['No documents', 'No documents for project plan'].include? result['detail'] metais_project = find_metais_project(project_uuid) project_origin = find_or_initialize_project_origin(metais_project) From d20b654f9d4493a8504f6492b5ca046ac8de1268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Wed, 9 Oct 2024 18:37:54 +0200 Subject: [PATCH 33/39] handle file deletion if even ai docs not found --- app/jobs/metais/project_data_extraction_result_job.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/jobs/metais/project_data_extraction_result_job.rb b/app/jobs/metais/project_data_extraction_result_job.rb index d04935f..d3c3944 100644 --- a/app/jobs/metais/project_data_extraction_result_job.rb +++ b/app/jobs/metais/project_data_extraction_result_job.rb @@ -15,13 +15,14 @@ def perform(project_uuid, location_header) raise RuntimeError.new('Result status is not "Done"') unless body['status'] == 'Done' result = body['result'] - return if ['No documents', 'No documents for project plan'].include? result['detail'] - metais_project = find_metais_project(project_uuid) - project_origin = find_or_initialize_project_origin(metais_project) + unless ['No documents', 'No documents for project plan'].include? result['detail'] + metais_project = find_metais_project(project_uuid) + project_origin = find_or_initialize_project_origin(metais_project) - update_project_origin(project_origin, result) - process_harmonogram(result['harmonogram'], project_origin) + update_project_origin(project_origin, result) + process_harmonogram(result['harmonogram'], project_origin) + end Metais::ProjectDataExtractionDeleteJob.perform_later(project_uuid) end From 5528eb2b28e4e76dc2a4b690013e5a7e76656f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= Date: Wed, 9 Oct 2024 18:40:58 +0200 Subject: [PATCH 34/39] fix redirect after ai start post --- app/controllers/admin/metais/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/metais/projects_controller.rb b/app/controllers/admin/metais/projects_controller.rb index db8968c..d40a9b0 100644 --- a/app/controllers/admin/metais/projects_controller.rb +++ b/app/controllers/admin/metais/projects_controller.rb @@ -26,6 +26,6 @@ def run_ai_extraction @project = Metais::Project.find(params[:id]) Metais::ProjectDataExtractionJob.perform_later(@project.uuid) - redirect_to admin_metais_project_path @project, notice: 'Projekt bol zaradený na spracovanie.' + redirect_to admin_metais_projects_path, notice: 'Projekt bol zaradený na spracovanie.' end end From 9b049b23b2c88bd969c74021c8db2a4385c12014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Thu, 10 Oct 2024 09:54:56 +0200 Subject: [PATCH 35/39] Sort direction field change fixed --- app/assets/javascripts/projects.js | 46 +++++++++++++++--------------- app/models/metais/project.rb | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/projects.js b/app/assets/javascripts/projects.js index 9a8b634..36e1c25 100644 --- a/app/assets/javascripts/projects.js +++ b/app/assets/javascripts/projects.js @@ -14,36 +14,36 @@ document.addEventListener("turbolinks:before-render", function() { window.printCalled = false; }); -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('turbolinks:load', function () { var sortDirectionField = document.getElementById('sort_direction'); var sortButton = document.getElementById('ascdesctoggle'); var upIcon = document.getElementById('up-icon'); var downIcon = document.getElementById('down-icon'); - var toggleState = sortDirectionField.value || 'desc'; + if (sortDirectionField && sortButton && upIcon && downIcon) { + var toggleState = sortDirectionField.value || 'desc'; + + function updateIcons() { + if (toggleState === 'desc') { + upIcon.style.fill = 'rgba(100, 100, 100, 0.4)'; + downIcon.style.fill = 'rgb(56, 94, 255)'; + } else { + upIcon.style.fill = 'rgb(56, 94, 255)'; + downIcon.style.fill = 'rgba(100, 100, 100, 0.4)'; + } + } - if (toggleState === 'desc') { - upIcon.style.fill = 'rgb(100, 100, 100, 0.4)'; - downIcon.style.fill = 'rgb(56, 94, 255)'; - } else { - upIcon.style.fill = 'rgb(56, 94, 255)'; - downIcon.style.fill = 'rgb(100, 100, 100, 0.4)'; - } + updateIcons(); - sortButton.addEventListener('click', function(event) { - event.preventDefault(); + sortButton.addEventListener('click', function (event) { + event.preventDefault(); - toggleState = (toggleState === 'asc') ? 'desc' : 'asc'; - sortDirectionField.value = toggleState; + toggleState = (toggleState === 'desc') ? 'asc' : 'desc'; + sortDirectionField.value = toggleState; - if (toggleState === 'desc') { - upIcon.style.fill = 'rgb(100, 100, 100, 0.4)'; - downIcon.style.fill = 'rgb(56, 94, 255)'; - } else { - upIcon.style.fill = 'rgb(56, 94, 255)'; - downIcon.style.fill = 'rgb(100, 100, 100, 0.4)'; - } + updateIcons(); - document.getElementById('form').submit(); - }); -}); + document.getElementById('form').submit(); + }); + } +}); \ No newline at end of file diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index 6f9cc45..d26c9fd 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -73,7 +73,7 @@ def self.filtered_and_sorted_projects(params) projects = case params[:sort] when 'alpha' - projects.order("project_origins.final_title #{sort_direction}") + projects.order("LOWER(project_origins.final_title) #{sort_direction}") when 'date' projects.order("metais.projects.updated_at #{sort_direction}") when 'price' From bcd551cdbbd0d4bfa25e87c9401e470a7883e7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Thu, 10 Oct 2024 10:04:54 +0200 Subject: [PATCH 36/39] Resolved conflict in metais/project.rb --- app/models/metais/project.rb | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index d59491a..a7725ff 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -72,34 +72,16 @@ def self.filtered_and_sorted_projects(params) projects = Metais::Project.where(id: projects) end -<<<<<<< HEAD - if params[:sort].present? - sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' - - projects = case params[:sort] - when 'alpha' - projects.order("LOWER(project_origins.final_title) #{sort_direction}") - when 'date' - projects.order("metais.projects.updated_at #{sort_direction}") - when 'price' - projects.order("project_origins.final_investment #{sort_direction} NULLS #{sort_direction == 'ASC' ? 'FIRST' : 'LAST'}") - end - else - sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' - projects.order("metais.projects.updated_at #{sort_direction}") - end -======= params[:sort] = 'date' unless params[:sort].present? sort_direction = params[:sort_direction]&.upcase == 'ASC' ? 'ASC' : 'DESC' projects = case params[:sort] when 'alpha' - projects.order("project_origins.final_title #{sort_direction}") + projects.order("LOWER(project_origins.final_title) #{sort_direction}") when 'date' projects.order("metais.projects.updated_at #{sort_direction}") when 'price' projects.order("project_origins.final_investment #{sort_direction} NULLS #{sort_direction == 'ASC' ? 'FIRST' : 'LAST'}") end ->>>>>>> 5528eb2b28e4e76dc2a4b690013e5a7e76656f3d projects.page(page).per(per_page) end From d8834a0cc39f7150d1a142a0f6714cd8fb153374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Fri, 31 Jan 2025 14:03:27 +0100 Subject: [PATCH 37/39] Data extraction jobs modified for new extractor --- .../metais/project_data_extraction_job.rb | 13 ++- .../project_data_extraction_result_job.rb | 13 ++- .../project_data_extraction_status_job.rb | 2 +- .../project_document_detection_delete_job.rb | 21 ++++ .../metais/project_document_detection_job.rb | 33 ++++++ .../project_document_detection_result_job.rb | 41 +++++++ .../project_document_detection_status_job.rb | 17 +++ app/jobs/sync_all_topics_job.rb | 10 +- app/jobs/sync_one_topic_job.rb | 2 +- app/models/metais/project.rb | 32 +++++ app/models/phase_revision.rb | 16 +++ app/views/phase_revision/pdf.html.erb | 2 +- app/views/phase_revision/show.html.erb | 2 +- .../project_data_extraction_job_spec.rb | 109 +++++++++++++----- ...ject_document_detection_delete_job_spec.rb | 51 ++++++++ .../project_document_detection_job_spec.rb | 67 +++++++++++ ...ject_document_detection_result_job_spec.rb | 89 ++++++++++++++ ...ject_document_detection_status_job_spec.rb | 62 ++++++++++ spec/jobs/sync_all_topics_job_spec.rb | 23 ++-- spec/jobs/sync_one_topic_job_spec.rb | 2 +- 20 files changed, 542 insertions(+), 65 deletions(-) create mode 100644 app/jobs/metais/project_document_detection_delete_job.rb create mode 100644 app/jobs/metais/project_document_detection_job.rb create mode 100644 app/jobs/metais/project_document_detection_result_job.rb create mode 100644 app/jobs/metais/project_document_detection_status_job.rb create mode 100644 spec/jobs/metais/project_document_detection_delete_job_spec.rb create mode 100644 spec/jobs/metais/project_document_detection_job_spec.rb create mode 100644 spec/jobs/metais/project_document_detection_result_job_spec.rb create mode 100644 spec/jobs/metais/project_document_detection_status_job_spec.rb diff --git a/app/jobs/metais/project_data_extraction_job.rb b/app/jobs/metais/project_data_extraction_job.rb index 227c3c9..395546e 100644 --- a/app/jobs/metais/project_data_extraction_job.rb +++ b/app/jobs/metais/project_data_extraction_job.rb @@ -4,9 +4,18 @@ class Metais::ProjectDataExtractionJob < ApplicationJob queue_as :metais_data_extraction - def perform(project_uuid) + def perform(project_uuid, document_uuid) + document = Datahub::Metais::ProjectDocument.find_by(uuid: document_uuid) + url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}" - response = Net::HTTP.post(URI(url), '') + uri = URI.parse(url) + uri.query = URI.encode_www_form({ document_uuid: document_uuid }) + http = Net::HTTP.new(uri.host, uri.port) + + request = Net::HTTP::Post.new(uri, { 'Content-Type' => 'application/json' }) + request.body = { filename: document.latest_version.filename, created_at: document.latest_version.metais_created_at }.to_json + + response = http.request(request) unless response.is_a?(Net::HTTPAccepted) error_message = "Response status is #{response.code}. Message: #{response.body}" diff --git a/app/jobs/metais/project_data_extraction_result_job.rb b/app/jobs/metais/project_data_extraction_result_job.rb index d3c3944..58ab013 100644 --- a/app/jobs/metais/project_data_extraction_result_job.rb +++ b/app/jobs/metais/project_data_extraction_result_job.rb @@ -6,7 +6,7 @@ class Metais::ProjectDataExtractionResultJob < ApplicationJob queue_as :metais_data_extraction def perform(project_uuid, location_header) - url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}" + url = "#{ENV.fetch('API_URL')}#{location_header}" response = Net::HTTP.get_response(URI(url)) handle_response_errors(response) @@ -15,6 +15,7 @@ def perform(project_uuid, location_header) raise RuntimeError.new('Result status is not "Done"') unless body['status'] == 'Done' result = body['result'] + result = parse_json(result) if result.is_a?(String) unless ['No documents', 'No documents for project plan'].include? result['detail'] metais_project = find_metais_project(project_uuid) @@ -62,16 +63,16 @@ def find_or_initialize_project_origin(metais_project) def update_project_origin(project_origin, result) project_origin.project_manager = "#{result['responsible']['first_name']} #{result['responsible']['surname']}" - project_origin.approved_investment = result['capex'] unless result['capex'].zero? - project_origin.approved_operation = result['opex'] unless result['opex'].zero? - project_origin.benefits = result['declared'] unless result['declared'].zero? + project_origin.approved_investment = result['capex'].to_i unless result['capex'].to_i.zero? + project_origin.approved_operation = result['opex'].to_i unless result['opex'].to_i.zero? + project_origin.benefits = result['declared'].to_i unless result['declared'].to_i.zero? project_origin.targets_text = format_targets_text(result) unless format_targets_text(result).empty? project_origin.save! end def format_targets_text(result) - kpis = result['kpis'].join("\n") unless result['kpis'].empty? - goals = result['goals'].join("\n") unless result['goals'].empty? + kpis = Array(result['kpis']).join("\n") unless result['kpis'].to_s.empty? + goals = Array(result['goals']).join("\n") unless result['goals'].to_s.empty? [kpis, goals].compact.join("\n") end diff --git a/app/jobs/metais/project_data_extraction_status_job.rb b/app/jobs/metais/project_data_extraction_status_job.rb index 0827951..c27ca74 100644 --- a/app/jobs/metais/project_data_extraction_status_job.rb +++ b/app/jobs/metais/project_data_extraction_status_job.rb @@ -2,7 +2,7 @@ class Metais::ProjectDataExtractionStatusJob < ApplicationJob queue_as :metais_data_extraction def perform(project_uuid, location_header) - url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}/status" + url = "#{ENV.fetch('API_URL')}#{location_header}" response = Net::HTTP.get_response(URI(url)) if response.key?('Retry-After') diff --git a/app/jobs/metais/project_document_detection_delete_job.rb b/app/jobs/metais/project_document_detection_delete_job.rb new file mode 100644 index 0000000..ecf7ccb --- /dev/null +++ b/app/jobs/metais/project_document_detection_delete_job.rb @@ -0,0 +1,21 @@ +require 'net/http' +require 'uri' + +class Metais::ProjectDocumentDetectionDeleteJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid) + url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}/documents" + uri = URI(url) + + req = Net::HTTP::Delete.new(uri) + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| + http.request(req) + end + + unless res.is_a?(Net::HTTPSuccess) + error_message = "Failed to delete project: #{res.code}, body: #{res.body}" + raise RuntimeError, error_message + end + end +end diff --git a/app/jobs/metais/project_document_detection_job.rb b/app/jobs/metais/project_document_detection_job.rb new file mode 100644 index 0000000..111afaa --- /dev/null +++ b/app/jobs/metais/project_document_detection_job.rb @@ -0,0 +1,33 @@ +require 'net/http' +require 'uri' + +class Metais::ProjectDocumentDetectionJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid) + project = Metais::Project.find_by(uuid: project_uuid) + documents = project.get_project_documents + + url = "#{ENV.fetch('API_URL')}/projects/#{project_uuid}/documents" + uri = URI.parse(url) + http = Net::HTTP.new(uri.host, uri.port) + + request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' }) + request.body = { documents: documents }.to_json + + response = http.request(request) + + unless response.is_a?(Net::HTTPAccepted) + error_message = "Response status is #{response.code}. Message: #{response.body}" + raise RuntimeError, error_message + end + + location = response['Location'] + if location.nil? + error_message = "Expected 'Location' header not found in the response." + raise RuntimeError, error_message + end + + Metais::ProjectDocumentDetectionStatusJob.perform_later(project_uuid, location) + end +end diff --git a/app/jobs/metais/project_document_detection_result_job.rb b/app/jobs/metais/project_document_detection_result_job.rb new file mode 100644 index 0000000..ff8ff35 --- /dev/null +++ b/app/jobs/metais/project_document_detection_result_job.rb @@ -0,0 +1,41 @@ +require 'net/http' +require 'uri' +require 'json' + +class Metais::ProjectDocumentDetectionResultJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid, location_header) + url = "#{ENV.fetch('API_URL')}#{location_header}" + response = Net::HTTP.get_response(URI(url)) + + handle_response_errors(response) + + body = parse_json(response.body) + raise RuntimeError.new('Result status is not "Done"') unless body['status'] == 'Done' + + result = body['result'] + + Metais::ProjectDocumentDetectionDeleteJob.perform_later(project_uuid) + + Metais::ProjectDataExtractionJob.perform_later(project_uuid, result) + end + + private + + def handle_response_errors(response) + case response.code.to_i + when 200, 202 + else + error_message = "Unexpected response status: #{response.code}, body: #{response.body}" + raise RuntimeError, error_message + end + end + + def parse_json(body) + JSON.parse(body) + rescue JSON::ParserError => e + error_message = "Failed to parse JSON response: #{e.message}" + raise RuntimeError, error_message + end +end diff --git a/app/jobs/metais/project_document_detection_status_job.rb b/app/jobs/metais/project_document_detection_status_job.rb new file mode 100644 index 0000000..45366c4 --- /dev/null +++ b/app/jobs/metais/project_document_detection_status_job.rb @@ -0,0 +1,17 @@ +class Metais::ProjectDocumentDetectionStatusJob < ApplicationJob + queue_as :metais_data_extraction + + def perform(project_uuid, location_header) + url = "#{ENV.fetch('API_URL')}#{location_header}" + response = Net::HTTP.get_response(URI(url)) + + if response.key?('Retry-After') + Metais::ProjectDocumentDetectionStatusJob.set(wait: response['Retry-After'].to_i.seconds).perform_later(project_uuid, location_header) + else + location = response['Location'] + raise "Location header missing in response" unless location + + Metais::ProjectDocumentDetectionResultJob.perform_later(project_uuid, location) + end + end +end diff --git a/app/jobs/sync_all_topics_job.rb b/app/jobs/sync_all_topics_job.rb index 895806b..a111851 100644 --- a/app/jobs/sync_all_topics_job.rb +++ b/app/jobs/sync_all_topics_job.rb @@ -1,7 +1,7 @@ class SyncAllTopicsJob < ApplicationJob queue_as :default - COLUMN_NAMES = ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'].freeze + COLUMN_NAMES = ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'].freeze def perform(sync_all: true) sheets_service = GoogleApiService.get_sheets_service @@ -23,20 +23,12 @@ def find_indices(header_row) def process_row(row, indices, sync_all) project_name = row[indices["Projekt"]] project_id = row[indices["Projekt ID"]] - project_metais_code = row[indices["MetaIS"]] platform_link = row[indices["Platforma"]] preparation_document_id = row[indices["ID draft prípravy"]] preparation_page_id = row[indices["ID prípravy"]] product_document_id = row[indices["ID draft produktu"]] product_page_id = row[indices["ID produktu"]] - - if project_metais_code.present? - project = Project.find_by(id: project_id) - project.metais_code = project_metais_code - project.save! - end - if sync_all process_row_for_sync_all(project_name, project_id, platform_link, preparation_document_id, preparation_page_id, product_document_id, product_page_id) else diff --git a/app/jobs/sync_one_topic_job.rb b/app/jobs/sync_one_topic_job.rb index 362bf9a..e3d8709 100644 --- a/app/jobs/sync_one_topic_job.rb +++ b/app/jobs/sync_one_topic_job.rb @@ -34,7 +34,7 @@ def process_row(row, indices, target_id) product_page_id = row[indices["ID produktu"]] if target_id == preparation_page_id.to_i - if platform_link != '' + if platform_link == 'Platforma link' SyncTopicJob.perform_now(project_id, preparation_page_id) else enqueue_job_for_update("#{project_name} - Príprava", project_id, preparation_document_id, preparation_page_id, 'Prípravná fáza') diff --git a/app/models/metais/project.rb b/app/models/metais/project.rb index a7725ff..adaa678 100644 --- a/app/models/metais/project.rb +++ b/app/models/metais/project.rb @@ -31,6 +31,10 @@ def get_ai_project_origin @ai_project_origin ||= project_origins.joins(:origin_type).find_by(origin_types: { name: 'AI' }) end + def get_metais_project_origin + metais_project_origin ||= project_origins.joins(:origin_type).find_by(origin_types: { name: 'MetaIS' }) + end + def self.evaluation_counts total_count = Metais::Project.count yes_count = Metais::Project @@ -86,6 +90,34 @@ def self.filtered_and_sorted_projects(params) projects.page(page).per(per_page) end + def get_all_documents_for_project + get_metais_project_origin&.documents || [] + end + + def get_latest_version_for_document(document_uuid) + Datahub::Metais::ProjectDocumentVersion + .joins(:document) # Joins via the :document association + .where(project_documents: { uuid: document_uuid }) # References the correct table name + .order(created_at: :desc) + .first + end + + def get_project_documents + get_all_documents_for_project.map do |document| + version = get_latest_version_for_document(document.uuid) + next unless version && version['filename'] + + date_str = version['created_at'].strftime("%Y-%m-%dT%H:%M:%S") + name_with_date = "#{File.basename(version['filename'], '.*')}_#{date_str}#{File.extname(version['filename'])}" + + { + 'uuid' => document.uuid, + 'name' => name_with_date, + 'date' => version['created_at'] + } + end.compact + end + private def load_project_origin_info diff --git a/app/models/phase_revision.rb b/app/models/phase_revision.rb index 53bb001..a087a29 100644 --- a/app/models/phase_revision.rb +++ b/app/models/phase_revision.rb @@ -100,6 +100,8 @@ def load_metadata(summary) current_label = nil current_status_content = '' collecting = false + description_collecting = false + description_content = '' doc.search('h3, p, ul, li').each do |element| if element.name == 'h3' @@ -111,12 +113,22 @@ def load_metadata(summary) current_status_content = '' end + if description_collecting + assign_value(metadata_mapping["Stručný opis:"], description_content.strip) + description_collecting = false + description_content = '' + end + collecting = true if current_label == "Čo sa práve deje" + description_collecting = true if current_label == "Stručný opis" end if collecting && %w[p ul].include?(element.name) current_status_content += element.to_html + elsif description_collecting && %w[p ul li].include?(element.name) + description_content += element.to_html + elsif element.name == 'p' strong_element = element.at('strong') if strong_element @@ -139,6 +151,10 @@ def load_metadata(summary) if collecting && metadata_mapping.key?("Čo sa práve deje:") assign_value(metadata_mapping["Čo sa práve deje:"], current_status_content) end + + if description_collecting && metadata_mapping.key?("Stručný opis:") + assign_value(metadata_mapping["Stručný opis:"], description_content.strip) + end end def assign_value(attribute, value) diff --git a/app/views/phase_revision/pdf.html.erb b/app/views/phase_revision/pdf.html.erb index 373ea63..ae5917f 100644 --- a/app/views/phase_revision/pdf.html.erb +++ b/app/views/phase_revision/pdf.html.erb @@ -11,7 +11,7 @@ <% end %>

    <% end %> -

    <%= @phase_revision.description %>

    +

    <%= @phase_revision.description&.html_safe %>

    diff --git a/app/views/phase_revision/show.html.erb b/app/views/phase_revision/show.html.erb index 13150eb..af3be30 100644 --- a/app/views/phase_revision/show.html.erb +++ b/app/views/phase_revision/show.html.erb @@ -14,7 +14,7 @@ <% end %>

    <% end %> -

    <%= @phase_revision.description %>

    +

    <%= @phase_revision.description&.html_safe %>

    diff --git a/spec/jobs/metais/project_data_extraction_job_spec.rb b/spec/jobs/metais/project_data_extraction_job_spec.rb index 2f00510..0591d7b 100644 --- a/spec/jobs/metais/project_data_extraction_job_spec.rb +++ b/spec/jobs/metais/project_data_extraction_job_spec.rb @@ -1,59 +1,108 @@ require 'rails_helper' -require 'net/http' +require 'webmock/rspec' RSpec.describe Metais::ProjectDataExtractionJob, type: :job do - let(:project_uuid) { 'sample-uuid' } - let(:api_url) { 'http://example.com/api' } - let(:url) { "#{api_url}/projects/#{project_uuid}" } + include ActiveJob::TestHelper + + let(:project_uuid) { 'project-123' } + let(:document_uuid) { 'doc-456' } + let(:api_url) { 'http://api.example.com' } + let(:latest_version) do + double( + 'latest_version', + filename: 'test.pdf', + metais_created_at: Time.zone.parse('2023-01-01 12:00:00 UTC') + ) + end + let(:document) do + double( + 'Datahub::Metais::ProjectDocument', + latest_version: latest_version + ) + end before do - allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + ENV['API_URL'] = api_url + allow(Datahub::Metais::ProjectDocument) + .to receive(:find_by) + .with(uuid: document_uuid) + .and_return(document) end describe '#perform' do - let(:response) { instance_double(Net::HTTPResponse) } - - before do - allow(Net::HTTP).to receive(:post).and_return(response) - end + context 'when the API returns 202 Accepted with Location header' do + let(:location_header) { 'http://api.example.com/status/123' } - context 'when the response status is 202 Accepted' do before do - allow(response).to receive(:is_a?).with(Net::HTTPAccepted).and_return(true) - allow(response).to receive(:[]).with('Location').and_return('http://example.com/location') + stub_request(:post, "#{api_url}/projects/#{project_uuid}?document_uuid=#{document_uuid}") + .with( + headers: { 'Content-Type' => 'application/json' }, + body: { + filename: 'test.pdf', + created_at: '2023-01-01T12:00:00.000Z' + }.to_json + ) + .to_return( + status: 202, + headers: { 'Location' => location_header } + ) end - it 'enqueues the Metais::ProjectDataExtractionStatusJob with the correct parameters' do - expect(Metais::ProjectDataExtractionStatusJob).to receive(:perform_later).with(project_uuid, 'http://example.com/location') - subject.perform(project_uuid) + it 'enqueues ProjectDataExtractionStatusJob with location' do + described_class.perform_now(project_uuid, document_uuid) + + expect(Metais::ProjectDataExtractionStatusJob) + .to have_been_enqueued + .with(project_uuid, location_header) end end - context 'when the response status is not 202 Accepted' do + context 'when the API returns non-202 response' do before do - allow(response).to receive(:is_a?).with(Net::HTTPAccepted).and_return(false) - allow(response).to receive(:code).and_return('400') - allow(response).to receive(:body).and_return('Bad Request') + stub_request(:post, /.*/) + .to_return(status: 500, body: 'Internal Server Error') end - it 'raises a RuntimeError with the appropriate message' do + it 'raises an error with response details' do expect { - subject.perform(project_uuid) - }.to raise_error(RuntimeError, /Response status is 400. Message: Bad Request/) + described_class.perform_now(project_uuid, document_uuid) + }.to raise_error(RuntimeError, /Response status is 500. Message: Internal Server Error/) end end - context 'when the Location header is missing' do + context 'when the API returns 202 without Location header' do before do - allow(response).to receive(:is_a?).with(Net::HTTPAccepted).and_return(true) - allow(response).to receive(:[]).with('Location').and_return(nil) + stub_request(:post, /.*/) + .to_return(status: 202) end - it 'raises a RuntimeError indicating the missing Location header' do + it 'raises an error about missing Location header' do expect { - subject.perform(project_uuid) - }.to raise_error(RuntimeError, /Expected 'Location' header not found in the response./) + described_class.perform_now(project_uuid, document_uuid) + }.to raise_error(RuntimeError, /Expected 'Location' header not found/) + end + end + + context 'when building the request' do + let(:location_header) { 'http://api.example.com/status/123' } + + it 'sends correct JSON payload' do + stub_request(:post, "#{api_url}/projects/#{project_uuid}?document_uuid=#{document_uuid}") + .with( + body: { + filename: 'test.pdf', + created_at: '2023-01-01T12:00:00.000Z' + }.to_json + ) + .to_return(status: 202, headers: { 'Location' => location_header }) + + described_class.perform_now(project_uuid, document_uuid) + + expect(a_request(:post, /projects/).with(body: hash_including( + filename: 'test.pdf', + created_at: '2023-01-01T12:00:00.000Z' + ))).to have_been_made end end end -end +end \ No newline at end of file diff --git a/spec/jobs/metais/project_document_detection_delete_job_spec.rb b/spec/jobs/metais/project_document_detection_delete_job_spec.rb new file mode 100644 index 0000000..3fa399b --- /dev/null +++ b/spec/jobs/metais/project_document_detection_delete_job_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' +require 'webmock/rspec' + +RSpec.describe Metais::ProjectDocumentDetectionDeleteJob, type: :job do + let(:project_uuid) { 'test-project-uuid' } + let(:api_url) { 'http://api.example.com' } + let(:expected_url) { "#{api_url}/projects/#{project_uuid}/documents" } + + before do + allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + end + + describe '#perform' do + context 'when the API request succeeds' do + before do + stub_request(:delete, expected_url) + .to_return(status: 200, body: '') + end + + it 'sends a DELETE request to the correct endpoint' do + described_class.new.perform(project_uuid) + expect(a_request(:delete, expected_url)).to have_been_made.once + end + + it 'does not raise an error' do + expect { + described_class.new.perform(project_uuid) + }.not_to raise_error + end + end + + context 'when the API request fails' do + before do + stub_request(:delete, expected_url) + .to_return(status: 500, body: 'Server Exploded') + end + + it 'raises an error with the response details' do + expect { + described_class.new.perform(project_uuid) + }.to raise_error(RuntimeError, /Failed to delete project: 500, body: Server Exploded/) + end + end + end + + describe 'queue configuration' do + it 'uses the correct queue' do + expect(described_class.new.queue_name).to eq('metais_data_extraction') + end + end +end \ No newline at end of file diff --git a/spec/jobs/metais/project_document_detection_job_spec.rb b/spec/jobs/metais/project_document_detection_job_spec.rb new file mode 100644 index 0000000..a43b778 --- /dev/null +++ b/spec/jobs/metais/project_document_detection_job_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' +require 'webmock/rspec' + +RSpec.describe Metais::ProjectDocumentDetectionJob, type: :job do + let(:project_uuid) { 'test-uuid-123' } + let(:project) { instance_double(Metais::Project, uuid: project_uuid) } + let(:documents) { [{ title: 'Test Document' }] } + let(:api_url) { 'http://api.example.com' } + + before do + ENV['API_URL'] = api_url + allow(Metais::Project).to receive(:find_by).with(uuid: project_uuid).and_return(project) + allow(project).to receive(:get_project_documents).and_return(documents) + end + + describe '#perform' do + context 'when the API responds with 202 Accepted and Location header' do + let(:location_header) { 'http://api.example.com/status/123' } + + before do + stub_request(:post, "#{api_url}/projects/#{project_uuid}/documents") + .with( + body: { documents: documents }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + .to_return( + status: 202, + headers: { 'Location' => location_header }, + body: '{}' + ) + end + + it 'enqueues the ProjectDocumentDetectionStatusJob with the location' do + expect { + described_class.perform_now(project_uuid) + }.to have_enqueued_job(Metais::ProjectDocumentDetectionStatusJob) + .with(project_uuid, location_header) + end + end + + context 'when the API responds with a non-202 status' do + before do + stub_request(:post, "#{api_url}/projects/#{project_uuid}/documents") + .to_return(status: 500, body: 'Internal Server Error') + end + + it 'raises an error with the response details' do + expect { + described_class.perform_now(project_uuid) + }.to raise_error(RuntimeError, /Response status is 500. Message: Internal Server Error/) + end + end + + context 'when the API responds with 202 but no Location header' do + before do + stub_request(:post, "#{api_url}/projects/#{project_uuid}/documents") + .to_return(status: 202, body: '{}') + end + + it 'raises an error about the missing Location header' do + expect { + described_class.perform_now(project_uuid) + }.to raise_error(RuntimeError, /Expected 'Location' header not found/) + end + end + end +end \ No newline at end of file diff --git a/spec/jobs/metais/project_document_detection_result_job_spec.rb b/spec/jobs/metais/project_document_detection_result_job_spec.rb new file mode 100644 index 0000000..6397de7 --- /dev/null +++ b/spec/jobs/metais/project_document_detection_result_job_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' +require 'webmock/rspec' + +RSpec.describe Metais::ProjectDocumentDetectionResultJob, type: :job do + include ActiveJob::TestHelper + + let(:project_uuid) { 'project-123' } + let(:location_header) { '/document-detection/result' } + let(:api_url) { 'http://api.example.com/' } + let(:full_url) { "#{api_url}#{location_header}" } + + before do + allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + WebMock.reset! + end + + describe '#perform' do + context 'when HTTP response is unsuccessful' do + it 'raises an error for non-200/202 status codes' do + stub_request(:get, full_url).to_return(status: 500, body: 'Server Error') + + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error(RuntimeError, /Unexpected response status: 500/) + end + end + + context 'when response contains invalid JSON' do + it 'raises a JSON parsing error' do + stub_request(:get, full_url).to_return(status: 200, body: 'invalid json') + + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error(RuntimeError, /Failed to parse JSON response/) + end + end + + context 'when status is not "Done"' do + it 'raises an error' do + stub_request(:get, full_url) + .to_return(status: 200, body: { status: 'Processing' }.to_json) + + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error(RuntimeError, /Result status is not "Done"/) + end + end + + context 'when status is "Done"' do + let(:result) { { key: 'value' } } + + context 'with 200 OK response' do + before do + stub_request(:get, full_url) + .to_return(status: 200, body: { status: 'Done', result: result }.to_json) + end + + it 'enqueues ProjectDocumentDetectionDeleteJob' do + expect { + described_class.perform_now(project_uuid, location_header) + }.to have_enqueued_job(Metais::ProjectDocumentDetectionDeleteJob) + .with(project_uuid) + end + + it 'enqueues ProjectDataExtractionJob with result' do + expect { + described_class.perform_now(project_uuid, location_header) + }.to have_enqueued_job(Metais::ProjectDataExtractionJob) + .with(project_uuid, a_hash_including('key' => 'value')) + .on_queue('metais_data_extraction') + end + end + + context 'with 202 Accepted response' do + before do + stub_request(:get, full_url) + .to_return(status: 202, body: { status: 'Done', result: result }.to_json) + end + + it 'still processes the result and enqueues jobs' do + expect { + described_class.perform_now(project_uuid, location_header) + }.to have_enqueued_job(Metais::ProjectDocumentDetectionDeleteJob) + .and have_enqueued_job(Metais::ProjectDataExtractionJob) + end + end + end + end +end \ No newline at end of file diff --git a/spec/jobs/metais/project_document_detection_status_job_spec.rb b/spec/jobs/metais/project_document_detection_status_job_spec.rb new file mode 100644 index 0000000..a781398 --- /dev/null +++ b/spec/jobs/metais/project_document_detection_status_job_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' +require 'webmock/rspec' + +RSpec.describe Metais::ProjectDocumentDetectionStatusJob, type: :job do + include ActiveJob::TestHelper + + let(:project_uuid) { 'project-123' } + let(:location_header) { '/document_detection/status' } + let(:api_url) { 'http://example.com' } + + before do + allow(ENV).to receive(:fetch).with('API_URL').and_return(api_url) + WebMock.disable_net_connect! + end + + after do + WebMock.allow_net_connect! + end + + describe '#perform' do + context 'when the response includes a Retry-After header' do + it 'reschedules the job with the specified delay' do + retry_after_seconds = 5 + stub_request(:get, "#{api_url}#{location_header}") + .to_return(headers: { 'Retry-After' => retry_after_seconds.to_s }) + + expect { + described_class.perform_now(project_uuid, location_header) + }.to have_enqueued_job(described_class) + .with(project_uuid, location_header) + .on_queue(:metais_data_extraction) + .at(a_value_within(1.second).of(retry_after_seconds.seconds.from_now)) + end + end + + context 'when the response includes a Location header' do + let(:result_location) { "#{api_url}/document_detection/result" } + + it 'enqueues the ProjectDocumentDetectionResultJob with the new location' do + stub_request(:get, "#{api_url}#{location_header}") + .to_return(headers: { 'Location' => result_location }) + + expect { + described_class.perform_now(project_uuid, location_header) + }.to have_enqueued_job(Metais::ProjectDocumentDetectionResultJob) + .with(project_uuid, result_location) + .on_queue(:metais_data_extraction) + end + end + + context 'when the response has no Location header' do + it 'raises an error' do + stub_request(:get, "#{api_url}#{location_header}") + .to_return(headers: {}) + + expect { + described_class.perform_now(project_uuid, location_header) + }.to raise_error('Location header missing in response') + end + end + end +end \ No newline at end of file diff --git a/spec/jobs/sync_all_topics_job_spec.rb b/spec/jobs/sync_all_topics_job_spec.rb index 3521e56..9f6d4a9 100644 --- a/spec/jobs/sync_all_topics_job_spec.rb +++ b/spec/jobs/sync_all_topics_job_spec.rb @@ -10,18 +10,15 @@ [ [], [], - ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], - ['Projekt1', 1, '', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], - ['Projekt2', 2, 'projekt_2741', '', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 'ABC2', '', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] ] end - let(:indices) { { 'Projekt' => 0, 'Projekt ID' => 1, 'MetaIS' => 2,'Platforma' => 3, 'ID draft prípravy' => 4, 'ID prípravy' => 5, 'ID draft produktu' => 6, 'ID produktu' => 7 } } + let(:indices) { { 'Projekt' => 0, 'Projekt ID' => 1, 'Platforma' => 2, 'ID draft prípravy' => 3, 'ID prípravy' => 4, 'ID draft produktu' => 5, 'ID produktu' => 6 } } before do - Project.create(id: 1) - Project.create(id: 2) - mock_document = instance_double("Google::Apis::DocsV1::Document") allow(mock_document).to receive(:title).and_return("Dokument RF-priprava-template") allow(GoogleApiService).to receive(:get_document).and_return(mock_document) @@ -48,8 +45,8 @@ [], [], ['Projekt'], - ['Projekt1', 1, '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], - ['Projekt2', 2, 'projekt_2741', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] ] end @@ -64,7 +61,7 @@ [ [], [], - ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'] + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'] ] end @@ -79,9 +76,9 @@ [ [], [], - ['Projekt', 'Projekt ID', 'MetaIS', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], - ['Projekt1', 1, '', '', 'ABC1', '', 'ABC1', ''], - ['Projekt2', 2, 'projekt_2741', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 'ABC1', '', 'ABC1', '', 'ABC1', ''], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] ] end diff --git a/spec/jobs/sync_one_topic_job_spec.rb b/spec/jobs/sync_one_topic_job_spec.rb index f83135d..53e5194 100644 --- a/spec/jobs/sync_one_topic_job_spec.rb +++ b/spec/jobs/sync_one_topic_job_spec.rb @@ -12,7 +12,7 @@ [], ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], - ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ['Projekt2', 'ABC2', 'Platforma link', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] ] end From 347ba579ef8021721cf6fed44dc924afdf73ff48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Fri, 31 Jan 2025 14:25:13 +0100 Subject: [PATCH 38/39] Migrated structure.sql --- db/structure.sql | 139 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/db/structure.sql b/db/structure.sql index ffd6bdc..53355bd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,6 +1,7 @@ SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); @@ -355,6 +356,38 @@ CREATE TABLE public.ar_internal_metadata ( ); +-- +-- Name: combined_projects; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.combined_projects ( + id bigint NOT NULL, + metais_project_id bigint NOT NULL, + evaluation_id bigint, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: combined_projects_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.combined_projects_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: combined_projects_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.combined_projects_id_seq OWNED BY public.combined_projects.id; + + -- -- Name: pages; Type: TABLE; Schema: public; Owner: - -- @@ -431,7 +464,7 @@ CREATE TABLE public.phase_revisions ( title character varying NOT NULL, full_name character varying, guarantor character varying, - description character varying, + description text, budget character varying, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, @@ -611,7 +644,7 @@ CREATE TABLE public.projects_metais_projects ( CREATE TABLE public.que_jobs ( priority smallint DEFAULT 100 NOT NULL, - run_at timestamp with time zone DEFAULT now() NOT NULL, + run_at timestamp without time zone DEFAULT now() NOT NULL, job_id bigint NOT NULL, job_class text NOT NULL, args json DEFAULT '[]'::json NOT NULL, @@ -785,6 +818,13 @@ ALTER TABLE ONLY metais.projects ALTER COLUMN id SET DEFAULT nextval('metais.pro ALTER TABLE ONLY metais.supplier_types ALTER COLUMN id SET DEFAULT nextval('metais.supplier_types_id_seq'::regclass); +-- +-- Name: combined_projects id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.combined_projects ALTER COLUMN id SET DEFAULT nextval('public.combined_projects_id_seq'::regclass); + + -- -- Name: pages id; Type: DEFAULT; Schema: public; Owner: - -- @@ -935,6 +975,14 @@ ALTER TABLE ONLY public.ar_internal_metadata ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); +-- +-- Name: combined_projects combined_projects_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.combined_projects + ADD CONSTRAINT combined_projects_pkey PRIMARY KEY (id); + + -- -- Name: pages pages_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -982,7 +1030,7 @@ ALTER TABLE ONLY public.phases ALTER TABLE ONLY public.project_stages ADD CONSTRAINT project_stages_pkey PRIMARY KEY (id); - + -- -- Name: projects projects_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1107,6 +1155,20 @@ CREATE INDEX "index_metais.project_suppliers_on_project_origin_id" ON metais.pro CREATE INDEX "index_metais.project_suppliers_on_supplier_type_id" ON metais.project_suppliers USING btree (supplier_type_id); +-- +-- Name: index_combined_projects_on_evaluation_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_combined_projects_on_evaluation_id ON public.combined_projects USING btree (evaluation_id); + + +-- +-- Name: index_combined_projects_on_metais_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_combined_projects_on_metais_project_id ON public.combined_projects USING btree (metais_project_id); + + -- -- Name: index_pages_on_phase_id; Type: INDEX; Schema: public; Owner: - -- @@ -1295,67 +1357,67 @@ ALTER TABLE ONLY metais.project_events -- --- Name: projects_metais_projects fk_rails_747f5e41f3; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: combined_projects fk_rails_0d08d8ab66; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.projects_metais_projects - ADD CONSTRAINT fk_rails_747f5e41f3 FOREIGN KEY (project_id) REFERENCES public.projects(id); +ALTER TABLE ONLY public.combined_projects + ADD CONSTRAINT fk_rails_0d08d8ab66 FOREIGN KEY (evaluation_id) REFERENCES public.projects(id); -- --- Name: phases fk_rails_7768cfc98c; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: combined_projects fk_rails_18e52e7275; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.phases - ADD CONSTRAINT fk_rails_7768cfc98c FOREIGN KEY (phase_type_id) REFERENCES public.phase_types(id); +ALTER TABLE ONLY public.combined_projects + ADD CONSTRAINT fk_rails_18e52e7275 FOREIGN KEY (metais_project_id) REFERENCES metais.projects(id); -- --- Name: projects_metais_projects fk_rails_7d4b01aec6; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: phase_revisions fk_rails_290d0a047c; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.projects_metais_projects - ADD CONSTRAINT fk_rails_7d4b01aec6 FOREIGN KEY (metais_project_id) REFERENCES metais.projects(id); +ALTER TABLE ONLY public.phase_revisions + ADD CONSTRAINT fk_rails_290d0a047c FOREIGN KEY (revision_id) REFERENCES public.revisions(id); -- --- Name: phase_revision_ratings fk_rails_89f94d4743; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: projects_metais_projects fk_rails_747f5e41f3; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.phase_revision_ratings - ADD CONSTRAINT fk_rails_89f94d4743 FOREIGN KEY (phase_revision_id) REFERENCES public.phase_revisions(id); +ALTER TABLE ONLY public.projects_metais_projects + ADD CONSTRAINT fk_rails_747f5e41f3 FOREIGN KEY (project_id) REFERENCES public.projects(id); -- --- Name: phase_revision_ratings fk_rails_8f89f5f94e; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: phases fk_rails_7768cfc98c; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.phase_revision_ratings - ADD CONSTRAINT fk_rails_8f89f5f94e FOREIGN KEY (rating_type_id) REFERENCES public.rating_types(id); +ALTER TABLE ONLY public.phases + ADD CONSTRAINT fk_rails_7768cfc98c FOREIGN KEY (phase_type_id) REFERENCES public.phase_types(id); -- --- Name: pages fk_rails_9214ad0f21; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: projects_metais_projects fk_rails_7d4b01aec6; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.pages - ADD CONSTRAINT fk_rails_9214ad0f21 FOREIGN KEY (phase_id) REFERENCES public.phases(id); +ALTER TABLE ONLY public.projects_metais_projects + ADD CONSTRAINT fk_rails_7d4b01aec6 FOREIGN KEY (metais_project_id) REFERENCES metais.projects(id); -- --- Name: phase_revisions fk_rails_9b7644f642; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: pages fk_rails_9214ad0f21; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.phase_revisions - ADD CONSTRAINT fk_rails_9b7644f642 FOREIGN KEY (stage_id) REFERENCES public.project_stages(id); +ALTER TABLE ONLY public.pages + ADD CONSTRAINT fk_rails_9214ad0f21 FOREIGN KEY (phase_id) REFERENCES public.phases(id); -- --- Name: phase_revisions fk_rails_9bbd5a8be5; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: phase_revision_ratings fk_rails_9a5c0eae42; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.phase_revisions - ADD CONSTRAINT fk_rails_9bbd5a8be5 FOREIGN KEY (revision_id) REFERENCES public.revisions(id); +ALTER TABLE ONLY public.phase_revision_ratings + ADD CONSTRAINT fk_rails_9a5c0eae42 FOREIGN KEY (phase_revision_id) REFERENCES public.phase_revisions(id); -- @@ -1382,6 +1444,14 @@ ALTER TABLE ONLY public.revisions ADD CONSTRAINT fk_rails_d1037952e2 FOREIGN KEY (page_id) REFERENCES public.pages(id); +-- +-- Name: phase_revision_ratings fk_rails_df4b31c2b5; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.phase_revision_ratings + ADD CONSTRAINT fk_rails_df4b31c2b5 FOREIGN KEY (rating_type_id) REFERENCES public.rating_types(id); + + -- -- Name: pages fk_rails_ee4b1c338f; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1390,6 +1460,14 @@ ALTER TABLE ONLY public.pages ADD CONSTRAINT fk_rails_ee4b1c338f FOREIGN KEY (latest_revision_id) REFERENCES public.revisions(id); +-- +-- Name: phase_revisions fk_rails_fb290ef702; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.phase_revisions + ADD CONSTRAINT fk_rails_fb290ef702 FOREIGN KEY (stage_id) REFERENCES public.project_stages(id); + + -- -- Name: pages fk_rails_ffffd09d52; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1449,9 +1527,9 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240724072736'), ('20240730180654'), ('20240731124741'), -('20240801081124'), ('20240820080110'), ('20240820080303'), +('20240820193320'), ('20240821210736'), ('20240821212620'), ('20240822112026'), @@ -1459,4 +1537,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240823082322'), ('20240825111806'), ('20240825115911'), -('20240924210648'); +('20240924210648'), +('20241212091706'); + + From 3dd0af54975bf5ec2d2d8fad2affe61bbbaab5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Drga?= Date: Fri, 31 Jan 2025 14:31:32 +0100 Subject: [PATCH 39/39] Removed transaction timeout from structure.sql --- db/structure.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/db/structure.sql b/db/structure.sql index 53355bd..f615d95 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,7 +1,6 @@ SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; -SET transaction_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false);
    Názov
    - <%= image_tag origin_type_logo(event.origin_type), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(event.origin_type) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <%= event.name %> <%= event.value %>
    - <%= image_tag origin_type_logo(event.origin_type), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(event.origin_type) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <%= event.name %> <%= event.value %>
    - <%= image_tag origin_type_logo(supplier.origin_type), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(supplier.origin_type) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <%= supplier.supplier_type.name if supplier.supplier_type.present? %> <%= link_to supplier.name, supplier.value, class: "w-100" %>
    - <%= image_tag origin_type_logo(document.origin_type), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(document.origin_type) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <%= document.name %> <%= link_to document.value, document.value %>
    - <%= image_tag origin_type_logo(link.origin_type), style: "width: 16px; height: 16px;" %> + <% logo_path, tooltip_text = origin_type_logo(link.origin_type) %> + <%= image_tag logo_path, alt: tooltip_text, title: tooltip_text, style: "width: 16px; height: 16px;", class:"tag-tooltip" %> <%= link.name %> <%= link_to link.value, link.value %> Dodávatelia Aktivity LinkyAI dátum Akcie
    <%= project.project_origins.joins(:links).count %> + <%= project.get_ai_project_origin&.updated_at&.in_time_zone('Europe/Bratislava')&.strftime('%H:%M %d.%m.%Y') || '-' %> + <%= link_to 'Upraviť', create_human_origin_admin_metais_project_path(project), method: :post, class: 'btn btn-secondary btn-sm' %> + <%= link_to 'Spracovať AI', run_ai_extraction_admin_metais_project_path(project), method: :post, class: 'btn btn-priamry btn-sm' %>