From 3cee485f46adc8d87243026728e2be2827c8a24e Mon Sep 17 00:00:00 2001 From: Wieland Lindenthal Date: Tue, 2 Apr 2019 16:49:42 +0200 Subject: [PATCH] Revert "Revert "Merge pull request #6964 from opf/spike/bim-bcf"" This reverts commit 766f9581453c8d191da4f2485dbc10c1ba1029fc. --- Gemfile.lock | 10 + Gemfile.modules | 1 + .../openproject-icon-font.svg | 348 ++++----- .../openproject-icon-font.ttf | Bin 43976 -> 44340 bytes .../openproject-icon-font.woff | Bin 25660 -> 25868 bytes .../openproject-icon-font.woff2 | Bin 20740 -> 20884 bytes .../fonts/openproject_icon/src/export-bcf.svg | 1 + .../fonts/openproject_icon/src/import.svg | 53 ++ .../fonts/_openproject_icon_definitions.scss | 684 +++++++++--------- .../fonts/_openproject_icon_font.lsg | 2 + app/assets/stylesheets/layout/_main_menu.sass | 18 + app/controllers/work_packages_controller.rb | 51 +- app/models/attachment.rb | 18 +- app/models/work_package/exporter/base.rb | 20 +- app/models/work_package/exporter/csv.rb | 2 +- app/models/work_package/exporter/pdf.rb | 4 +- app/models/work_package/exporter/success.rb | 2 +- app/services/add_work_package_note_service.rb | 6 +- app/services/create_work_package_service.rb | 2 + app/uploaders/file_uploader.rb | 16 +- config/locales/en.yml | 1 + lib/redmine/menu_manager/menu_helper.rb | 23 +- lib/redmine/menu_manager/menu_item.rb | 3 +- modules/bcf/Gemfile | 2 + .../bcf/app/assets/stylesheets/bcf/bcf.sass | 20 + .../app/controllers/bcf/base_controller.rb | 4 + .../bcf/linked_issues_controller.rb | 88 +++ modules/bcf/app/models/bcf.rb | 5 + modules/bcf/app/models/bcf/comment.rb | 14 + .../app/models/bcf/initialize_with_uuid.rb | 17 + modules/bcf/app/models/bcf/issue.rb | 33 + modules/bcf/app/models/bcf/viewpoint.rb | 26 + .../views/bcf/linked_issues/import.html.erb | 30 + .../views/bcf/linked_issues/index.html.erb | 56 ++ .../bcf/linked_issues/prepare_import.html.erb | 60 ++ modules/bcf/config/locales/en.yml | 30 + modules/bcf/config/routes.rb | 46 ++ .../migrate/20181214103300_add_bcf_plugin.rb | 36 + modules/bcf/lib/open_project/bcf.rb | 6 + modules/bcf/lib/open_project/bcf/bcf_xml.rb | 5 + .../lib/open_project/bcf/bcf_xml/exporter.rb | 145 ++++ .../open_project/bcf/bcf_xml/file_entry.rb | 14 + .../lib/open_project/bcf/bcf_xml/importer.rb | 67 ++ .../open_project/bcf/bcf_xml/issue_reader.rb | 201 +++++ .../open_project/bcf/bcf_xml/issue_writer.rb | 180 +++++ .../bcf/bcf_xml/markup_extractor.rb | 84 +++ modules/bcf/lib/open_project/bcf/engine.rb | 59 ++ .../bcf/patches/api/v3/export_formats.rb | 12 + .../bcf/patches/setting_seeder_patch.rb | 36 + .../bcf/patches/work_package_patch.rb | 44 ++ modules/bcf/lib/open_project/bcf/version.rb | 7 + modules/bcf/lib/openproject-bcf.rb | 1 + modules/bcf/openproject-bcf.gemspec | 23 + .../bim_seeder/basic_data/type_seeder.rb | 5 +- .../config/locales/en.seeders.bim.yml | 8 +- .../bim_seeder/openproject-bim_seeder.gemspec | 2 + .../spec/seeders/demo_data_seeder_spec.rb | 2 +- .../xls_export/filename_helper.rb | 9 - .../xls_export/work_package_xls_export.rb | 4 +- package.json | 3 + 60 files changed, 2086 insertions(+), 563 deletions(-) create mode 100644 app/assets/fonts/openproject_icon/src/export-bcf.svg create mode 100644 app/assets/fonts/openproject_icon/src/import.svg create mode 100644 modules/bcf/Gemfile create mode 100644 modules/bcf/app/assets/stylesheets/bcf/bcf.sass create mode 100644 modules/bcf/app/controllers/bcf/base_controller.rb create mode 100644 modules/bcf/app/controllers/bcf/linked_issues_controller.rb create mode 100644 modules/bcf/app/models/bcf.rb create mode 100644 modules/bcf/app/models/bcf/comment.rb create mode 100644 modules/bcf/app/models/bcf/initialize_with_uuid.rb create mode 100644 modules/bcf/app/models/bcf/issue.rb create mode 100644 modules/bcf/app/models/bcf/viewpoint.rb create mode 100644 modules/bcf/app/views/bcf/linked_issues/import.html.erb create mode 100644 modules/bcf/app/views/bcf/linked_issues/index.html.erb create mode 100644 modules/bcf/app/views/bcf/linked_issues/prepare_import.html.erb create mode 100644 modules/bcf/config/locales/en.yml create mode 100644 modules/bcf/config/routes.rb create mode 100644 modules/bcf/db/migrate/20181214103300_add_bcf_plugin.rb create mode 100644 modules/bcf/lib/open_project/bcf.rb create mode 100644 modules/bcf/lib/open_project/bcf/bcf_xml.rb create mode 100644 modules/bcf/lib/open_project/bcf/bcf_xml/exporter.rb create mode 100644 modules/bcf/lib/open_project/bcf/bcf_xml/file_entry.rb create mode 100644 modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb create mode 100644 modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb create mode 100644 modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb create mode 100644 modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb create mode 100644 modules/bcf/lib/open_project/bcf/engine.rb create mode 100644 modules/bcf/lib/open_project/bcf/patches/api/v3/export_formats.rb create mode 100644 modules/bcf/lib/open_project/bcf/patches/setting_seeder_patch.rb create mode 100644 modules/bcf/lib/open_project/bcf/patches/work_package_patch.rb create mode 100644 modules/bcf/lib/open_project/bcf/version.rb create mode 100644 modules/bcf/lib/openproject-bcf.rb create mode 100644 modules/bcf/openproject-bcf.gemspec delete mode 100644 modules/xls_export/lib/open_project/xls_export/filename_helper.rb diff --git a/Gemfile.lock b/Gemfile.lock index 48e50a8cf6a..e9579cee39f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -124,10 +124,19 @@ PATH acts_as_silent_list (~> 3.0.0) openproject-pdf_export +PATH + remote: modules/bcf + specs: + openproject-bcf (9.0.0) + activerecord-import + rails (~> 5) + rubyzip (~> 1.2) + PATH remote: modules/bim_seeder specs: openproject-bim_seeder (1.0.0) + openproject-bcf PATH remote: modules/boards @@ -975,6 +984,7 @@ DEPENDENCIES openproject-auth_saml! openproject-avatars! openproject-backlogs! + openproject-bcf! openproject-bim_seeder! openproject-boards! openproject-costs! diff --git a/Gemfile.modules b/Gemfile.modules index 860d2ae172b..ff05bcd1480 100644 --- a/Gemfile.modules +++ b/Gemfile.modules @@ -43,4 +43,5 @@ group :opf_plugins do gem 'openproject-boards', path: 'modules/boards' gem 'openproject-bim_seeder', path: 'modules/bim_seeder', require: !!(ENV['OPENPROJECT_EDITION'] == 'bim') + gem 'openproject-bcf', path: 'modules/bcf', require: !!(ENV['OPENPROJECT_EDITION'] == 'bim') end diff --git a/app/assets/fonts/openproject_icon/openproject-icon-font.svg b/app/assets/fonts/openproject_icon/openproject-icon-font.svg index 9a49fcda398..c7fd4fefd87 100644 --- a/app/assets/fonts/openproject_icon/openproject-icon-font.svg +++ b/app/assets/fonts/openproject_icon/openproject-icon-font.svg @@ -229,509 +229,515 @@ - + - - + - - + - + + diff --git a/app/assets/fonts/openproject_icon/openproject-icon-font.ttf b/app/assets/fonts/openproject_icon/openproject-icon-font.ttf index e168f16c671e519be322f99df994bd0eb1e0e34d..d4c3c19d6a951939f90af09db1f26fdbfdcc72db 100644 GIT binary patch delta 1140 zcmXw2eMnnZ6hHU9m-q6WXp@&1P0V-vt|n;`V~mLwSBkT?+iY=m^&_=Tw2G6}Ntw3G zK%`_Dfl{!unK)U8jI~d?AVH+KZXIQ~(RHPWlrqZxaA;Q7rKO9zD!Ydt_nhB7_uSw8 z`r$WX{t*#1jm`p~O90T^+1Ebs;)c5$fcGVUbm2_z2VH}aA5Q|XhXBUsyHB@=vLi1h zaqeRbS2rfeYs3ZYCo#<3eM7^K#oOQGdcOmZ%f0=b?LWsh(*TOl4%gQ{JV4~22=>)D zUw^i}?{vzfiH`wHy#gR^4)nh_bZzQmA;9O~0Eoh$CQUUdKsXmevSB!RO6L0;L;I3FNL3?t@5sG8=wJRhem8ipb3t`F?a*sgtwp> zj)RYzruQRq12(x8MuG%mI|^~z;-2y+Nz@R#C^n(S*sS=Rc|sb@IG zfQ_BS_5hnPzv9Yh)@t_LKKs6#iY{e=@}6>?jj(a{iAt?%RQ0H$s(Y$+by%IwN#s1y zL^WS&9^`s*6M5>qXkJ>|tnJla(k8T9Iume zmgAu_+u7ng?@T%~2ZmkX=DR)asK@AO@CTDww5)z#O9>Za=M98?@^;q>fX<0u2LokUA0i7!|HxnPDesK=KI$Y#hgSh5_d zEQ`agBNQ~FLkcNkjEu>xblY7{tIfnH9dU7yvtct;ekyfw{yGwPz zp)@IXfR@f>{zMa*nO|Bn|LO%ohDY-l{wPmCQ}SFn%cCetDv;rbWJG7=lw2ZLD)WUr zQcehHfk?z>1bVm4XhikzXP^wtv}O_)2wEr+5u{iqW)M2UB3i&h77alQXp*62GEJ#Q zNQ-!UzRV;O=A4qV@PJ`ZD_0wxS6BBHlh?a5?@0UN}<2F6Vn=!?C9*` VdcJ5v4E=83K>y$nw=gT_{RjHcJ8J*{ delta 774 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 F^x{;`OQrw- delta 25183 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

    G(Ky%wD)U6@y+L7F@rqKZ|D>61 zo{#0H{BY}EB$TpMfbt&1veN!Q*_XpTBFpx{Lx?SfJvRT!j!gEO{6Qy|)Rt$$>A3dV zU7O=s%hQV7YB+1|?P+=MTg$3@o|&uLJbwN1`rsTZflBW03ZwIuSo@o)0Z+i1tiQ;% zxbgx_!qrQF{f+jyx@3&?ZY2?;QN{`L zk?dd#isF7JJpBhCHlBi_-U^5vA6T5N|JGwcQKfGokqFV%E#B)Qt97E;a(UbB^$_|W zJn0TPN6NzWvi1wt5`~ktzpsdIFObU#*iA=jdOA>!-qoW(ym0E_GfTg3%(>!>GSMc{ z`btzw2utX6SBAZ0>4^?_vWn1L*%q^=AQUC&DlL?h$Yt48Dk+eI5c492yC8G4*&VQc zh51vptP2aJqF$wOfpXc%l3`#10{Ismw4m<8w14|VClkC7q0PV>c=lW!`7NGJ<)LCsM}a1`cvfN*Pi5;fz7A ze>r&dM{BaCfs3=eOL_VP+ctQVbAIN&RCeRB+)HuUTNU@@4-6(Cih(*c=-$o@-2Qw%Dm2ha`kEstSrDt|97oNG zSNFqgUJ|p#R*#2H2&dMfHzw|Va^%l9uS0l_7~(1tAv|r5Jy?*s(>@bN4Sy%= z@uv)kPKZoo>w&Ymog2^7*%=V`GPP8Sd-PlN{BKw z>0V|EY~4Ll_F0_$2JY;k4BZEV=WK9v-fgCxP_~OEC(wU6ERz(8eD3)_RDNpx-rIW0 zyF(|Z>fF_w<^DY{KTZo5eZZ5=-gCd_iu<4Vpz(LSxE+N$G~J8+yw_cUw2C}_9{;%b zCOlXi#5(ofE@s|dGrdA=72f!{{6RsZLgI^YL7)MUG#YseGjcgUqC!VoBPiB0rBLI( z^5lm*ng^Yo0Z08nuHt^;7{3g%4>QAv*ofHpm~}7q0 zR+~n&X)I?j^Sj;4_B)yV#;b}Cce~O4?DfhvwMceBLw9{`dFGE%0Lct%F@z6`6ki*$ z;aWE$v6S2$=;C6dc0uchiWUYIUZ2vt z#*pb>o@q3l=+n9_pN94>F{1lx;c4E~F^_uR#qQt$JI<3IHcWA#a%cggPo)eAWzOOr z>xBCsgY%^e8NxGMW8ue;!JZ+m_e-VhoU6m}0rP(}?R5h&I(XVssz4Xz!~dm5OTL?+ zjT(py)Rsi@;%5GeZ~Efmvmi*?hM37e{<`pYa)24r(9N2djHR_YBcqoj>Q8?lk5;Ax z57V2~J()8u!N!~-PRsow^2!M&1t*p2p7HbVq$y>BpF~)+y#3V@MkHX2JD_HWNOaiL zLM~>?NBkSw4?q=J!=uNO>oqhxnCZaw|Ry2MIw2j@Dl zZi>65FHO8Li2wjf6vTFV|94rxasU#cNW)V5$7uTb6!yMcStNNsAuCStv(ZOP3%4r% z;Pk^`_*v1OQ=JaDyd=u0)FSnRiS0+VoPLU;e^Cw^5*f$5-C`P@sk{JBw}XCB{Up~T z%LZ9z${RR(YXoFa1&y6J|322)5lUn5!xl3XI3kpY5<7HZyx0dI8y<)q1eBH(FQ@jD zbc_@cqqejQcDS-Q!2uohU$x4ZBQ1(D%*n5-C|+PN3oBrbEog)UjubKI#-R;fwUVI& zbJtjN_d0Qx{X7j=URox&V{_-mL#e4I#>MsYM12<{8ceTt5!XYMcUpU4_--k+mrhRv z9|ZuuTJO+G|8x(+aDx;FA_f@$YUszq#}h(Ib>t|bhF@49WD(3LZ|NH7O1c^*ipddA zvyO5u319;a0|f~&#WPW=HgOcdVAS-1mvEuc3DGdL=0%qfX(0<7xjtJ~RBKzLXANO) z-kG}t2R8c~T2Fc&`AE7rvqapH*qm$6Z9_J6u<`EMz$UPc8TN_xiPbQ!J$XxgCfL_k zuCLFgKNMD92Sc)Z5?;Fm~2;jc;|d@CEzYc`U93SF$G&T z>YV6VjL2NTIKYe@f(Eq(H@r$WFmR0r#(g}WO1|ph2_Db5Z^eD$f%w$td82EQ>f*eU zr*HrDWe?Xn~Y09Co>B9BdtInYV7)=u3xic?U9&p%p3Fs9ERROA$_64^CBI zcVAUcH}*AhL$70e{DI5CGF9|2wqSnd3btk@9d0j|tDw+l*x$IGaL zHhYW~&AD|?_uKv?`DKYl&)a=j2|@n{7Acu%)k54C^Q|!Biy%m^=a4|%IP&~3SG0;c zhhGLWhtv>iXGar~?8PnnvLy?W^H0jk5qDSHdYEC(nb{|in20nb+U$c9V%SFOp2LgL zbp(%d$cf=h@T9TgG*fQD;@_ipow|*Ob7<%?!0Ims%pJ_m zz2^gQqjGqf?U^S!b=&-(*SsFP`H++itb*MgQL;Bn&>mf5LkKGnPpVtBrYM763ANJEII^0>1%q&||88=>Z`iN5 zww>!*rd+)89p+PlRl=gD#MBf;LJvcUqoQn-I~ghv7oyny~67_g`j`e26keM+Sfil%r+nZ%t{$T0%I&((~ ziitH16^0{w+~aq7H%n=bHig;lm5o&LO5%kUXa%fG=f7)J^dl1|6SUI^DYK-d4Q0x= zDi{R{)mv^YpOrscZY~VTzjCE(3Fr^BT{Fhl3VWru4yo2&2Ibu)4zufmZ1D*te$5+C zR2=k7!9qeU9;)`W{qy66yYi=nbOagge15Mx!V0;+o|H@qPR7USFoP%;N-|n{2AS6p zlAR9XGjQlGP%eKM1Z^y*l169tVQoS=29TfKjKI_xPL^E<;i%{~^0VuL%8I!1rs>(y z#8sGDciyW*>U~bFGOr*jY}=*lufqghtrAJda zA4S+NRL^lq}40KWhh7A$)DeAdDci+MGiWpqA+S7B6En-vaD?$&V zO+L<0je;!#_^9g9V{PqnL{oyYGXyqqM-p+fb}}|5iP7XBcJ3UXswBVId7M`PVaN1& zXtyM376wPN?EuEnpxN%GoNCBWpXAUcF?T%M9(q}4O9wG2;N-!BLU#%%j=~?k4F+7B zxD8arWjHtmJPMD;Kqr5sBW_{K0^^V_K*z8}yZ5@B<`F*K_*>#5=(Wi%_yOe~)E}LK z!5*}n7I}Xk9p;{fBKKYm_^F_FTZ7tGxmt%}$Tpw(z5&0~rk^ID&^wbs2N-WeotRtb zCW`Nh2pXmf=Dv?%y$%%}yxz*!r@O7uv=-(_&&I*FbyrY7f~T)YaHdvrQat&1(9++^ zcZG}44k%o^W=DrO_NEr1a97E39@{3y%=fCY*=wy5@|$0TYumi>B$x0+QZxqmd~0EE z*GX}8RJP$yczDu4PVUVH?tIXa*%muHcG`g~P0t3Rs}WmawTF;ujNsR{ql(P-Xl8XN z2Q*uHcX|;oj2W5^(x0`NikGD$7-x#IZQ>yN{0Qhdom90!PDL_l5Vr`8WYw3D?u&Q= z%^OG?tK^F+S4`acX&lBFi}ECzj9&)ijhgA7Ihy*Xk?l?5)a~2_j@c_b`BaOyZLm+6 zy)6+x6aK{8+bf^RuBXWC!E!&<6M^Yo$jr72D;LlC8cp8I?yox1=#F(J#7pmeFaR;i z!cX^>yy<_yAS1KN&uWKZyy3W=VeSNihi$&J4&=$qXdjrBu6e?Np^e|qX?Oc(y<^cl z$o~mJxr##mVKBDdnNQX>E>JrB-YEy7KDH^>2pz7YMWjF)d2CLl|(d zB7iQeOhRRsIzFD1rx8s|kr5-@q$qW2W+Wc}@R^DydIDXM!oVd&Sa*(jnA&fTZPyWF zbbj8SxAG)`JVF(Nf$N3$b!)!RlP-!@Pm$kvd*OKdqXepCWB!)r5~Q*oMDFn}mCB8OB?fl&AhvIz@uSaQo=IxDWC6g$i?s$UbAI4L>}}U`w`xQt+a~ z*j5OlS^|8o30;Xa@Y<`rEzxgiwisZ%v7s7BlLw7(J1 z)$g}tzraE;Dv(|N=CHl+dy%b=5z1k9VkYnm%*i{I^yDjV2HzBKl=acv@XKEsc2Ba} z*ln?9bf4qYKa}X|l5*0*!xQlt!ZA)}{Le_7F|Ori4dj^=?j*p_AkmfI?kr4mRt9Ve zgk&;t!y5Mn0>BM_rvQNg&l~3f`RDGsfIvxTvWKCocg4&{Cw8cCwGt;_WE0B-Q<$;jUVsz zNOvzxci#y7ba1uJw=xWe97#b39hc*K@^%L%NKhjXj9n3Z0gpLGUdGs<(bp?fHl6kr z@lf#0zSn0RF z?o1api<1-F??jsJ%5%`8Tz_NUZceVxzkB}(!|P-@bfnu45LI@d00Y#W+|z;iJtQ>{P+FsoEg zPAphd4fVT&*nK=-zBW|iUhtvR44~nuZ4(ZnVGepakxxr)@~Go?qyA8@1a(D&Z4BB_ zbc2wAHGAGXQd307BuG^Eo2rRiXz;LI*z&3Fm8s~jxg;e=HM!dOF`Oa+1{-XNu9TrO z_v0rNK?wyBi)mR$G=#<^9a3TTyW<3uBvw>N;a9^P43YF5O!Txt%|zNTDj6Y_ia21; z+T3CAB1O;wh(iX;kgy>`C0V$ujacS%ai7k+MV7FVF0P%5c-ydm6OJ;CqKLHv({JU&_0 zMF>n)+Gra-hDcZ6Qg4KQi2Za-OH9c#6C_~?Kc?0J)3rzyO4U< zJOTverrPQ2W4pC{->r+z%E!0Bh-^NSYQegd%O$(>KB3lZNP0{0M|Oc1?^P8N_|*(# zIr|QV46Zr8PScls%mw}WTvFE9n6iO_&BM+qK0hfo6CvTtwXQ8}?vPNt@{DY0$bDrd ziV;A>hFHQr`hVfhLfstBXZTv+Mk&N9aN}q?a6l)`iBxND9DgN4boAVvleaF~+rBf4 z%;m-~rp+35>8HW|DbGA!$1Np(LcF)IRJE1u1yo|iLxD_4b6)Rd)1(^FTws@6-s1yFjgI}Z4AbzKO<@4Vn; zc2sUr&<)^aAhS;P$^A})H-Gr9WL9KS&IYCk_>BQ^luW%BK*v7bQ3lKI1WFyq?CFxt%aubAX7Q<8n znKU@j%w|0P(8wFfKP?-$FT1pvH*4;KXGi6myn&cYGIi;AT+1UNPTsXk0osf0AVt8z ze;)+`(&|Am$j||H_1Tt`>N58c!)eR9PqLeah?gz`G#1Y%LY2=_%BI}?{$_tU-C7U^ zh3O&Qz`#OdzzRX8x!P&aVnPO(07?%;Nv;3Z3RU_he3IxR#2ttNXtWJYMZ07&s=^sE zEEbYr`sIXE%fR{-B;Ghcg~lo(u#cAwh(c5fJt(0Hg$k`HpX_M>`aih=TGAaZCQw4| zEtrp~j*V9@ubA8zCYv~ zJ5+Rv!6au`g#e=N>B3tPqn;UAY5I&o0fUHyi60DL>uy;BHBC5#3|LX!-D5+lPGI^- z;TZotweJPA)L9$Dd^*5G2^OZ%%zzUj$N3=)0|d9sh$qSEVV5X&)pLmQH zOv#kf_w_I)n`sq|Y0b3qc_aHWK5uV7-&P(}8b!(858O+Wa(+214Yt5RO!{e}sKe;=!Ra&&5b&Da%k`bP9zsvv zTRp zIkB>%b$i&LX`-)8I|}w@cRwTQ6+d^tyOwU7pZL=OyMjaQ`Bmh>PN_o6^y!tcv@2_p zqL;CQoU4eErlx|fhT|HHpcfUR7@3%!d?!vOqKh^jW|9^2sSgO$kWDt~CvZ({SloV~ z3mfOR{L)DlA8#MWIW{kB##vk0l1i6~Mezas+=q%ptj=*}WP4&YKD#54-(oEr5uh#gl}U&o zut8g5v-oqZQU=jbr7{A`TvA_3APz^0v~2&yrB)5~o_|f8h?Pm#3~JUba@(V-Hs+m^ zn1UP;9GG{B?v|xGpi|--b3HrC35830zHV33>*(5T4q*|_Qx?t|g8l5uNuN$J&yv-2 za_{Uc;3QDg?{6gVRG3p)K)iyT!%w?`tp!|02gX`f)zLw%O#7iNd|i}FH`IEzU@PT4 z&IezgJAVCik(II*>-4JA^7n0*Za%)gNs4XZr6n>mxojqLdu{C_kOg)QnBfBV1}_tu zE27~p|GYa3ReX52u;KF?6Cx8-b}Dd;K;&>8@eDn~n~mSFrolw!*OMLkJz53Zm~xE+6e|S%a;!aj_$e=6?k@5yR}3lz0Sm0%4#3%V)u*&WCpc%fJK2>!FL`LS%K%j#;0u?YjADQ;o~?NOQP9M zw#Ptp#=Hi`-ffri410%_eX#GnFL7PO-f>x;PxnOE=#44w|7!H?3O5=D$tyLJ%XwzR z{3H{}biTRePd`H!(D%{(1rPRTAQ?B|2>q*ql=S_YPuk|L05^=)E4Ye}!9d`vQ)7&N zw8}LQDf@S)-Pvgx^6fX#D5RpPCLL4TmX746N}%K_isczjVZDu0P3&qRz9Ms*IJ@8MiF)oS6OEMbM`{djF{Qdg$^WkvIEQz)#%~+5guQvz zEkdr%S-BgIQP@dArzd)<{fw?VYCNR7udx9NIs*SVC72n}eb9TT|1DW;u-^{9 zwzFPvQbAnL#C3s^=(O57xaUrR!B7g?;=KB3e$i7N=Tr5*w^)1)BiYLf%AoMw*NHuw%)4-7L4E;%>smgy%&J)8;g#7?Lq12`K!Wt^(>}L;SXAPM%S9nfw22~K zLzmFe2mYUjf{+Po#FP#)D@26j?#};3aC{b<#X;Nz5Uu#T%OaKze`kjze)?U^DIL-{-n5T=4(=i23h2A?d=!_)O~81r)#~u_ zdiE?Y*SrAHQ@^LHhW&1W$p^WSTuDR8hgG4gUeq_a@$en&Y@aikeNXGi9^^Bhi2Hc}Z#$!2v-VtpK;lRC0kq5#LbCa5^z6YK3A>!TgT zY#5=P-CTT`meBfm$Hsc!+#1YDPMb?(ImJ#EZEo7o*FPfc_cfl&sI@*b0EPs97PEJ( zVxyVay@!j@~D8d`21 zZItx8_IJ{Ah(QY|xleEjGX;X7HHEQNYfzE`f^aHy^1~%0p3C146-pWlKdUGIq1ChW zk?Fzh6yKGsbLe^IN?&TTz5#gS(tTY`_t=e3XvYIh@nHpc)k`|ZH+q4E<+1+cl-=tm zuq+Fr{h0JuRd_*Iwu!UT<;jhxd(AU*JcV8iq5n}Uw}zKim_m)o2p6-k*X1=5&-nE4K3fB<8L z=H;(vpLyqcoR^2Cyy^pk*V_6;T$6|b_lCO@%JMQ}K%}ZIqYjApnsSBgOi#UbEd1Q+4%$V{4Q{#mNMAb@B2}3ul|RZklhY=oy;$ zsonmECohdjl>5o9)Gq6ckojZPod@AoTUht3&hU+!!-{9k?vAjPg)_Eo-X307JnFXK z|E!9P@QVmjg;{4*9>1Z~AMEw(1=fP6bstCmXz*|x2_Appy!~OO*Z*~&Prm#=#%b-y zYpDq+#h(sC12T&ow-kV4IZqsb+nclQm+ltCLZ~nj*g^1Y;4F)CmAKvv6P*2sp3v%W z?l#hfo}yeJyLkEcc_HEz?J%`-b30AupJBCDoL$GIj5|$v{2m}v@^(27Abr-)pA$vB z@Eqd5BjhscAj*q}p?~-o39{!!k`X^98h?nmKDBm!w`Ipa^Fp{0WzRs)@K<^HLFB&d zzEJFR+@#?8(8_ZjYwZJBhfNXrnWDLEu1^0=*$rg$v^YMCxU)$P5BV%JmCz!awmOnR zq+tL60D{8yIQ2a{xHe%*1ouFmlm`HRN_769%=vRgP`@3goFL9TRq&6}i|eZcB+>{P z;JwZPq}0RT=C-j)32O==}2{tF;Vz) zy!2Uh^i5kFS5|(!t$W)h|C+c@x}HveyhyO98|PvWfpB5RPja*WCjbg`C{C=ti1u~G z&|r{gfQOt?T(92rAC^sJa{WUS^yyd}NT1RfBnr#pC4RK69)-7CUL)TKQDaJ=DniCWw)2g-Zk${uYuIJ5pNrE-(ZU{ zUk9=r;LmIZQXhYk+tTSG{`0L~>CRICvn+H3yx>)1BL(Qa z+pb*py#WW@Q5rD%*fnk`?*Vk@)*RG>HSl=Q01l)cv1w=86e=?j@3a8 z*aWY*Wx1dDI;=`Ps0WADcmTKMpjGe!Ret}kf#X*`u`^(%^c##QZxX6Cq5J|@f+qGU ze|)z82V$S_FulBwYvd;&kQT6qWk2xA%_8ezr4$c8*254&I`D7)vWevH@7}GfIe0NC zzDXG{Q~C`?ls9Rms+ClhU*Jm6e5XsA)9Eeue<1en?_Y=M<$YT6lZS?8EqZ%ccFT@W zZWdV&I7%r}{zp0mg7MFLpYQJvfDnwJ z7*3EB&9EFVh?13q4~?vLl|{Q(eR zf~07M<#<7qWJT3c zL`hauO*iHOjoOau`9T=PNt)$FS#9nUP22UuIL#N!)q1nt?f0>}BOU|He(}M}Ug=>k zb$N%%5C!PUGv;?#{JM|YP~a*TPa4T>+Nao*&dU0X(`w~>3zJSv1o!eMW$k(G4=X=j zqI3oG>pCAMCRyjxI-gg58HZ$g9M&7ko@hh!wLa_}f>mM_IqECgY!Off4cG>suv=h` z3Avs`Lvs8ev$DaXQ+}<9qE295FAla{VjV~|+j6}@0JNG0$1t}B5lF*+Xdu)TNv_S* z=zu{gF7ML2q3mIeTN)yn>>XzOA?hlDIYv##-q?Z?xY0NyQo_G-v$H760tJt7;qNH1 z2Kh@Dz52UX%o*rn)&%oj7!BmmR^$}m&?wpn*J9q&Rbo-ZoJe@|OiSJjd~d>%qOv?e zNV)R(tNn|=3WDXzM6N6Y2&mP;0<}J?(J}*CK(oKVGx0(0tjiKWwTHrcghb|gSzYOn zw7syco!Wplz+jVuH(H>E;bm;e*_k}UC9G2MHN+?62AUFGz`q0|l)fpB?)v3f)xFI* z-F^yrtjdxNjeB19PJfH@-@=%)WK+a&sJxNsZ+AA;ia+hDAVBq6XN{G{u+qa`p^?Gn z2=?N6C~d%Y4eS_|$TwHbnrcl5YzP2v97WpW^}|<_fbCo*W=$8)$puik3iqy4$!d8U zCu7(UmqHGoWZ0-}@CXxKGB5wo0Z6JhhTNEB(m-@=0ckSZUT==jZRW$LxBjCYOg25k z)8(4WgiO}b`6qy%JSEC&Cq4jz|I9_+A+JVw!Yi zFH>6}uque9OOfCat}{qG(s}C!c{g74Aq=m8$?e7Gs!c43_NILHRTkE{^xowv&LeyD zg{$0fiTfE)q=`FZb@Fg-a(k5C8kNY)el>wZ9h~odHMq-Jg6HibCxlK22S%@(+Wbip zXsyw6O|wl^d97RfG>?TO0$V zj@)~`oVs{$gjrkif%PT#L*R zM16Dgg1vZy*?KBt5sz7vzCO$ozy6s{>wW|Nnm?cgtRT~?O>d>7}e#cW{PvP05#EC=W)$*_y% zDqO4FO=BJ}92xvD12WPS-oeY222Uj`c!cf@I&!|tKR%pw^wQvJ$~9)q`RZ&sstj|~ h4!HN}){xHCu6nz9YN4<;{X8iP5&4BU;qCFjxv}kKn@;MU>r!mq8A_=5D9P?4|I?|00;qS2ord! zEjC6JyRj~HY|t%$$d{agBXOdXCLd?*&}ai7oQ~Pw-ybBTaWo+W9L&px6odp==s=4|#WexMw8pH6Q~=TZ$@T!p?L7Fv$0>Sii{b zZXU}M)-={S+gPu03-B#Zo+W`gz{Ar@=AT~5b>*N|3Ji>+3Cs*CL7`et7j-`w!$(@v zUzE&CtEU!I;?)uEp#Vq(6qBP2wNuLT-J3FJ7J-3J;*Xl`+1m+(W;m|7y&JPpO)Dnc z63(uFKvhFo!OwsPVH^jvfzu4DTEX~*k&U8uf%;}(B1%3ekt@+d8q z)u}o02-{!y4o20-Vc>{OZ(ccg?O^TalLC%NlU-rr=U9F=e2C@O0m6v-MIM$^`?FJZ zl)$p%`)7Rem~Asz%-s0%bJYHG2=-A99*{DvyP)zW!PNlms(rt({wDeWL;-niw$-IX z0^d+Sp9Wvsx4pJhvbX+AA)v~tBO9pzYfJLLiF`H78(lZX>DPZa0s)slZCiluVaW~k zaZ~|V<^Td`dWHeOFgy?f0w9pa^7-!|!6L&XI2?{dC(|L+n9(?Ncm>hW70raJ9|%fx zEia5~e6v@+x&XoYAS3pH#l-=pzhFG6N1zIcs4ovbtUC;}TMoSZ5JemkP2Ch#ofcib z7-b$BZ9g1!UvJ<75Tf!hR0>apk|5~|8F?9x-N+H>%O6tasnVpNlrwwty1qZYcW zHvIn;*}a2DL>YoiEX4f3{3=VZOO3c|PeBZkNi0)Qjg?ugSK#xjb1#6`djn74^9@4H zrI~?9tjmgG8J<|>^Ww-e&GVtH%|*Pp=i=n&Bsqmk&Y`o5oLq9~=R`V%Q_dl^i>#fp z?B`@Vg~Eb`YWF;-S_Qp_b;Mwiuxn>Y+9q zp;qppb|9kWBBItLP!;4%#A+1XMJ!ZBY*X%5(N z4_J8*S^kij@sL^J5t-=`S-}#S$r4%76Pei)S%H+9iIiE96`7e8S)mr0sTNtW7n!*i zSpk@t37ATZ8<{y9SwWncNt{_x9hq4jSz#WTX&zZ|ADMX{*!LNX z>}aEl>X=qWM%FPo4eW5E3mlkcM~2!lSq|)Yqf73Xc1K3uu;v6{mj8ckidq_r+Mr1o#n!;~8IbB5T!XToQU_w60D4Bo) z*oX;iG&YNaxhW$Ry2SuNS0hxM+s$>`&!s%W( zWHy4dfS5vdl2~Dpb!|2SO^2x%$hbZ4I?zqhxOkj+#?fyr4ZV%xKjCn19i+8i6SAN4 zlr>ZX(ta}{(;!NBa^&E|*fAKFD}t7(9^?4nU^iHFp!+H+%K3uxTwI#I^kg`J@pX>gu_Uj z(rAweX0Td7*faV#?dlSehOi#~U`gfiItC@~I!hWwqv{NrRF8#rgM{4B9qoxR$q*Tm zhChn57xUHum}5>{r!EKr7jl2DHb?z~I=CRmzHdDv?hr>|uM7)%n0t>V zU)$AlY3NPFP*a;FO! z)-c&C#sCqJ>%RsjMwh0K5Ly*_agAc$=Fe#ST+}R3W1&zAxGe|G?7ddvrVQyA#_^o$ z9gg&LQj_-qTo>wsFWLothVA*5dlo@wDiuOa_&`4Cr{5((ULz86kLZm6#m744Eg|nW zi%w5#C&z!Mw>lhF*pB3$*kh8wB@H)$8(uy<@8MGXw%rh+!F+xkAUw|E0u(iw zT{btG#~}OlBMqaXmF?~Gjhe2r4rqq&T)g9!6jm0CE94EbfO`%X*1Et6=UOF_mk)H| zYI-ti^RZH`kb`tIsBF**BPDogRv>FcTk(KK1jlI$AV&)eOlb0;_VDDYa9i01YOW8d8T0-`jh>)HZD#E!%P zlG4rLB`Q2`mwMKjiEy*w~lJm%D8C#Qfe!;HPm%j&v-R8CqmaYS=o%n z2YP7&Ry|DZ+3j8&=Dr1`KG2V9(mNx62j2p*an$FLTxhs|k!lrt(pPPN1RvAZ8PK>= zCtNS!h9U^H4EBsFQJ{+nIwYP7mda;|3*6UHfM<|BWs(8XK~Fah6tPZb8+AoQByUc3 z_Uu0!n#A+S+y&6xCSqDGp=QhSBQe3Zo0o7{l@$-~?ts!)8T=fnl@r_qwb4M)d)*Ir zFX(&(->}kO3S+UlP(8ZUsWt)kpk2jGAZXkS;Bepi9wDXwDX1h=k}croagH2eD4? zrAnC}jW=yCtDKyiWu8s(8l^Vp6wTWs3Q1T%*$J5zLIPag{TRGHeL#vY(+vW#G9cVk zqQ`dN%($UI!JTkc_XI|=HHRMi@MF9{qsM@{DR>OGeGU1#2d$%P3tMyNG2jqRp=u5l zNlC^9!5c@bRr<4k{Pqym`uNLkFF(4`%aen_k;;xpWM@aEmO>Bm3=d9v4Z~!nVRIBw zk6_`;SI{$D_~;Sb-Ju3=2^qonAV_SVU*#tJ#C;mvcP>e&R{H|c5}{k<_$zU^hM0KtK#*W)_c zqdpkthCR}-3BfGBza8XQIzaOCqS#e&Y7lX{tgOK{Qxaj66p^z@|YaQ^a<)C>UIV``#LbSVY>H8d_Z}VEqC} z1Jz6YNCxpB+yMu4O@+*Oc!)2G=I~%z^30+oCwgEF&IlX01}G4jnVAsv1U+dj%7rO!!nzgszf;W=vz^JP;0E zF>Gb2`II$cf}`w^>2o^tM4npRGM^xUB@~LJ?7*lBgvIdlUC8MVgY*3!M0LgQ1v!vt z8S@T4u%X!TmjXTG)!gW*Y|>7FGcfIT>ZbiL zvDKeTX%R9#^jtBrgAUPJ0^g_ksbHL2RczHvhx1u}HwaH-hecYE zIe#%4TJ=`K(wcqwT>v(rl)zr8Q@|o?AW_AND%*M02Bkx9M;?Q!N0=A2a1;nS=wNI` zK=MgEqH7 za%IM3Yx*Ay`_IZ-HO9uNytX`dG!ct!OeTO|VLm_DrNt3To$NkvNVLuXi+B?*#`C7t zdOHxMtJj}L%QVS^$XI#Gaa2v6_VQUB9(=fD^@@;3QUr?zaT(M!YCvBgm9Ho!1p-c) zW~=pi=cr((lFp+E*yOd@d7R4{FPw2?v_bHU_N(oSMZ(vuLvMr5dG@#*tr-01=HF z6Rg!u;J*br0|9=J)y2_hU+Gtx5QNrY1)bF{5vVQ!xa76**G+rIx$|bMruLY1yz9U3 z?W)Pqd!6{*@kk=raDn466E-rYliz|&(zZunI2E)vhd|t2PPrmk&{kUlbrIWg9fYQh z%0EC%Fg~2#eMjdyS9gIg4du6(C@B{n!f8&oa}&q{yfI^u?s0g(zUDPoA)yei0o93ETLUpAu9QQ1G7dcB zEhj=nVxjp8wj@U=E8tRqGK;czhfGhTF^*^LFD%P${D>#e1#tj3DZLP>Y`-32$xp)x zt4_`;Y%jJL_Bg(0N3iQ(hxh1}-{g)85{F4anP-yrD$27w)n>tl=6`J_v-O?HeOY8D z*nl>bp^5Zk^J9Fb_2RXIX>n2vI-aGDibTXhWxwN4bIOc|h)K;5zygrE!j^1k@u-n# zCmA3b#0#W|acHk>X7V*BtcB@^^!>~Q?De9PYhhv`P(&*KV(q-ypl2g&b8I8?2eZEr z6o@IHPMR@F7_k1~O)w1d`&Bt+kjobPzKwWiaE=w8%~|94T$qaM^Ps1QX<@>zlVu}M z!>hI?>HTruqmE@;!mpNTDgD0t{@Qz88q50o?s+($cm(X65gz0Rh8c}52tKdAhBd-w zUq%~d6B4-NJKv>#ISuf+_67uUSN=AWTds(7+K-IW6O8Iy<^tc`$`msSyFY*-MBZ@pR|vI&&3(6|PxLaQRm zv7Ac@S0!-w+=c$Wj~D}~i|6r+pmyFAC2gWJko(e0TQouRu8>eHn_wd$?7t zW1E)jw(Pw7d_4rezsXiMNB5oAU59?#ya3yNM*zAs^oZ^@hf5kJNc`wU;P@n_(Jg@D zeXK+P6+#J)AzCylkG&j7)FNvKj8RNwO$2a~VaUH7&&``~`#@+azfgxLwhNU+b(8NMc^g+?x26n+l` zs0^Yr_zc3m8}bB=$3PJLAs~P&dOYtDE?tD9!yW@SFAN48nEbKUqt`4KeA+{Np30f^ zmwj?}8t@upX4KJiUYWB0edeYYFwWhW$z(oIC($CQR@LO6wM*Z%E|=GK+gR;$ zulmikE7+fHTc7roEbh*=cL2AD^!M6K(?m8F%|dF&=ELf$x|RRfc1;bqWO!rPERsci zAAgkNdESIoj-BmX^{g^yZJqjtz~?)J&XsagZE0ZQnxM^KYBa_`IA&(7?ULqpe= zmn4sd-N(0LHO2;pUJ9{oNc+5Rnhi+=7MtVni0Ho+41XzbC<~M5;RHTM#{El2Z#{S! z^I_#oB!OZkH*e`6z3@e7(MzORNwBVPMH$6vGtZirGfbS+^DPvE_p#oK{s|Q4wnhCMe!Hc*{CAU*9eGuHxVl(~N+6S-mvePi# z50h0#9Y77(XI-?patKX>AZlAk70ncJ#JZai9ca)sU!BQGkgAIb2^9kx^{SDb4M<7< z<(L*YNt2M+VPQ0Kwk+ve+#6HC7ObQc6c8pjAhH6>tC_N9*0=FlAae19LdcJ_3;@e* z+Vn9=SSjFRnMR`{1rnvf3&ofR_UX4Fgn=^M%;e1h?`3Iy@I4HTtcn|7w6&e;1!lar zSKTJ{)LPyx6JqXUkJQNexBVMBKaC#?chiu91vB28yl5FZHD5a50m`diAvFz{lO$J` zPtb&8QhZCE<9LR(N0&1FaAKN6}Mm!L)W^Lw*)`;=Yl{etr=Puum(;*9&AbvnPf#<-#sQ>pZV^xl^Rzq z;$H~e)b&AMD6nux1OjFeLw zuW|EE$w`vKlO^drk1)C1ICxlK&Uj#{b4p822xIW6Rgo#d?pOwoHepQQu2}>Jh9zlg za4Jp>;HGc@&?}MMip^b0%%viU#eqG2`;vnyirLhaSxUZSZz?0elN0!Q86u$&FVT=k ziMbAL>OZ8ar*RD^CPz@0Cy>vV&u17D@Hj+ps-L=GLcfUw(FKdM^bYX_?K!+JKTpDN z0+UaxXl4W#5m|F0Fw|neddT#oK$ATgygD058j|Wq8AD&8hZ>f9Ma~#_ z@QMmtfv+yhXh74vMP&wNupBTtjO?7sE$)7b?0gDCDV$lo8yMXLFvQq(e|pL3?aRET zW+4(}N!N@L?c?EIh4u%zcizMbFQp92N8?)S-4DkCWf)VM*Rsoyp?sZRr&?o#9})Uz zp1jf8txEgbdUCN`mvTjX_ED5i~sXG>OE1cb5 zcvM3-c@tAb@QSXc>H=|pv(n6sqk~)#Ik}E7l0t?&14k#eI(CHJbV@T?AJ0&(n$X^{fM+u+GFxaDoib>gJ%V_4A|*3A0K|_QSF~`7kdKJL@b~C=drT(-Tm!3j z2dau#=cY*T+Z7lQWd#2i9|o73!~QT+MJnUbB=8lLvZ#dY34tSGl7RlOudOGuL*jF8 z=2i$$`^ycu?F(h_l`guD+ut9ETG{~NW+@QL=5u$i1TM?bL0yEGt>#xC@Tb)YIdlBG zk7(ts(&q5(T4AIh(Le9s9GC`e>GBpJ>3-0IdOek*uVeTLyEr_}a`P`F)o|X1!@~dJ zr!g0J@5AwCDkyGojJfIu3`Z|z+;nU>QeE6mE6JM?oaSwcY?vd&+NsLu0J~|E*@>WM zv;KaD*vk~Y*w)s&0IE2a%E0MH6PjN|Fh5_d=~ZBz02qCIo9@@P=-cobq(h0FTU~zd zOv(qssFd4bd5ygFzJAr0{6S9RrkgGlP1Q=sEIXS`?22vD;(d916B6kz0m`}bJ=!Ta zqcwY8bqOa`qw0uW<7NrOSkc&^RH7%u>^v0JwqUD}0*85&4IK%jygetq*X zv*EP6+rfG})hzAtk%%8Ze{5=m`XfjWO!+18odVb{U+s!hvMeKSb!Q>CiXB2A^z zSMFE3{m%uXXsml{S^R}VhhpewWI)l7JB83uT)7dH?iUAehj>A7IKP^#c;NPzqwT&K zyv1Ru3lHNhjaqaqIr-l53M7A>-JYJ}ISn^K&N|~68fkbsCn;%ZY;%ERi6UO#eXT%P zbpSd5bt;{c`pumSUHivjm`~TK71-~A$1QbhEmQwSgW3wC#!9c3jo0P)p*OM4%#=WN zK&ls&(-PO)!_A_Xda9vPF$$9$AR}1)WF%dMYf=(QHlgq_ddZneXDXj*8%u^&)JUeQ z199sG=BQ|XaDRWWb6==B=p~@RY?wokMXVMZp9(ywwzcARw@fI`{m86UqD^MS4KkpN zVAjac2$k8QS;zV6Z;QRG?^^khUP%{+stRPA2XWJ<>^NOiPZcp=0_SFEG{laNKMTRW zXtFOi|Ch_A0sQ=8G+in`Zkh>hl-MhiHhpUd342AVZ$nXub1LEo{_!`?20hQq8J&xp zn-omd9OxtaY1?l{ONu8}rOW?(ypR6!2xJe*abS6z!!=!3@+Gw{-|MeR!X;{-B*)v0 zk3&mi%fV;OO-RW)(ilY6j975Dwhr{$m|&_EM(dFXGP)j`L69HU&I2gR=A_`Pfqc;? zk%G&9Z)|JoOYKDwSGjoT%^85Z+olyMv%vmc)I3y2*J9T~-oK5CbqmE?JzhB}S7$$s zFOWbque+dnj0d;NlF%~&myw6hI>`XUG!Df#{B*-JLbv`z`BWa|@`>vEApz;n$QM*E z{WW>-${9k3Z_evUMivJB#1MzGTbl#?=#jHwJ4fGj*)Vj4dmtlH{xo)tDuVJ@|`H`odL3oZq+(vx!dg4F_>2Jc+fN`&DSih;cw*yvF zD{h*TVdstIKe0rstVch+o1??6g_3PkK;-qjP8)jd%Vc;M1I8_|a+^*s2Mb`eq&tIe z4Fe}V3mc=DyX$YWBe9i|yR6ZfpN^mJD{LXNx1xc$GC(IFNJB&4oVlkx3}l9#gVE%z zV=Gi{r7g+q=Y(5cN&*k$LbqGRCU)y|mP+PQ-e+@tLDh5MMc+zi3{+Lj4ad31{_bl1 zU1H@y4%W%c#>rX>l+4Fm=eewTh=sJrUo-diDaB>&P_n`xDXGBQF=|cwV!(>ECW#F~ zC3_W=Q(M;$_lDlj4|%Pe-7z|nlX3JvY3{xbS2XP#UPhLTq}nlm&mJjz`=R&8!AtN$ zQAJlWMcgyT<=lWV* z-%UmClMBBNLZf)(#-tm{EEj$nqPswqY>bSWG>iByK5{|>hO8nyTd<|H;oDFFtWxU$ zJ!~;X#YzHTKiIUAoKA2F%JcwG?|kl94fCR0FS}4^Ou%e9yl@DJ5MTO58!cS&zRHh{ zy4?{@h`_qv6@1Hzo$3nH)^=Yj+?y}h_I~k=}#&Cd!pfOJL z3~;<*z#_qEGKq08&_Qd4>Jyr_VewdE%I*W&xDB`rurOaYuamf;x+m4wdvj`hj%7-)~T_JrTRy37{kHPLmye{w8&T^B3EqD6P+iV0TrY z*(VygHj6Kw(k}Aei_uPM1rK{hVYRw0^ZK`qt4IfG+jU->_68OBZ7@9041we-gsPES zoawncxm>i!@8z{rMw3#57XYzs{L)`C_pGLAmRhtt3Kv(es_4wy)i%gWqAuUFV2%jk zsS+VPEyu69bYbE~=XFd66yAv_Pm-UEKk05b;U<9W{LYv0Uh^5TmL9|0-0Vs5k`hG> zpa6V=tc>HrE(_E{sM$|P91FxYhL}E6Xak5qM2j3|@W12^RB~EUZuBmK+m@D`*>9tCi118GmLF9Z?SZL3@I3gy zg3(*bTl&R5Pi`mS0h7ekd;+JTmaYqATfUw9?Bjrw7^2V3kHGKuLkrT6OtMxA_m7)% z&XNsaP?L|+_U9s`0)o6UU%PqL$gY}VC1y~eiG}A3Q-Z0{P_GUOxAZ5cE%mLrY^2z1 zDSLakdgLLHa|zOYtXnIzuovz&Q=%-7Xs>8W6;E2(Y1~q^oq=f=$CVH?X0eRkauOHQ z!)d~rL_-rz%0S@riqu+uzNhnPstm79<%*dcV;IM=HoWot_HCE`Biy@7jc1rAb8^i( zskz4wpG}rHj?clNRdn~hzkr>v=oTqtRlU3NL+$t9$z}_d&N4Y6jko>2Sr=S<#{1`k zqyjD5sgt(PR}q=EdeliXIWW7!q3AMU^qto6Jl*XkJ}tS18e1PZa61D5kF%qW7yt-B z5T1J8D#|G8EPU)X%G+yEkE?a&fU_i~d3bHtW5rMG{@41t7uvoYQum8$_(w>n2iS#= z^XDtq-I>YV6R%9!$@hJ32!H`eRZ^$ujqK^@XLozQ*PpMXw-l^PesA@r)1fbSr>bUV z_O_y%%%-v5uG_>!HEB;)9}^X!ZRJX5r^P7-l(4-y#C++RGDlbt|_BL zs@Y=D)l|@T;A&A-Jcw+lWDQ@DzIj1uf=5 zwS54hlp&FXCQML1lF}6AZpCGQh=a*dDizpDVl$?~e$&DQ5vW3r2-W=lv!*H$7U(!c zvPCu5zmmR&%a3raoYAvSawYrx24!;uSot9VysXS%It8ac^$RkAWK1q3D1q)EVV>Y5 zkV)+Xt=fkX^@Sxc6O)`9S2*2d9gq$5hA7T)k~~E%Y5*GzP}9b3KPBS2T>+OF!4sTK zd}^T$$T(1d1YxX4g71*>`F)^p1qv2hI(=$K6qq-3W|5`61S~nF4+U>gN0v+c$*Jf4 z(Bu4Ds9GVY*{n;;GKyphVd{~zrVBeR9tFOS9CuBD_;rphjcrrPCa9a`#UY1WqPKQv z@#9>D41mgkosX9=T2;T zLa=~wbIKhZ1XWV>UQ$!t*qi+-$(hy%xT7(b-m>3}paTKT}RCU1WU1ejm z`XKJNdFogFO&2lxaXSBN==BgBk^06<3%j{lx(P86K(_7tsYlQHEG{#*MY9@b?T$zu zn%9Vuy>m^a?pA1Tvw!-_=s@<%e@<&fMMe;TNTK=^ky6gx*fWs(9U7$dwdGWO0!0J~ zsF~N=76h>V1^|Kq7G)U8k{&GcAt=?Yp2bvJJ@VgIJVy>NX#d>*rBDNCcCa0g&K!KKi83+*7Mf(9>37S(WivU z^he;rnSrvR71C|dhczWQMy!q0;uNaqaXo^z+`eVv5USG z%6Y&sxS%RTJD8O1oUV10e7D!Dm*wedCQj=tHZu>rn$2G_(X5)@$l`KW!+XFR+R+4= zv_%V1(I<%fRQ1ppCV>HM0rk)0^$4|$`v#p{KAJn|*jxHDRQBa==QC4H+CVdi7w3SR6yV{K4aX-hnw3BvuFR^M zDr*{QDv>X;+vvf%TY47yapeT=Aq&oKHK0Qrb<+1Hk;(CjbXogqI517-J4F{E z%Z%j5BvYg6APMAyYVdV%dHdaCOnYO2BV`^!e>QNx)MTwpq{SK}tx}R}Uv8O&iM?Ce&hDX_hy-^_gc%J#JW%8GiSr z(hYT-&9@6DNl1LO5$bXep4r_TO%!CRnCNG%@sB2eHu_~+)pq@%68~b@0 z-Y3$+ysY+mtl)2MXIkqGMMx92D7|%k^nc@AbbGS(c*a>tMPtb9R2KF=HSAva{6{;d zt*z5mw2nD_V&PJUj8wvfjqxfd3cljK;e|w6%1B@@>V;C_UAp9U9jP0utsgt+@tcLA z{xn|c@k{`$Y>9;TrA4ii@K@3_9eqm_Tb1Zen;tlXl4T*e8?I; z{`{}&m7yRP4pVqpck%wBaoh_a61@#~f6k)RW&u znI0B>7}*8pDd%ejKomwIsL|N{nCn>$@tuypDo(Tq!o|)=#fll;#8o4qG9xbC6Z34T z|N8uJT?th?P=)7q9)_V1p4_kE5y*;pi)G~i z%cupQbHgbio15pv?GosJ(kVJ+;?i~PmN(j9){j(1N0C&*w33hyjsMi@YgBIuV@@@=LgzkNJn|J*(K?wNIeJAC|}^y}X3 zoR!R{8!k^yLo%^4P*ToMoqzRg3T{%K+@a}#3qz_iv!OtG28a-yzg;Jxxt%~L6wR<% z+j>!ZAoZSOW$Eez;FWQBQ_`hKEo`(LNpKY8?)1G)uVOQ&F_vr2Tmeoom|EPDdRnjd z5@Dp8jfFBI*pL+*6ETT65(yfQiHb2vqUA&HmiqiA0DWS{Piq}wvLk9z-!Qd96Rcs_c-*=$tG0(xF!*A4Q{GkDR^9u3`_cGCfH|o-CjR~1 zX*{V#RuQ$fY4gfAZD)Hk(s5JuKeoD#Un#M1mJ`cZi74JDo6CRKeG-J^#$gXc zs6vCEwF_%OI|`xuZH^G-#dqOTX=>XATLk`nHIE!Ra_PDOb(DdO9yrmO$I%@u2MnCH zua5>4GnnCHyB<3ES4XUl+!TILI8npShvoH@$-$>P{y6&gl`weU_6#-)LupHJ5A%XV z>_VQR$tOFy*}6+Q9F&A8DbW-FzRiY$7vn-v9|Wft)_Au<>`?aXN4PEKF;vf^uJ2N< zs9Z2`h^FzD;xEmTWH4SO6xU0ny|k#fj%>V{5TzXb|QsjSOC4as-3?t6+@ymfo*V7C9#k;40|^Prr5hy!^SfS!M<+>|h7 z@G8y7Xwb9P>gDf!Nimpx8sTM;S5*0TXKi{Z$4t#Q;z;8}WaxhJ9nsp}ryu6jcPa20 zM1tG8XMOxlm!_AGe>@SVk)?G8P-^3@spQ9V)#*F_WMM;GEK+0sTkbMFs|5J`vu4N2 z?|5l_iHt4?t-qn}N8n&ATlzHWYro}gh_UhuM7ec4-^i8Tn;Jkv7YF2b=ttbXK$2CA zzwdCI8_JCYDkrGK0f3m$KL9}A6Jf7t;iapjlN-5rh3lBD9uMv(gMr!3YWxRx3pvw*;#BmIpsL`@72PoYMg}x2XZm{WG8u3ib zZK}*b5he17{}(yGRzy6vAC09bz8UD9Y%HLcpxn%|S*4V~?S@60C5?nZB#wu2;FALX zNQeugR?z~gfThTE#=~pI-bGgFU(AcDa3?1e1PH0-F`e#eEVtR(+AO$|koj-4%wo)| zZ3bzj)Fn<=+>3!Qx85vu>5BjF&*S>40gh+ORlgkZIupAH&JIk3xv9hBprTY^es8*! z5ZgP~CgC)JpmC0JhJVIFy5={9YuUkk&_~aJiYKCKUs%0#9Kz$(jL!PrDMw+PF^edYndVB$Os~TS_U- zN2}GEJWXd{c6KyaQXZ2SM#(hasgfhZmG6ta3y|D_I1ma5n$icpxpB72Ty>d%EdwW( zk&izhXum8tGzie^Ql8Nh{}=@+zK%U*yxB=vSlYVq0jM8GAAqjO3Qb_e)NPS_Y53jyb19_$4vggM&Au`<5;Zq=M|Rd+BCj0626 z+_^MzN=b=u5z0Sa3{81-XxH8k?_yYBb>n0ECk6vswxETVRn3iO`mcsH{+ajt&APv3 zmg6BY#8j=FDZ~m{O)= zZ0`Ng?%KMUJj47eLw_W-)-#Pz7429A4!K!p55}ysek1fEJ&sNJw-u+{kR-kDJD{6TogyYadhV+qLT4xd@ z6#ys%uq;t?0u-`^>S|3w5W?aYIPV@t$i;a|3H31StiqurT2tJh%=pac@`Z0Wz;>2} zj#0{)3x>_l_-7C+E?n5yGhCNG8rjZ-l|jS-1A{z!*tv!MVJ+fo^jj@#%v&_P3pC6V zG*pd`7uyqC{khZojw2jxsBD_pD!s_v zw?oIR27nLjosr3XOgZ||!9B5b+SuNZ!`b`a-_l%4;6Cr?Q9&(lvR-o82}%2G4wI7kFpIka9NU*mBsb#BvI%1#5H;GPja>_J{XLK zCX7SBzoo2H&4Hh&umx3=9IVR5jm3c-3~_$w3d1(rGJKKom6^7-Dp=htNj<7A^x|1P z*039A6~g;mgUAi<$XFUa*J2j|0mj6#W^F`y>Xiv`|9EW;31M5?t!)l&Jr?)x5ub=H zaW6gh+<3x#x=dtRS>e$XG&2Q97g$t16~&|c-Sl@_ndgF^#3gIjfd%Q#A%OJGo=M{M zv19Bj_L(k~?@{0;PoHIsvJ9%J;GN#TXvB36M!ep3fGJV}2X22I#dmw*t!KX57fB&4 z)EbUUahb93n|?qRnIqq^Z-?DDS+@?i9A&f9R8|g#Cutt${L=gy_aGEoncx03*e&m* zc2AW?pMiBh5p#ouC*8qa?(a^|Fk_6F1UBe%P_+4L{WOd=`DVcsUU% zCtV)khnrb<1@p(R&suHYH!FUngQ#d*Js8m#I5iu7ZDeAcjU@7?4s3@tav=SBJ~*iQ zZ>c?#ijH0{%d<4@SMpZ96j?tKm2&QcT!F#b6A51qYrju_zPm@QSxtn4AWqNSd ze4rsP9fCklren^i(IA74Ac~KKin>2F!WG^L7dUzdLHj}g8Eyl%CzOr`iTuF>B0Z_u zeCjkR4B~}h3}Kn_bYnP<*dQQygCTYZ5#e| zF|mqe)&uJm4xgi=$PR-RRO1eKz;E!hO#K>D%g@7KeYiv_6#7YXVL{R> zn(4#J#3b_X`mEI<^4W0}2Zvp&!U!;cND=T#_BLUNA{{P(AY{V6Zfg|niUr5iMGt|@ zpRro;s|HHSeE72@gcQl?u^Ip(g9~>0=>+0kF(ex$N^R`Qg4M&=!8RcfT4A6HCF&c8Fi zUr3is)xH)Jzu&LoBXs0VOLU;3#iEVTcLV@pRJGE1GNMV3>Pkzy-9mDGrvAWwKHRA! zbI*2g$m`J=kHZnN=s8aXELOwUy`2zNSn9Z0&->`}L$aPJBp527m9dOZ0eXV)e~$@} zeT;p2vHrs4Pu+4jEw_N|J7WGT(rgzF*E{=Kc8vOA+TMTqi;Y_r4~F|zJ`2%FKupRwYfhATwLv~-g! zCp*Bio<$nJq&wRAX87-u>gRXcfZOaBOx?oBvLr!|ODlS^p!lVr6)K`~@J5MKn7oQu zK$XNo(1AhFmO;=>s`qhKkB;uv>nH181`P*qC<_4^o<6VM_6SqX!pm*Vs#vl;fG3vL zUvZ^VOk8b#M&nHF=SyI&qzg#g$QFhEIh!AfqcGY-!3k|KSNs;oZ1J?IHQGnT{OH<| zI5^fW%<8)Bzew1hbH167^2JE2UOKx{&uD=3=Zt=IRw(3?96B^tGZ2Ba#6SPWzzNog z>y0;xu9R=#^;H_eF{|Y^MEHX)C5TGbu4k&zh;Mu3iBhoB6PfauB;I?wNJ+bWpoI(R z;X*q%Xsy$x{6oaIK-O}ioD4ZNXXBPcw1z_2(p=}kIOpJM;S5{MJ{&n5*o;!b>$#^cPX7bxuZZ=~=qqVlS`pRRHeIa19!oNi~1lAHg zg*yOw2dlCP%103jCJ3;!g1a>!JeHHV2g6SYf7u(-F2 zkp~Vb`tvrruu0SA3yd&BGbbOH3p1FoO9w_SMsb?Gq83ZC=AqZ#!r1g~p<3X%ul<_p z2z_n7;)jiQgJ&)Izf<7|DR}g+@x7OUd$A--+nF<1lSC16md2+j-&O8@S*&Gqr4Q=c zu{o+XZiaGi>>Tk~_|o3!XV`i6|NIbe-(Q}kAMYT)z`bxe6LA~;1KFB&y^RsqQNC-m zyGpVC=^Y@=tNy}^-NNa#bMBmq>RE{S4Q-C|V`+B|6@Li|ZppkT;XyX^%ndEx*xPw2 z)hmWR54+e691~MaSl-$<_qqDnJ{K#*`=0=*5LWNWJ34K`DAKOss$d*?eczm)aOp-X z9ekmBattvn0qn7RhnZZykF;xc45 z#nH1A1lt#{`N}Ekvawia1F&D47Dsc1$x?p~>Lkq{_$$j09na&%8w@GEirrocQSH0$ z4FQTm<4-?8kbte&<)z?&zWN3>^cleL)K+`~FaBAU4rx1`#{`tL;(4?-y3qo_2G%fl z8*sz0Q4GWzB|Ow@m%QKg$Ae?cQRYUlRLSiSvYU>Y6jnZUBj%-beChNHi&5>CDi*SY zDju_fkwj%4;i}jyHlN|Z=0V~MG&P6FpgKhHTP|gl-FI4PANPwLY{}+vTy#N@rlob~5SH{L}QNVX0A{sy~FT*ft65eR0j3voIS5 zN_Bh<$TJs<1$M*rgwf;6FMU;iDdf_!i_5~TPgf*p5)!oW+Jtz&BK8$5%?g|4K=w|_ z4vzMYo}xaVkR2sEBd|SEwii?-R80+QU_TaddWf{GI^gW~d5dfAC6t6=T zFmD;ZgTIUi$A4iRVEm(na$bwQL<>(ISJ;&1Yz>k^3$?cBb)q$}H<@aU##&P|Xg1B= zv({*i)rFl)~SzuA%-N2})j5R+Xp@8Fq+$k&2V zj}ebPkN&B@sJG~yE8^E=vp11l5*Nfz+w@1=fnpXCHB%05xBhDd_R#|^F}o+s!S7yn ziI5vLp<+k3^sKF?DeQQ^3sYUxUIvG~)y}d5H`o9L?Y_UA7t9UqI_5so_ns`)TknP3 z*jFbwrzCEs;W}SwL~_jn+-09YuP+9z(XPETuZS7Xa;W~EcUgQ5Bf-@bN~Q1}718xz zb0gsEDDF&XhO6T=XB}02*UO)t{WLjD_3kTI6RgnzvB5IGUg=X+Puq05q$*5Va_)P~ z4XIBHu-jJ^i`UJr!^Y?pYd$ci#Lf`j9;(@sS=mO@zWrTgWh3R~E6dCJ%F6qi zn&RUYG!+@SM%2jj2N0MY7hezHK58TsV|rAOcaOK8y(b#W@KyPW@9Lm$KS)}0H`|{R zPSiNpDVJP5r&$y7<92;L#r0bsm$Ou|8(-EH-X)8Z9S%PXu9)^PQ_D3;YIRb9xe*KW z=c4$lT+nUyu5=Ya`ic67oze>$_mFjvFqz5$5)D`*!5PEH0U}=9Y zaW!IVrS9$WUAQ4PCl|oA&7Yg+m9KDiO{=da4-mHyKmRSspIbgHD{{mYqCh6vND*(O zOX=ui@6RKC$drv@N;{bqD8g|k`+o#DGmp*UAdUivR`~T*5le@^FucSc=`ZC%et4sf zgJi3dn*2@FLPjB{kO{b&lB-E)uVPlU$)b6){^4_QXE|3u-=A$^xGZh~CbRPLY_6^^ zU*_dn~*KIKU7&nY7tuOktK4|@Sb&ZZZd>4DzzKND5x;*PFu{j*xjBN%P zY~3?eICrLwHK7AmWPw`;aP@R!6~2@VR{J!!Pat$!dpb+xV>?v@3Iy_WU&HZMBrsMxko9t znVYpw7GW;%lkxp$|Z7@|L^j^zdtuMq}{WAe|g%ex8{#mZNl5I`9_C^mX%E# zC;h7ZmGlZ?(0qy>5nRGVfnaE3epKZ~l%#|poC=-$bOnjw@{d6I(uVxc>gm5}{aOBD zDFJO1&$X-pbdb5$liK8858QFt;m*c~?1tyGGrq={;9R`&HJ#%bvBb=B-g0infh|*5 z<|PqcO!}KjJU2MQ$k``(ek=T8)7(rKp)12}BK$%)$b<+RLAqL<>}Sk~&lObu&fuyd zmY6ucTW%!Z*y77EEs0QZ8NXKwa^o|MerG&Cgu6V#aUVjD3;fC+um z>bGSVKRBG>C1I1WM-sMYlOE%5Z%{!OXM%*TEU;x zxK*o2H$9$|UMs!vB+X^7OWG@$nIBeZUTp?oRM>nIER%puJJmYM)p8B4#(`bE+r0|; zC=)5MogEp%#@_3dk$JDK>pmhUdkdsHV_}k2F+Jk-ABPv3TN7tA%$V^fOWgV!&&)I> z9i%c$Fo0441OfKsjN45&fr+9M=Rax3r8oU_{)Aj(N*U1xjyU5!LLi>{vm6&vTe_(k zvkZ?9Td;Kis4gb}VThDry;zUjY>TX#dco<9ZX<;W1a^J->JBq!x4UkZZ>fkmnwXhg z-Y2H7jEqxw$*ImcPRN*=9y6{qcOiq_F4xI4Wec_qm1FLW%Is0$Ffn@^2ATP3s zt66a95a;vpEX{@Ul1b)IRQDf;T@+YwwiudFN7QWC=0y<<;ENg>s3!~ang z7U~rmtO{1nsMB5imQr`D$EyeAWjJzQ)c!N}TRpZF%>O)MpYh;m)6b9o99#Fn{&}hl z0On1R=3dktIL6+2;-MzH0Omb<5XcPI9{a(AeXfrV zP|+?m!06YCT-6;q)OVeL#ev@M!2eUvKOY zr6CMZy`&mI+Wg0#EzGS_lbr$cBYUffyo~LezKa5FppFMIP^S&_n{D!!fLdGhJky3F zr5yKBp#xa9UwiAT92^%aoy>86PSUr^d(QJ>0_22&&HRgfIe-G;!p{GfmHl4=0Or|_ zo22>1NAu>%$Z!xffFb9(=gpgM4}-Rc*|&d?`>_5HP^$siE$Fttj-2z!>+C08)4yXQ{`*=Js=$uV89dCJ&rc@rMuHB# zM!x97s$$72Ur(l&>fwh@-15RcITj2mJe?aZhK<`l-#s|cO~4K~r3I=UhE*6~&ivlA z@(cQ!gw1vLm;ErA`^^%zd9a^@|6n%{zvsf=`pu5YpUK1Tw9l|K{7KHC^+Nvz_VAzG zc?wisp`~sB=6Fx8sZ1a`Ph5MxcLoM@oB?csALHegIs?1m)f~DB0(gnHbl#*x_rMP8 z1@D8-1`a4P11PX)y+G{!kW=gMM zM0t}?tqJ9Ca3yHcPrd$J{}04I;bHR8RL?I!AT3}I%YNXKn?=^cN+}+Gjt3xwbil9s zvWes$AHuI&fESbEyOaSlrB^Vbyh#I1t*W&A4Xy-@TQ4^2X*u$LAoi;dti$y3mo52) zVWClr-X50y;KV04i>!xToKn1{KkZZq@+GB%5A9{Mi02<4*i0*yybz%AJ&W%He@aoE z>zL9Cij+}SImLfF;7U|fNo7?4M40?z#KWBnskE`q2VoQ^X_gmdRX1(d592g1>$V@~ zbwBU-10Vz=D25XxMKdhN3!)?|YHu6=FKD`9TDIeQeh@}+l4f~PR&~>M{V-1RvTpqL zlDVkw9UJxZ&Q8nE#E!%NDKM12ZNwd5ttGa2sei)~DS-1T-ukR`P z_xr=iEKv+6NQ!1yj{go&uOus~rW>YZJFe#k;p8@tlQheVvZ|Z5>xXf&xg#C}%zp8~ z%UcObju=sT!NBx1TTuh@Rw~ao{rgT=;XPgGx&$qDY#73}`JMk6g z6+dqO=@QxH&5M;jPHeK$=as%}|8?w_>2VmZE_=GIYre)|?+~mKtH@Db(PoQ)GK|2{ z9 z6gPm;C?S*aa)CRNx2Adqb(E>FLFJni}&g2;`VU?1vAU+{C(3I!`{!cJO>6_wc z^qOSxUH%m3z6Ybul1&jq@;Rexu)R7|D}J}Df&kTPH56Y414<8j z1&0QkBiM`A{1ohI&L#R%nAiiqXEUqIg#{{kQ>)rM`9VH45hp!gdr*bi_Ww?Sa=ecS@Pl<=&-dd zAdG4Px$y)I_F{#wH;>4(%B2y@+5`m=P-=&@e2G@&fiFzaH=;`BRJcM*j#RLX&ReU`xiO_ERvA9mO`C4JKOEv~Lpt;Q4J^UanCXMP`#gPaFkpd)M>>PEj=x|F}7ND9D0|(edhJlGzwI5;Z zilczkk$cZQsf`CsoW-p^GM`R76NxFpcf-y~S5TEi9&HyHO`tZDe@iGj46n*HlbYBu zfSsFkW^H+I)HX}E;9!mTjJ{4~xBM-dpysRTR_>I-*ozMsum9^%7+x;XQ1Ck*f);2$WG#wtB_=v0Td{Ki9;CJJ4+dl85{ja} zmtUasfssT?M>at5y_hszN*qQcTFS{uMDBqg+{J8U;Ru+ jm0^zB0rwW&8q&GikN$q17~Y)XgDR=hHNSc_ks|K^ \ No newline at end of file diff --git a/app/assets/fonts/openproject_icon/src/import.svg b/app/assets/fonts/openproject_icon/src/import.svg new file mode 100644 index 00000000000..e7c50a49ba2 --- /dev/null +++ b/app/assets/fonts/openproject_icon/src/import.svg @@ -0,0 +1,53 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/assets/stylesheets/fonts/_openproject_icon_definitions.scss b/app/assets/stylesheets/fonts/_openproject_icon_definitions.scss index 5ea57592b7c..885c93ea0f3 100644 --- a/app/assets/stylesheets/fonts/_openproject_icon_definitions.scss +++ b/app/assets/stylesheets/fonts/_openproject_icon_definitions.scss @@ -448,1011 +448,1023 @@ .icon-export-atom:before { content: "\f14a"; } -@mixin icon-mixin-export-csv { +@mixin icon-mixin-export-bcf { content: "\f14b"; } -.icon-export-csv:before { +.icon-export-bcf:before { content: "\f14b"; } -@mixin icon-mixin-export-pdf-descr { +@mixin icon-mixin-export-csv { content: "\f14c"; } -.icon-export-pdf-descr:before { +.icon-export-csv:before { content: "\f14c"; } -@mixin icon-mixin-export-pdf-with-descriptions { +@mixin icon-mixin-export-pdf-descr { content: "\f14d"; } -.icon-export-pdf-with-descriptions:before { +.icon-export-pdf-descr:before { content: "\f14d"; } -@mixin icon-mixin-export-pdf { +@mixin icon-mixin-export-pdf-with-descriptions { content: "\f14e"; } -.icon-export-pdf:before { +.icon-export-pdf-with-descriptions:before { content: "\f14e"; } -@mixin icon-mixin-export-xls-descr { +@mixin icon-mixin-export-pdf { content: "\f14f"; } -.icon-export-xls-descr:before { +.icon-export-pdf:before { content: "\f14f"; } -@mixin icon-mixin-export-xls-with-descriptions { +@mixin icon-mixin-export-xls-descr { content: "\f150"; } -.icon-export-xls-with-descriptions:before { +.icon-export-xls-descr:before { content: "\f150"; } -@mixin icon-mixin-export-xls-with-relations { +@mixin icon-mixin-export-xls-with-descriptions { content: "\f151"; } -.icon-export-xls-with-relations:before { +.icon-export-xls-with-descriptions:before { content: "\f151"; } -@mixin icon-mixin-export-xls { +@mixin icon-mixin-export-xls-with-relations { content: "\f152"; } -.icon-export-xls:before { +.icon-export-xls-with-relations:before { content: "\f152"; } -@mixin icon-mixin-export { +@mixin icon-mixin-export-xls { content: "\f153"; } -.icon-export:before { +.icon-export-xls:before { content: "\f153"; } -@mixin icon-mixin-faq { +@mixin icon-mixin-export { content: "\f154"; } -.icon-faq:before { +.icon-export:before { content: "\f154"; } -@mixin icon-mixin-filter { +@mixin icon-mixin-faq { content: "\f155"; } -.icon-filter:before { +.icon-faq:before { content: "\f155"; } -@mixin icon-mixin-flag { +@mixin icon-mixin-filter { content: "\f156"; } -.icon-flag:before { +.icon-filter:before { content: "\f156"; } -@mixin icon-mixin-folder-add { +@mixin icon-mixin-flag { content: "\f157"; } -.icon-folder-add:before { +.icon-flag:before { content: "\f157"; } -@mixin icon-mixin-folder-locked { +@mixin icon-mixin-folder-add { content: "\f158"; } -.icon-folder-locked:before { +.icon-folder-add:before { content: "\f158"; } -@mixin icon-mixin-folder-open { +@mixin icon-mixin-folder-locked { content: "\f159"; } -.icon-folder-open:before { +.icon-folder-locked:before { content: "\f159"; } -@mixin icon-mixin-folder-remove { +@mixin icon-mixin-folder-open { content: "\f15a"; } -.icon-folder-remove:before { +.icon-folder-open:before { content: "\f15a"; } -@mixin icon-mixin-folder { +@mixin icon-mixin-folder-remove { content: "\f15b"; } -.icon-folder:before { +.icon-folder-remove:before { content: "\f15b"; } -@mixin icon-mixin-forums { +@mixin icon-mixin-folder { content: "\f15c"; } -.icon-forums:before { +.icon-folder:before { content: "\f15c"; } -@mixin icon-mixin-from-fullscreen { +@mixin icon-mixin-forums { content: "\f15d"; } -.icon-from-fullscreen:before { +.icon-forums:before { content: "\f15d"; } -@mixin icon-mixin-getting-started { +@mixin icon-mixin-from-fullscreen { content: "\f15e"; } -.icon-getting-started:before { +.icon-from-fullscreen:before { content: "\f15e"; } -@mixin icon-mixin-glossar { +@mixin icon-mixin-getting-started { content: "\f15f"; } -.icon-glossar:before { +.icon-getting-started:before { content: "\f15f"; } -@mixin icon-mixin-google-plus { +@mixin icon-mixin-glossar { content: "\f160"; } -.icon-google-plus:before { +.icon-glossar:before { content: "\f160"; } -@mixin icon-mixin-group-by { +@mixin icon-mixin-google-plus { content: "\f161"; } -.icon-group-by:before { +.icon-google-plus:before { content: "\f161"; } -@mixin icon-mixin-group { +@mixin icon-mixin-group-by { content: "\f162"; } -.icon-group:before { +.icon-group-by:before { content: "\f162"; } -@mixin icon-mixin-hamburger { +@mixin icon-mixin-group { content: "\f163"; } -.icon-hamburger:before { +.icon-group:before { content: "\f163"; } -@mixin icon-mixin-headline1 { +@mixin icon-mixin-hamburger { content: "\f164"; } -.icon-headline1:before { +.icon-hamburger:before { content: "\f164"; } -@mixin icon-mixin-headline2 { +@mixin icon-mixin-headline1 { content: "\f165"; } -.icon-headline2:before { +.icon-headline1:before { content: "\f165"; } -@mixin icon-mixin-headline3 { +@mixin icon-mixin-headline2 { content: "\f166"; } -.icon-headline3:before { +.icon-headline2:before { content: "\f166"; } -@mixin icon-mixin-headset { +@mixin icon-mixin-headline3 { content: "\f167"; } -.icon-headset:before { +.icon-headline3:before { content: "\f167"; } -@mixin icon-mixin-help { +@mixin icon-mixin-headset { content: "\f168"; } -.icon-help:before { +.icon-headset:before { content: "\f168"; } -@mixin icon-mixin-help1 { +@mixin icon-mixin-help { content: "\f169"; } -.icon-help1:before { +.icon-help:before { content: "\f169"; } -@mixin icon-mixin-help2 { +@mixin icon-mixin-help1 { content: "\f16a"; } -.icon-help2:before { +.icon-help1:before { content: "\f16a"; } -@mixin icon-mixin-hierarchy { +@mixin icon-mixin-help2 { content: "\f16b"; } -.icon-hierarchy:before { +.icon-help2:before { content: "\f16b"; } -@mixin icon-mixin-home { +@mixin icon-mixin-hierarchy { content: "\f16c"; } -.icon-home:before { +.icon-hierarchy:before { content: "\f16c"; } -@mixin icon-mixin-hosting { +@mixin icon-mixin-home { content: "\f16d"; } -.icon-hosting:before { +.icon-home:before { content: "\f16d"; } -@mixin icon-mixin-image1 { +@mixin icon-mixin-hosting { content: "\f16e"; } -.icon-image1:before { +.icon-hosting:before { content: "\f16e"; } -@mixin icon-mixin-image2 { +@mixin icon-mixin-image1 { content: "\f16f"; } -.icon-image2:before { +.icon-image1:before { content: "\f16f"; } -@mixin icon-mixin-info1 { +@mixin icon-mixin-image2 { content: "\f170"; } -.icon-info1:before { +.icon-image2:before { content: "\f170"; } -@mixin icon-mixin-info2 { +@mixin icon-mixin-import { content: "\f171"; } -.icon-info2:before { +.icon-import:before { content: "\f171"; } -@mixin icon-mixin-installation-services { +@mixin icon-mixin-info1 { content: "\f172"; } -.icon-installation-services:before { +.icon-info1:before { content: "\f172"; } -@mixin icon-mixin-italic { +@mixin icon-mixin-info2 { content: "\f173"; } -.icon-italic:before { +.icon-info2:before { content: "\f173"; } -@mixin icon-mixin-key { +@mixin icon-mixin-installation-services { content: "\f174"; } -.icon-key:before { +.icon-installation-services:before { content: "\f174"; } -@mixin icon-mixin-link { +@mixin icon-mixin-italic { content: "\f175"; } -.icon-link:before { +.icon-italic:before { content: "\f175"; } -@mixin icon-mixin-loading1 { +@mixin icon-mixin-key { content: "\f176"; } -.icon-loading1:before { +.icon-key:before { content: "\f176"; } -@mixin icon-mixin-loading2 { +@mixin icon-mixin-link { content: "\f177"; } -.icon-loading2:before { +.icon-link:before { content: "\f177"; } -@mixin icon-mixin-location { +@mixin icon-mixin-loading1 { content: "\f178"; } -.icon-location:before { +.icon-loading1:before { content: "\f178"; } -@mixin icon-mixin-locked { +@mixin icon-mixin-loading2 { content: "\f179"; } -.icon-locked:before { +.icon-loading2:before { content: "\f179"; } -@mixin icon-mixin-logout { +@mixin icon-mixin-location { content: "\f17a"; } -.icon-logout:before { +.icon-location:before { content: "\f17a"; } -@mixin icon-mixin-mail1 { +@mixin icon-mixin-locked { content: "\f17b"; } -.icon-mail1:before { +.icon-locked:before { content: "\f17b"; } -@mixin icon-mixin-mail2 { +@mixin icon-mixin-logout { content: "\f17c"; } -.icon-mail2:before { +.icon-logout:before { content: "\f17c"; } -@mixin icon-mixin-maintenance-support { +@mixin icon-mixin-mail1 { content: "\f17d"; } -.icon-maintenance-support:before { +.icon-mail1:before { content: "\f17d"; } -@mixin icon-mixin-meetings { +@mixin icon-mixin-mail2 { content: "\f17e"; } -.icon-meetings:before { +.icon-mail2:before { content: "\f17e"; } -@mixin icon-mixin-menu { +@mixin icon-mixin-maintenance-support { content: "\f17f"; } -.icon-menu:before { +.icon-maintenance-support:before { content: "\f17f"; } -@mixin icon-mixin-microphone { +@mixin icon-mixin-meetings { content: "\f180"; } -.icon-microphone:before { +.icon-meetings:before { content: "\f180"; } -@mixin icon-mixin-milestone { +@mixin icon-mixin-menu { content: "\f181"; } -.icon-milestone:before { +.icon-menu:before { content: "\f181"; } -@mixin icon-mixin-minus1 { +@mixin icon-mixin-microphone { content: "\f182"; } -.icon-minus1:before { +.icon-microphone:before { content: "\f182"; } -@mixin icon-mixin-minus2 { +@mixin icon-mixin-milestone { content: "\f183"; } -.icon-minus2:before { +.icon-milestone:before { content: "\f183"; } -@mixin icon-mixin-mobile { +@mixin icon-mixin-minus1 { content: "\f184"; } -.icon-mobile:before { +.icon-minus1:before { content: "\f184"; } -@mixin icon-mixin-modules { +@mixin icon-mixin-minus2 { content: "\f185"; } -.icon-modules:before { +.icon-minus2:before { content: "\f185"; } -@mixin icon-mixin-more { +@mixin icon-mixin-mobile { content: "\f186"; } -.icon-more:before { +.icon-mobile:before { content: "\f186"; } -@mixin icon-mixin-move { +@mixin icon-mixin-modules { content: "\f187"; } -.icon-move:before { +.icon-modules:before { content: "\f187"; } -@mixin icon-mixin-movie { +@mixin icon-mixin-more { content: "\f188"; } -.icon-movie:before { +.icon-more:before { content: "\f188"; } -@mixin icon-mixin-music { +@mixin icon-mixin-move { content: "\f189"; } -.icon-music:before { +.icon-move:before { content: "\f189"; } -@mixin icon-mixin-new-planning-element { +@mixin icon-mixin-movie { content: "\f18a"; } -.icon-new-planning-element:before { +.icon-movie:before { content: "\f18a"; } -@mixin icon-mixin-news { +@mixin icon-mixin-music { content: "\f18b"; } -.icon-news:before { +.icon-music:before { content: "\f18b"; } -@mixin icon-mixin-no-hierarchy { +@mixin icon-mixin-new-planning-element { content: "\f18c"; } -.icon-no-hierarchy:before { +.icon-new-planning-element:before { content: "\f18c"; } -@mixin icon-mixin-no-zen-mode { +@mixin icon-mixin-news { content: "\f18d"; } -.icon-no-zen-mode:before { +.icon-news:before { content: "\f18d"; } -@mixin icon-mixin-not-supported { +@mixin icon-mixin-no-hierarchy { content: "\f18e"; } -.icon-not-supported:before { +.icon-no-hierarchy:before { content: "\f18e"; } -@mixin icon-mixin-notes { +@mixin icon-mixin-no-zen-mode { content: "\f18f"; } -.icon-notes:before { +.icon-no-zen-mode:before { content: "\f18f"; } -@mixin icon-mixin-openproject { +@mixin icon-mixin-not-supported { content: "\f190"; } -.icon-openproject:before { +.icon-not-supported:before { content: "\f190"; } -@mixin icon-mixin-ordered-list { +@mixin icon-mixin-notes { content: "\f191"; } -.icon-ordered-list:before { +.icon-notes:before { content: "\f191"; } -@mixin icon-mixin-outline { +@mixin icon-mixin-openproject { content: "\f192"; } -.icon-outline:before { +.icon-openproject:before { content: "\f192"; } -@mixin icon-mixin-paragraph-left { +@mixin icon-mixin-ordered-list { content: "\f193"; } -.icon-paragraph-left:before { +.icon-ordered-list:before { content: "\f193"; } -@mixin icon-mixin-paragraph-right { +@mixin icon-mixin-outline { content: "\f194"; } -.icon-paragraph-right:before { +.icon-outline:before { content: "\f194"; } -@mixin icon-mixin-paragraph { +@mixin icon-mixin-paragraph-left { content: "\f195"; } -.icon-paragraph:before { +.icon-paragraph-left:before { content: "\f195"; } -@mixin icon-mixin-payment-history { +@mixin icon-mixin-paragraph-right { content: "\f196"; } -.icon-payment-history:before { +.icon-paragraph-right:before { content: "\f196"; } -@mixin icon-mixin-phone { +@mixin icon-mixin-paragraph { content: "\f197"; } -.icon-phone:before { +.icon-paragraph:before { content: "\f197"; } -@mixin icon-mixin-pin { +@mixin icon-mixin-payment-history { content: "\f198"; } -.icon-pin:before { +.icon-payment-history:before { content: "\f198"; } -@mixin icon-mixin-play { +@mixin icon-mixin-phone { content: "\f199"; } -.icon-play:before { +.icon-phone:before { content: "\f199"; } -@mixin icon-mixin-plugins { +@mixin icon-mixin-pin { content: "\f19a"; } -.icon-plugins:before { +.icon-pin:before { content: "\f19a"; } -@mixin icon-mixin-plus { +@mixin icon-mixin-play { content: "\f19b"; } -.icon-plus:before { +.icon-play:before { content: "\f19b"; } -@mixin icon-mixin-pre { +@mixin icon-mixin-plugins { content: "\f19c"; } -.icon-pre:before { +.icon-plugins:before { content: "\f19c"; } -@mixin icon-mixin-presentation { +@mixin icon-mixin-plus { content: "\f19d"; } -.icon-presentation:before { +.icon-plus:before { content: "\f19d"; } -@mixin icon-mixin-preview { +@mixin icon-mixin-pre { content: "\f19e"; } -.icon-preview:before { +.icon-pre:before { content: "\f19e"; } -@mixin icon-mixin-print { +@mixin icon-mixin-presentation { content: "\f19f"; } -.icon-print:before { +.icon-presentation:before { content: "\f19f"; } -@mixin icon-mixin-priority { +@mixin icon-mixin-preview { content: "\f1a0"; } -.icon-priority:before { +.icon-preview:before { content: "\f1a0"; } -@mixin icon-mixin-project-types { +@mixin icon-mixin-print { content: "\f1a1"; } -.icon-project-types:before { +.icon-print:before { content: "\f1a1"; } -@mixin icon-mixin-projects { +@mixin icon-mixin-priority { content: "\f1a2"; } -.icon-projects:before { +.icon-priority:before { content: "\f1a2"; } -@mixin icon-mixin-publish { +@mixin icon-mixin-project-types { content: "\f1a3"; } -.icon-publish:before { +.icon-project-types:before { content: "\f1a3"; } -@mixin icon-mixin-pulldown-up { +@mixin icon-mixin-projects { content: "\f1a4"; } -.icon-pulldown-up:before { +.icon-projects:before { content: "\f1a4"; } -@mixin icon-mixin-pulldown { +@mixin icon-mixin-publish { content: "\f1a5"; } -.icon-pulldown:before { +.icon-publish:before { content: "\f1a5"; } -@mixin icon-mixin-quote { +@mixin icon-mixin-pulldown-up { content: "\f1a6"; } -.icon-quote:before { +.icon-pulldown-up:before { content: "\f1a6"; } -@mixin icon-mixin-quote2 { +@mixin icon-mixin-pulldown { content: "\f1a7"; } -.icon-quote2:before { +.icon-pulldown:before { content: "\f1a7"; } -@mixin icon-mixin-redo { +@mixin icon-mixin-quote { content: "\f1a8"; } -.icon-redo:before { +.icon-quote:before { content: "\f1a8"; } -@mixin icon-mixin-relation-follows { +@mixin icon-mixin-quote2 { content: "\f1a9"; } -.icon-relation-follows:before { +.icon-quote2:before { content: "\f1a9"; } -@mixin icon-mixin-relation-new-child { +@mixin icon-mixin-redo { content: "\f1aa"; } -.icon-relation-new-child:before { +.icon-redo:before { content: "\f1aa"; } -@mixin icon-mixin-relation-precedes { +@mixin icon-mixin-relation-follows { content: "\f1ab"; } -.icon-relation-precedes:before { +.icon-relation-follows:before { content: "\f1ab"; } -@mixin icon-mixin-relations { +@mixin icon-mixin-relation-new-child { content: "\f1ac"; } -.icon-relations:before { +.icon-relation-new-child:before { content: "\f1ac"; } -@mixin icon-mixin-reload { +@mixin icon-mixin-relation-precedes { content: "\f1ad"; } -.icon-reload:before { +.icon-relation-precedes:before { content: "\f1ad"; } -@mixin icon-mixin-reminder { +@mixin icon-mixin-relations { content: "\f1ae"; } -.icon-reminder:before { +.icon-relations:before { content: "\f1ae"; } -@mixin icon-mixin-remove { +@mixin icon-mixin-reload { content: "\f1af"; } -.icon-remove:before { +.icon-reload:before { content: "\f1af"; } -@mixin icon-mixin-rename { +@mixin icon-mixin-reminder { content: "\f1b0"; } -.icon-rename:before { +.icon-reminder:before { content: "\f1b0"; } -@mixin icon-mixin-reported-by-me { +@mixin icon-mixin-remove { content: "\f1b1"; } -.icon-reported-by-me:before { +.icon-remove:before { content: "\f1b1"; } -@mixin icon-mixin-resizer-vertical-lines { +@mixin icon-mixin-rename { content: "\f1b2"; } -.icon-resizer-vertical-lines:before { +.icon-rename:before { content: "\f1b2"; } -@mixin icon-mixin-roadmap { +@mixin icon-mixin-reported-by-me { content: "\f1b3"; } -.icon-roadmap:before { +.icon-reported-by-me:before { content: "\f1b3"; } -@mixin icon-mixin-rss { +@mixin icon-mixin-resizer-vertical-lines { content: "\f1b4"; } -.icon-rss:before { +.icon-resizer-vertical-lines:before { content: "\f1b4"; } -@mixin icon-mixin-rubber { +@mixin icon-mixin-roadmap { content: "\f1b5"; } -.icon-rubber:before { +.icon-roadmap:before { content: "\f1b5"; } -@mixin icon-mixin-save { +@mixin icon-mixin-rss { content: "\f1b6"; } -.icon-save:before { +.icon-rss:before { content: "\f1b6"; } -@mixin icon-mixin-search { +@mixin icon-mixin-rubber { content: "\f1b7"; } -.icon-search:before { +.icon-rubber:before { content: "\f1b7"; } -@mixin icon-mixin-send-mail { +@mixin icon-mixin-save { content: "\f1b8"; } -.icon-send-mail:before { +.icon-save:before { content: "\f1b8"; } -@mixin icon-mixin-server-key { +@mixin icon-mixin-search { content: "\f1b9"; } -.icon-server-key:before { +.icon-search:before { content: "\f1b9"; } -@mixin icon-mixin-settings { +@mixin icon-mixin-send-mail { content: "\f1ba"; } -.icon-settings:before { +.icon-send-mail:before { content: "\f1ba"; } -@mixin icon-mixin-settings2 { +@mixin icon-mixin-server-key { content: "\f1bb"; } -.icon-settings2:before { +.icon-server-key:before { content: "\f1bb"; } -@mixin icon-mixin-settings3 { +@mixin icon-mixin-settings { content: "\f1bc"; } -.icon-settings3:before { +.icon-settings:before { content: "\f1bc"; } -@mixin icon-mixin-settings4 { +@mixin icon-mixin-settings2 { content: "\f1bd"; } -.icon-settings4:before { +.icon-settings2:before { content: "\f1bd"; } -@mixin icon-mixin-shortcuts { +@mixin icon-mixin-settings3 { content: "\f1be"; } -.icon-shortcuts:before { +.icon-settings3:before { content: "\f1be"; } -@mixin icon-mixin-show-all-projects { +@mixin icon-mixin-settings4 { content: "\f1bf"; } -.icon-show-all-projects:before { +.icon-settings4:before { content: "\f1bf"; } -@mixin icon-mixin-show-more-horizontal { +@mixin icon-mixin-shortcuts { content: "\f1c0"; } -.icon-show-more-horizontal:before { +.icon-shortcuts:before { content: "\f1c0"; } -@mixin icon-mixin-show-more { +@mixin icon-mixin-show-all-projects { content: "\f1c1"; } -.icon-show-more:before { +.icon-show-all-projects:before { content: "\f1c1"; } -@mixin icon-mixin-sort-ascending { +@mixin icon-mixin-show-more-horizontal { content: "\f1c2"; } -.icon-sort-ascending:before { +.icon-show-more-horizontal:before { content: "\f1c2"; } -@mixin icon-mixin-sort-by { +@mixin icon-mixin-show-more { content: "\f1c3"; } -.icon-sort-by:before { +.icon-show-more:before { content: "\f1c3"; } -@mixin icon-mixin-sort-descending { +@mixin icon-mixin-sort-ascending { content: "\f1c4"; } -.icon-sort-descending:before { +.icon-sort-ascending:before { content: "\f1c4"; } -@mixin icon-mixin-sort-down { +@mixin icon-mixin-sort-by { content: "\f1c5"; } -.icon-sort-down:before { +.icon-sort-by:before { content: "\f1c5"; } -@mixin icon-mixin-sort-up { +@mixin icon-mixin-sort-descending { content: "\f1c6"; } -.icon-sort-up:before { +.icon-sort-descending:before { content: "\f1c6"; } -@mixin icon-mixin-square { +@mixin icon-mixin-sort-down { content: "\f1c7"; } -.icon-square:before { +.icon-sort-down:before { content: "\f1c7"; } -@mixin icon-mixin-star { +@mixin icon-mixin-sort-up { content: "\f1c8"; } -.icon-star:before { +.icon-sort-up:before { content: "\f1c8"; } -@mixin icon-mixin-status-reporting { +@mixin icon-mixin-square { content: "\f1c9"; } -.icon-status-reporting:before { +.icon-square:before { content: "\f1c9"; } -@mixin icon-mixin-status { +@mixin icon-mixin-star { content: "\f1ca"; } -.icon-status:before { +.icon-star:before { content: "\f1ca"; } -@mixin icon-mixin-strike-through { +@mixin icon-mixin-status-reporting { content: "\f1cb"; } -.icon-strike-through:before { +.icon-status-reporting:before { content: "\f1cb"; } -@mixin icon-mixin-text { +@mixin icon-mixin-status { content: "\f1cc"; } -.icon-text:before { +.icon-status:before { content: "\f1cc"; } -@mixin icon-mixin-ticket-checked { +@mixin icon-mixin-strike-through { content: "\f1cd"; } -.icon-ticket-checked:before { +.icon-strike-through:before { content: "\f1cd"; } -@mixin icon-mixin-ticket-down { +@mixin icon-mixin-text { content: "\f1ce"; } -.icon-ticket-down:before { +.icon-text:before { content: "\f1ce"; } -@mixin icon-mixin-ticket-edit { +@mixin icon-mixin-ticket-checked { content: "\f1cf"; } -.icon-ticket-edit:before { +.icon-ticket-checked:before { content: "\f1cf"; } -@mixin icon-mixin-ticket-minus { +@mixin icon-mixin-ticket-down { content: "\f1d0"; } -.icon-ticket-minus:before { +.icon-ticket-down:before { content: "\f1d0"; } -@mixin icon-mixin-ticket-note { +@mixin icon-mixin-ticket-edit { content: "\f1d1"; } -.icon-ticket-note:before { +.icon-ticket-edit:before { content: "\f1d1"; } -@mixin icon-mixin-ticket { +@mixin icon-mixin-ticket-minus { content: "\f1d2"; } -.icon-ticket:before { +.icon-ticket-minus:before { content: "\f1d2"; } -@mixin icon-mixin-time { +@mixin icon-mixin-ticket-note { content: "\f1d3"; } -.icon-time:before { +.icon-ticket-note:before { content: "\f1d3"; } -@mixin icon-mixin-to-fullscreen { +@mixin icon-mixin-ticket { content: "\f1d4"; } -.icon-to-fullscreen:before { +.icon-ticket:before { content: "\f1d4"; } -@mixin icon-mixin-toggle { +@mixin icon-mixin-time { content: "\f1d5"; } -.icon-toggle:before { +.icon-time:before { content: "\f1d5"; } -@mixin icon-mixin-training-consulting { +@mixin icon-mixin-to-fullscreen { content: "\f1d6"; } -.icon-training-consulting:before { +.icon-to-fullscreen:before { content: "\f1d6"; } -@mixin icon-mixin-two-factor-authentication { +@mixin icon-mixin-toggle { content: "\f1d7"; } -.icon-two-factor-authentication:before { +.icon-toggle:before { content: "\f1d7"; } -@mixin icon-mixin-types { +@mixin icon-mixin-training-consulting { content: "\f1d8"; } -.icon-types:before { +.icon-training-consulting:before { content: "\f1d8"; } -@mixin icon-mixin-underline { +@mixin icon-mixin-two-factor-authentication { content: "\f1d9"; } -.icon-underline:before { +.icon-two-factor-authentication:before { content: "\f1d9"; } -@mixin icon-mixin-undo { +@mixin icon-mixin-types { content: "\f1da"; } -.icon-undo:before { +.icon-types:before { content: "\f1da"; } -@mixin icon-mixin-unit { +@mixin icon-mixin-underline { content: "\f1db"; } -.icon-unit:before { +.icon-underline:before { content: "\f1db"; } -@mixin icon-mixin-unlocked { +@mixin icon-mixin-undo { content: "\f1dc"; } -.icon-unlocked:before { +.icon-undo:before { content: "\f1dc"; } -@mixin icon-mixin-unordered-list { +@mixin icon-mixin-unit { content: "\f1dd"; } -.icon-unordered-list:before { +.icon-unit:before { content: "\f1dd"; } -@mixin icon-mixin-unwatched { +@mixin icon-mixin-unlocked { content: "\f1de"; } -.icon-unwatched:before { +.icon-unlocked:before { content: "\f1de"; } -@mixin icon-mixin-upload { +@mixin icon-mixin-unordered-list { content: "\f1df"; } -.icon-upload:before { +.icon-unordered-list:before { content: "\f1df"; } -@mixin icon-mixin-user-minus { +@mixin icon-mixin-unwatched { content: "\f1e0"; } -.icon-user-minus:before { +.icon-unwatched:before { content: "\f1e0"; } -@mixin icon-mixin-user-plus { +@mixin icon-mixin-upload { content: "\f1e1"; } -.icon-user-plus:before { +.icon-upload:before { content: "\f1e1"; } -@mixin icon-mixin-user { +@mixin icon-mixin-user-minus { content: "\f1e2"; } -.icon-user:before { +.icon-user-minus:before { content: "\f1e2"; } -@mixin icon-mixin-view-fullscreen { +@mixin icon-mixin-user-plus { content: "\f1e3"; } -.icon-view-fullscreen:before { +.icon-user-plus:before { content: "\f1e3"; } -@mixin icon-mixin-view-list { +@mixin icon-mixin-user { content: "\f1e4"; } -.icon-view-list:before { +.icon-user:before { content: "\f1e4"; } -@mixin icon-mixin-view-split { +@mixin icon-mixin-view-fullscreen { content: "\f1e5"; } -.icon-view-split:before { +.icon-view-fullscreen:before { content: "\f1e5"; } -@mixin icon-mixin-view-timeline { +@mixin icon-mixin-view-list { content: "\f1e6"; } -.icon-view-timeline:before { +.icon-view-list:before { content: "\f1e6"; } -@mixin icon-mixin-warning { +@mixin icon-mixin-view-split { content: "\f1e7"; } -.icon-warning:before { +.icon-view-split:before { content: "\f1e7"; } -@mixin icon-mixin-watched { +@mixin icon-mixin-view-timeline { content: "\f1e8"; } -.icon-watched:before { +.icon-view-timeline:before { content: "\f1e8"; } -@mixin icon-mixin-wiki-edit { +@mixin icon-mixin-warning { content: "\f1e9"; } -.icon-wiki-edit:before { +.icon-warning:before { content: "\f1e9"; } -@mixin icon-mixin-wiki { +@mixin icon-mixin-watched { content: "\f1ea"; } -.icon-wiki:before { +.icon-watched:before { content: "\f1ea"; } -@mixin icon-mixin-wiki2 { +@mixin icon-mixin-wiki-edit { content: "\f1eb"; } -.icon-wiki2:before { +.icon-wiki-edit:before { content: "\f1eb"; } -@mixin icon-mixin-work-packages { +@mixin icon-mixin-wiki { content: "\f1ec"; } -.icon-work-packages:before { +.icon-wiki:before { content: "\f1ec"; } -@mixin icon-mixin-workflow { +@mixin icon-mixin-wiki2 { content: "\f1ed"; } -.icon-workflow:before { +.icon-wiki2:before { content: "\f1ed"; } -@mixin icon-mixin-yes { +@mixin icon-mixin-work-packages { content: "\f1ee"; } -.icon-yes:before { +.icon-work-packages:before { content: "\f1ee"; } -@mixin icon-mixin-zen-mode { +@mixin icon-mixin-workflow { content: "\f1ef"; } -.icon-zen-mode:before { +.icon-workflow:before { content: "\f1ef"; } -@mixin icon-mixin-zoom-auto { +@mixin icon-mixin-yes { content: "\f1f0"; } -.icon-zoom-auto:before { +.icon-yes:before { content: "\f1f0"; } -@mixin icon-mixin-zoom-in { +@mixin icon-mixin-zen-mode { content: "\f1f1"; } -.icon-zoom-in:before { +.icon-zen-mode:before { content: "\f1f1"; } -@mixin icon-mixin-zoom-out { +@mixin icon-mixin-zoom-auto { content: "\f1f2"; } -.icon-zoom-out:before { +.icon-zoom-auto:before { content: "\f1f2"; } +@mixin icon-mixin-zoom-in { + content: "\f1f3"; +} +.icon-zoom-in:before { + content: "\f1f3"; +} +@mixin icon-mixin-zoom-out { + content: "\f1f4"; +} +.icon-zoom-out:before { + content: "\f1f4"; +} diff --git a/app/assets/stylesheets/fonts/_openproject_icon_font.lsg b/app/assets/stylesheets/fonts/_openproject_icon_font.lsg index 395bd810e6e..7bb7ae9898b 100644 --- a/app/assets/stylesheets/fonts/_openproject_icon_font.lsg +++ b/app/assets/stylesheets/fonts/_openproject_icon_font.lsg @@ -77,6 +77,7 @@

  1. enumerations
  2. error
  3. export-atom
  4. +
  5. export-bcf
  6. export-csv
  7. export-pdf-descr
  8. export-pdf-with-descriptions
  9. @@ -114,6 +115,7 @@
  10. hosting
  11. image1
  12. image2
  13. +
  14. import
  15. info1
  16. info2
  17. installation-services
  18. diff --git a/app/assets/stylesheets/layout/_main_menu.sass b/app/assets/stylesheets/layout/_main_menu.sass index 3d4b781635d..bc00ff93265 100644 --- a/app/assets/stylesheets/layout/_main_menu.sass +++ b/app/assets/stylesheets/layout/_main_menu.sass @@ -400,3 +400,21 @@ a.main-menu--parent-node .tree-menu--title @include varprop(color, main-menu-hover-font-color) text-decoration: none + + + +// Badges for menu items such as "EXPERIMENTAL" or "BETA" +$badge_offset: 4px + +.menu-item--title.-has-badge + // As this is flex box with a vertical center we need this trick to undo centering the whole content including a badge. + // The goal is to leave the caption where it was before adding a badge. + margin-top: -1 * $badge_offset + +.main-item--badge + font-size: 10px + vertical-align: $badge_offset + text-transform: uppercase + margin-left: 4px + font-weight: bold + font-style: italic diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 44a5490f223..ec83e6d51f4 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -87,29 +87,15 @@ class WorkPackagesController < ApplicationController def export_list(mime_type) exporter = WorkPackage::Exporter.for_list(mime_type) - export = exporter.list(@query, params) - - if export.error? - flash[:error] = export.message - redirect_back(fallback_location: index_redirect_path) - else - send_data(export.content, - type: export.mime_type, - filename: export.title) + exporter.list(@query, params) do |export| + render_export_response export, fallback_path: index_redirect_path end end def export_single(mime_type) exporter = WorkPackage::Exporter.for_single(mime_type) - export = exporter.single(work_package, params) - - if export.error? - flash[:error] = export.message - redirect_back(fallback_location: work_package_path(work_packages)) - else - send_data(export.content, - type: export.mime_type, - filename: export.title) + exporter.single(work_package, params) do |export| + render_export_response export, fallback_path: work_package_path(work_packages) end end @@ -126,13 +112,36 @@ class WorkPackagesController < ApplicationController title: "#{@project || Setting.app_title}: #{l(:label_work_package_plural)}") end + private + + def render_export_response(export, fallback_path:) + if export.error? + flash[:error] = export.message + redirect_back(fallback_location: fallback_path) + elsif export.content.is_a? File + # browsers should not try to guess the content-type + response.headers['X-Content-Type-Options'] = 'nosniff' + + # TODO avoid reading the file in memory here again + # but currently the tempfile gets removed in between + send_data(export.content.read, + type: export.mime_type, + disposition: 'attachment', + filename: export.title) + else + send_data(export.content, + type: export.mime_type, + filename: export.title) + end + end + def authorize_on_work_package deny_access unless work_package end def protect_from_unauthorized_export if supported_export_formats.include?(params[:format]) && - !User.current.allowed_to?(:export_work_packages, @project, global: @project.nil?) + !User.current.allowed_to?(:export_work_packages, @project, global: @project.nil?) deny_access false @@ -187,7 +196,7 @@ class WorkPackagesController < ApplicationController .changing .includes(:user) .order(order).to_a - end + end end def index_redirect_path @@ -198,8 +207,6 @@ class WorkPackagesController < ApplicationController end end - private - def load_work_packages @results = @query.results @work_packages = if @query.valid? diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 4df4abdf254..c698ed1396d 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -128,19 +128,33 @@ class Attachment < ActiveRecord::Base container.present? end + ## + # Retrieve a local file, + # this may result in downloading the file first def diskfile file.local_file end + ## + # Retrieve the local file path, + # this may result in downloading the file first to a tmpdir + def local_path + diskfile.path + end + def filename attributes['file'] end def file=(file) super.tap do - set_content_type file set_file_size file - set_digest file + + set_content_type file + + if File.readable? file.path + set_digest file + end end end diff --git a/app/models/work_package/exporter/base.rb b/app/models/work_package/exporter/base.rb index d190ed82c04..df68f273649 100644 --- a/app/models/work_package/exporter/base.rb +++ b/app/models/work_package/exporter/base.rb @@ -37,14 +37,17 @@ class WorkPackage::Exporter::Base self.options = options end - def self.list(query, options = {}) - new(query, options).list + def self.list(query, options = {}, &block) + new(query, options).list(&block) end - def self.single(work_package, options = {}) - new(work_package, options).single + def self.single(work_package, options = {}, &block) + new(work_package, options).single(&block) end + # Provide means to clean up after the export + def cleanup; end + def page options[:page] || 1 end @@ -58,6 +61,15 @@ class WorkPackage::Exporter::Base alias :query :object alias :work_package :object + # Remove characters that could cause problems on popular OSses + def sane_filename(name) + parts = name.split /(?<=.)\.(?=[^.])(?!.*\.[^.])/m + + parts.map! { |s| s.gsub /[^a-z0-9\-]+/i, '_' } + + parts.join '.' + end + def work_packages @work_packages ||= query .results diff --git a/app/models/work_package/exporter/csv.rb b/app/models/work_package/exporter/csv.rb index cc261378f2e..95487038166 100644 --- a/app/models/work_package/exporter/csv.rb +++ b/app/models/work_package/exporter/csv.rb @@ -45,7 +45,7 @@ class WorkPackage::Exporter::CSV < WorkPackage::Exporter::Base end end - success(serialized) + yield success(serialized) end def self.encode_csv_columns(columns, encoding = l(:general_csv_encoding)) diff --git a/app/models/work_package/exporter/pdf.rb b/app/models/work_package/exporter/pdf.rb index 87a5ec284b4..938eba3e48f 100644 --- a/app/models/work_package/exporter/pdf.rb +++ b/app/models/work_package/exporter/pdf.rb @@ -31,7 +31,7 @@ class WorkPackage::Exporter::PDF < WorkPackage::Exporter::Base # Returns a PDF string of a list of work_packages def list - ::WorkPackage::PdfExport::WorkPackageListToPdf + yield ::WorkPackage::PdfExport::WorkPackageListToPdf .new(query, options) .render! @@ -39,7 +39,7 @@ class WorkPackage::Exporter::PDF < WorkPackage::Exporter::Base # Returns a PDF string of a single work_package def single - ::WorkPackage::PdfExport::WorkPackageToPdf + yield ::WorkPackage::PdfExport::WorkPackageToPdf .new(work_package) .render! end diff --git a/app/models/work_package/exporter/success.rb b/app/models/work_package/exporter/success.rb index e6677b492f9..79c265bc2e1 100644 --- a/app/models/work_package/exporter/success.rb +++ b/app/models/work_package/exporter/success.rb @@ -34,7 +34,7 @@ class WorkPackage::Exporter::Success < WorkPackage::Exporter::Result :content, :mime_type - def initialize(format:, title:, content:, mime_type:) + def initialize(format:, title:, content: nil, mime_type:) self.format = format self.title = title self.content = content diff --git a/app/services/add_work_package_note_service.rb b/app/services/add_work_package_note_service.rb index 674e04067e3..5eccca27db9 100644 --- a/app/services/add_work_package_note_service.rb +++ b/app/services/add_work_package_note_service.rb @@ -45,12 +45,12 @@ class AddWorkPackageNoteService JournalManager.with_send_notifications send_notifications do work_package.add_journal(user, notes) - result, errors = validate_and_yield(work_package, user) do + success, errors = validate_and_yield(work_package, user) do work_package.save_journals end - ServiceResult.new(success: result, - errors: errors) + journal = work_package.journals.last if success + ServiceResult.new(success: success, result: journal, errors: errors) end end end diff --git a/app/services/create_work_package_service.rb b/app/services/create_work_package_service.rb index 182445d82bc..1e411c3d28c 100644 --- a/app/services/create_work_package_service.rb +++ b/app/services/create_work_package_service.rb @@ -61,5 +61,7 @@ class CreateWorkPackageService def assign_defaults(work_package) work_package.author ||= user + work_package.priority ||= IssuePriority.active.default + work_package.status ||= Status.default end end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 5af23d1f0db..92702644a3d 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -31,6 +31,20 @@ module FileUploader base.extend ClassMethods end + ## + # Returns an URL if the attachment is stored in an external (fog) attachment storage + # or nil otherwise. + def external_url + url = URI.parse download_url + url if url.host + rescue URI::InvalidURIError + nil + end + + def external_storage? + !external_url.nil? + end + def local_file file.to_file end @@ -48,7 +62,7 @@ module FileUploader end # store! nil's the cache_id after it finishes so we need to remember it for deletion - def remember_cache_id(_new_file) + def remember_cache_id(_new_file) @cache_id_was = cache_id end diff --git a/config/locales/en.yml b/config/locales/en.yml index 3ed081bbfb0..bacd7987971 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1304,6 +1304,7 @@ en: label_equals: "is" label_everywhere: "everywhere" label_example: "Example" + label_import: "Import" label_export_to: "Also available in:" label_expanded_click_to_collapse: "Expanded. Click to collapse" label_f_hour: "%{value} hour" diff --git a/lib/redmine/menu_manager/menu_helper.rb b/lib/redmine/menu_manager/menu_helper.rb index e89d0868285..d1e4c7b0e00 100644 --- a/lib/redmine/menu_manager/menu_helper.rb +++ b/lib/redmine/menu_manager/menu_helper.rb @@ -184,14 +184,21 @@ module Redmine::MenuManager::MenuHelper end def render_single_menu_node(item, caption, url, selected) + link_text = ''.html_safe link_text << op_icon(item.icon) if item.icon.present? link_text << you_are_here_info(selected) - link_text << content_tag(:span, caption, class: 'menu-item--title ellipsis', lang: menu_item_locale(item)) + link_text << content_tag(:span, + class: "menu-item--title ellipsis #{item.badge.present? ? '-has-badge' : ''}", + lang: menu_item_locale(item)) do + ''.html_safe + caption + badge_for(item) + end link_text << ' '.html_safe + op_icon(item.icon_after) if item.icon_after.present? html_options = item.html_options(selected: selected) html_options[:title] ||= selected ? t(:description_current_position) + caption : caption - link_to link_text, url, html_options + link_to url, html_options do + link_text + end end def render_unattached_menu_item(menu_item, project) @@ -265,10 +272,10 @@ module Redmine::MenuManager::MenuHelper end if project - return user && user.allowed_to?(node.url, project) + user && user.allowed_to?(node.url, project) else # outside a project, all menu items allowed - return true + true end end @@ -309,4 +316,12 @@ module Redmine::MenuManager::MenuHelper def wiki_prefix? current_menu_item.to_s.match? /^wiki-/ end + + def badge_for(item) + badge = ''.html_safe + if item.badge.present? + badge += ' '.html_safe + content_tag('span', I18n.t(item.badge), class: 'main-item--badge') + end + badge + end end diff --git a/lib/redmine/menu_manager/menu_item.rb b/lib/redmine/menu_manager/menu_item.rb index 7c20b7c6014..24375e2c8c8 100644 --- a/lib/redmine/menu_manager/menu_item.rb +++ b/lib/redmine/menu_manager/menu_item.rb @@ -29,7 +29,7 @@ class Redmine::MenuManager::MenuItem < Redmine::MenuManager::TreeNode include Redmine::I18n - attr_reader :name, :url, :param, :icon, :icon_after, :context, :condition, :parent, :child_menus, :last, :partial + attr_reader :name, :url, :param, :icon, :icon_after, :context, :condition, :parent, :child_menus, :last, :partial, :badge def initialize(name, url, options) raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call) @@ -53,6 +53,7 @@ class Redmine::MenuManager::MenuItem < Redmine::MenuManager::TreeNode @child_menus = options[:children] @last = options[:last] || false @partial = options[:partial] + @badge = options[:badge] super @name.to_sym end diff --git a/modules/bcf/Gemfile b/modules/bcf/Gemfile new file mode 100644 index 00000000000..851fabc21dd --- /dev/null +++ b/modules/bcf/Gemfile @@ -0,0 +1,2 @@ +source 'https://rubygems.org' +gemspec diff --git a/modules/bcf/app/assets/stylesheets/bcf/bcf.sass b/modules/bcf/app/assets/stylesheets/bcf/bcf.sass new file mode 100644 index 00000000000..cd03476d8fe --- /dev/null +++ b/modules/bcf/app/assets/stylesheets/bcf/bcf.sass @@ -0,0 +1,20 @@ +.bcf--issues + display: flex + flex-direction: row + flex-wrap: wrap + + > div + flex: 0 0 250px + margin: 10px + padding: 10px + + img + width: 100% + +.bcf--import-listing + margin-bottom: 50px + +table.attributes-table + td,th + padding: 5px + text-align: center diff --git a/modules/bcf/app/controllers/bcf/base_controller.rb b/modules/bcf/app/controllers/bcf/base_controller.rb new file mode 100644 index 00000000000..52fe67b391a --- /dev/null +++ b/modules/bcf/app/controllers/bcf/base_controller.rb @@ -0,0 +1,4 @@ +module ::Bcf + class BaseController < ApplicationController + end +end diff --git a/modules/bcf/app/controllers/bcf/linked_issues_controller.rb b/modules/bcf/app/controllers/bcf/linked_issues_controller.rb new file mode 100644 index 00000000000..dd9a233c1b7 --- /dev/null +++ b/modules/bcf/app/controllers/bcf/linked_issues_controller.rb @@ -0,0 +1,88 @@ +module ::Bcf + class LinkedIssuesController < BaseController + include PaginationHelper + + before_action :find_project_by_project_id + before_action :authorize + + before_action :check_file_param, only: %i[prepare_import] + before_action :get_persisted_file, only: %i[perform_import] + before_action :persist_file, only: %i[prepare_import] + + before_action :build_importer, only: %i[prepare_import perform_import] + + before_action :get_issue_type + + menu_item :bcf + + def index + @issues = ::Bcf::Issue.in_project(@project) + .with_markup + .includes(:comments, :work_package, viewpoints: :attachments) + .page(page_param) + .per_page(per_page_param) + end + + def import; end + + def prepare_import + @bcf_file = params[:bcf_file] + + begin + @listing = @importer.get_extractor_list! @bcf_file.path + @issues = ::Bcf::Issue + .with_markup + .includes(work_package: %i[status priority assigned_to]) + .where(uuid: @listing.map { |e| e[:uuid] }) + rescue StandardError => e + flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: e.message) + redirect_to action: :index + end + end + + def perform_import + begin + result = @importer.import! @bcf_attachment.local_path + flash[:notice] = I18n.t('bcf.bcf_xml.import_successful', count: result) + rescue StandardError => e + flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: e.message) + end + + @bcf_attachment.destroy + redirect_to action: :index + end + + private + + def build_importer + @importer = ::OpenProject::Bcf::BcfXml::Importer.new @project, current_user: current_user + end + + def get_issue_type + @issue_type = @project.types.find_by(name: 'Issue [BCF]') + end + + def get_persisted_file + @bcf_attachment = Attachment.find_by! id: session[:bcf_file_id], author: current_user + rescue ActiveRecord::RecordNotFound + render_404 'BCF file not found' + end + + def persist_file + file = params[:bcf_file] + @bcf_attachment = Attachment.create! file: file, description: file.original_filename, author: current_user + session[:bcf_file_id] = @bcf_attachment.id + rescue StandardError => e + flash[:error] = "Failed to persist BCF file: #{e.message}" + redirect_to action: :index + end + + def check_file_param + path = params[:bcf_file]&.path + unless path && File.readable?(path) + flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: 'File missing or not readable') + redirect_to action: :import + end + end + end +end diff --git a/modules/bcf/app/models/bcf.rb b/modules/bcf/app/models/bcf.rb new file mode 100644 index 00000000000..f5acb9ac1b4 --- /dev/null +++ b/modules/bcf/app/models/bcf.rb @@ -0,0 +1,5 @@ +module Bcf + def self.table_name_prefix + 'bcf_' + end +end diff --git a/modules/bcf/app/models/bcf/comment.rb b/modules/bcf/app/models/bcf/comment.rb new file mode 100644 index 00000000000..579cf3c32df --- /dev/null +++ b/modules/bcf/app/models/bcf/comment.rb @@ -0,0 +1,14 @@ +module Bcf + class Comment < ActiveRecord::Base + include InitializeWithUuid + + belongs_to :journal + belongs_to :issue, foreign_key: :issue_id, class_name: "Bcf::Issue" + + validates_presence_of :uuid + + def self.has_uuid?(uuid) + where(uuid: uuid).exists? + end + end +end diff --git a/modules/bcf/app/models/bcf/initialize_with_uuid.rb b/modules/bcf/app/models/bcf/initialize_with_uuid.rb new file mode 100644 index 00000000000..b7a406e6a28 --- /dev/null +++ b/modules/bcf/app/models/bcf/initialize_with_uuid.rb @@ -0,0 +1,17 @@ +module Bcf + + ## + # Module to set an initial UUID on the model + # whenever it is created + module InitializeWithUuid + extend ActiveSupport::Concern + + included do + after_initialize :set_initial_uuid, if: :new_record? + end + + def set_initial_uuid + self.uuid ||= SecureRandom.uuid + end + end +end diff --git a/modules/bcf/app/models/bcf/issue.rb b/modules/bcf/app/models/bcf/issue.rb new file mode 100644 index 00000000000..ccf06c94aa5 --- /dev/null +++ b/modules/bcf/app/models/bcf/issue.rb @@ -0,0 +1,33 @@ +module Bcf + class Issue < ActiveRecord::Base + include InitializeWithUuid + + belongs_to :work_package + belongs_to :project + + class << self + def in_project(project) + where(project_id: project.try(:id) || project) + end + + def with_markup + select '*', + extract_xml('/Markup/Topic/Title/text()', 'title'), + extract_xml('/Markup/Topic/Description/text()', 'description'), + extract_xml('/Markup/Topic/Priority/text()', 'priority_text'), + extract_xml('/Markup/Topic/@TopicStatus', 'status_text'), + extract_xml('/Markup/Topic/AssignedTo/text()', 'assignee_text'), + extract_xml('/Markup/Topic/DueDate/text()', 'due_date_text') + end + + private + + def extract_xml(path, as) + "(xpath('#{path}', markup))[1] AS #{as}" + end + end + + has_many :viewpoints, foreign_key: :issue_id, class_name: "Bcf::Viewpoint" + has_many :comments, foreign_key: :issue_id, class_name: "Bcf::Comment" + end +end diff --git a/modules/bcf/app/models/bcf/viewpoint.rb b/modules/bcf/app/models/bcf/viewpoint.rb new file mode 100644 index 00000000000..726fa12d69a --- /dev/null +++ b/modules/bcf/app/models/bcf/viewpoint.rb @@ -0,0 +1,26 @@ +module Bcf + class Viewpoint < ActiveRecord::Base + include InitializeWithUuid + + acts_as_attachable view_permission: :view_linked_issues, + delete_permission: :manage_bcf, + add_on_new_permission: :manage_bcf, + add_on_persisted_permission: :manage_bcf + + def self.has_uuid?(uuid) + where(uuid: uuid).exists? + end + + belongs_to :issue, foreign_key: :issue_id, class_name: "Bcf::Issue" + delegate :project, :project_id, to: :issue, allow_nil: true + + def snapshot + attachments.find_by_description('snapshot') + end + + def snapshot=(file) + snapshot&.destroy + attach_files('first' => { 'file' => file, 'description' => 'snapshot' }) + end + end +end diff --git a/modules/bcf/app/views/bcf/linked_issues/import.html.erb b/modules/bcf/app/views/bcf/linked_issues/import.html.erb new file mode 100644 index 00000000000..69ec5042ac9 --- /dev/null +++ b/modules/bcf/app/views/bcf/linked_issues/import.html.erb @@ -0,0 +1,30 @@ +<%= toolbar title: t('bcf.bcf_xml.import_title') %> + +<% unless @issue_type %> +
    +
    +

    + <%= t('bcf.bcf_xml.type_not_active') %> + <%= link_to t(:label_project_settings), settings_project_path(@project, tab: :types) %> +

    +
    +
    +<% end %> + +<%= form_tag({ action: :prepare_import }, multipart: true, method: :post) do %> + +
    + <%= styled_label_tag 'bcf_file', t('bcf.bcf_xml.xml_file') %> +
    + <%= styled_file_field_tag :bcf_file, required: true %> +
    +
    +

    <%= t('bcf.bcf_xml.import.description') %>

    +
    +
    + + <%= submit_tag t('bcf.bcf_xml.import.button_prepare'), class: 'button -highlight' %> + <%= link_to t(:button_cancel), + { action: :index }, + class: 'button' %> +<% end %> diff --git a/modules/bcf/app/views/bcf/linked_issues/index.html.erb b/modules/bcf/app/views/bcf/linked_issues/index.html.erb new file mode 100644 index 00000000000..7af363a2e0e --- /dev/null +++ b/modules/bcf/app/views/bcf/linked_issues/index.html.erb @@ -0,0 +1,56 @@ +<%= stylesheet_link_tag 'bcf/bcf.css' %> + + +<%= toolbar title: t('bcf.linked_issues'), html: {class: '-with-dropdown'} do %> + +
  19. + <%= link_to({ action: 'import' }, + title: I18n.t(:label_import), + class: 'button import-bcf-button') do %> + <%= op_icon('button--icon icon-import') %> + <%= l(:label_import) %> + <% end %> +
  20. + <% if @issue_type && @issues.present? %> +
  21. + <% query = { f: [filter_object('type', '=', @issue_type.id)] } %> + <%= link_to(project_work_packages_with_query_path(@project, query, format: :bcf), + title: t('bcf.bcf_xml.export'), + class: 'button export-bcf-button') do %> + <%= op_icon('button--icon icon-export') %> + <%= t('bcf.bcf_xml.export') %> + <% end %> +
  22. + <% end %> +<% end %> + +<% if @issues.present? %> +
    + <% @issues.each do |issue| %> + <% status_id = issue.work_package&.status_id %> + <% hl_classname = status_id.present? ? "__hl_row_status_#{status_id}" : '' %> +
    +

    + <%= issue.title %> +
    + <% if issue.work_package %> + <%= link_to_work_package(issue.work_package) %> +
    + <% end %> +

    + <% if issue.viewpoints.empty? %> +

    (No viewpoints)

    + <% end %> + <% issue.viewpoints.each do |vp| %> + + <% end %> +
    + <% end %> +
    +<% else %> + <%= no_results_box %> +<% end %> + +<%= pagination_links_full @issues %> diff --git a/modules/bcf/app/views/bcf/linked_issues/prepare_import.html.erb b/modules/bcf/app/views/bcf/linked_issues/prepare_import.html.erb new file mode 100644 index 00000000000..ab797ee56cf --- /dev/null +++ b/modules/bcf/app/views/bcf/linked_issues/prepare_import.html.erb @@ -0,0 +1,60 @@ +<%= stylesheet_link_tag 'bcf/bcf.css' %> +<%= toolbar title: t('bcf.bcf_xml.import_title') %> + +

    <%= t('bcf.bcf_xml.import.num_issues_found', x_bcf_issues: t('bcf.x_bcf_issues', count: @listing.count)) %>

    + +<% @listing.each do |entry| %> + <% issue = @issues.detect { |bcf| bcf.uuid == entry[:uuid] } %> + +
    +

    <%= entry[:title] %>

    + + + + + + + + + + + + + + + + <% if issue && issue.work_package %> + + <% else %> + + <% end %> + + + + + + + + + <% if issue&.work_package %> + + + + + + + + + <% end %> + +
    Work packageTitleStatusPriorityAssigneeDue date# Viewpoints# Comments
    <%= link_to_work_package issue.work_package %>(will be created)<%= entry[:title] || '-' %><%= entry[:status] || '-' %><%= entry[:priority] || '-' %><%= entry[:assignee] || '-' %><%= format_date(entry[:due_date]) || '-' %><%= entry[:viewpoint_count] %><%= entry[:comments_count] %>
    <%= issue.work_package.subject %><%= issue.work_package.status.name %><%= issue.work_package.priority.name %><%= issue.work_package.assigned_to&.name %><%= format_date(issue.work_package.due_date) %><%= issue.viewpoints.count %><%= issue.comments.count %>
    +
    +<% end %> + +<%= form_tag({ action: :perform_import }, multipart: true, method: :post) do %> +

    <%= t('bcf.bcf_xml.import.perform_description') %>

    + <%= submit_tag t('bcf.bcf_xml.import.button_perform_import'), class: 'button -highlight' %> + <%= link_to t(:button_cancel), + { action: :index }, + class: 'button' %> +<% end %> diff --git a/modules/bcf/config/locales/en.yml b/modules/bcf/config/locales/en.yml new file mode 100644 index 00000000000..cd3f7e3ff0c --- /dev/null +++ b/modules/bcf/config/locales/en.yml @@ -0,0 +1,30 @@ +# English strings go here for Rails i18n +en: + bcf: + label_bcf: 'BCF' + linked_issues: "Linked issues" + experimental_badge: "Experimental" + + x_bcf_issues: + zero: 'No BCF issues' + one: 'One BCF issue' + other: '%{count} BCF issues' + + bcf_xml: + xml_file: 'BCF XML File' + import_title: 'Import from BCF file' + export: 'Export all to BCF-XML' + import_update_comment: '(Updated in BCF import)' + import_failed: 'Cannot import BCF file: %{error}' + import_successful: 'Imported %{count} BCF issues' + type_not_active: "The issue type is not activated for this project." + + import: + num_issues_found: '%{x_bcf_issues} are contained in the BCF-XML file, their details are listed below.' + button_prepare: 'Prepare import' + button_perform_import: 'Confirm import' + description: "Provide a BCF-XML v2.1 file to import into this project. You can examine its contents before performing the import." + perform_description: "Do you want to import or update the issues listed above?" + export: + format: + bcf: "BCF-XML" diff --git a/modules/bcf/config/routes.rb b/modules/bcf/config/routes.rb new file mode 100644 index 00000000000..ec4e6f23e1e --- /dev/null +++ b/modules/bcf/config/routes.rb @@ -0,0 +1,46 @@ +#-- copyright +# OpenProject Backlogs Plugin +# +# Copyright (C)2013-2014 the OpenProject Foundation (OPF) +# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda +# Copyright (C)2010-2011 friflaj +# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns +# Copyright (C)2009-2010 Mark Maglana +# Copyright (C)2009 Joe Heck, Nate Lowrie +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 3. +# +# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs. +# The copyright follows: +# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj +# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +OpenProject::Application.routes.draw do + scope '', as: 'bcf' do + scope 'projects/:project_id', as: 'project' do + resources :linked_issues, controller: 'bcf/linked_issues' do + get :import, action: :import, on: :collection + post :prepare_import, action: :prepare_import, on: :collection + post :import, action: :perform_import, on: :collection + end + end + end +end diff --git a/modules/bcf/db/migrate/20181214103300_add_bcf_plugin.rb b/modules/bcf/db/migrate/20181214103300_add_bcf_plugin.rb new file mode 100644 index 00000000000..5831a4d5967 --- /dev/null +++ b/modules/bcf/db/migrate/20181214103300_add_bcf_plugin.rb @@ -0,0 +1,36 @@ +class AddBcfPlugin < ActiveRecord::Migration[5.1] + + def change + create_table :bcf_issues do |t| + t.text :uuid, index: true + t.column :markup, :xml + + t.references :project, foreign_key: { on_delete: :cascade }, index: true + t.references :work_package, foreign_key: { on_delete: :cascade }, index: { unique: true } + end + + create_table :bcf_viewpoints do |t| + t.text :uuid, index: true + t.column :viewpoint, :xml + t.text :viewpoint_name + + t.references :issue, + foreign_key: { to_table: :bcf_issues, on_delete: :cascade } + + # Create unique index on issue and uuid to avoid duplicates on resynchronization + t.index %i[uuid issue_id], unique: true + end + + create_table :bcf_comments do |t| + t.text :uuid, index: true + t.references :journal, index: true + + t.references :issue, + foreign_key: { to_table: :bcf_issues, on_delete: :cascade }, + index: true + + # Create unique index on issue and uuid to avoid duplicates on resynchronization + t.index %i[uuid issue_id], unique: true + end + end +end diff --git a/modules/bcf/lib/open_project/bcf.rb b/modules/bcf/lib/open_project/bcf.rb new file mode 100644 index 00000000000..b99fc418955 --- /dev/null +++ b/modules/bcf/lib/open_project/bcf.rb @@ -0,0 +1,6 @@ +module OpenProject + module Bcf + require "open_project/bcf/engine" + require "open_project/bcf/bcf_xml" + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml.rb b/modules/bcf/lib/open_project/bcf/bcf_xml.rb new file mode 100644 index 00000000000..24ce5b6b780 --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/bcf_xml.rb @@ -0,0 +1,5 @@ +module OpenProject::Bcf + module Bcf + require_relative './bcf_xml/importer' + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/exporter.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/exporter.rb new file mode 100644 index 00000000000..b1f9b45da18 --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/exporter.rb @@ -0,0 +1,145 @@ +require 'fileutils' + +module OpenProject::Bcf::BcfXml + class Exporter < ::WorkPackage::Exporter::Base + include Redmine::I18n + + def current_user + User.current + end + + def work_packages + super.includes(:journals, bcf_issue: [:comments, { viewpoints: :attachments }]) + end + + def list + Dir.mktmpdir do |dir| + files = create_bcf! dir + + zip = zip_folder dir, files + yield success(zip) + end + rescue StandardError => e + Rails.logger.error "Failed to export work package list #{e} #{e.message}" + raise e + end + + def success(zip) + WorkPackage::Exporter::Success + .new format: :xls, + content: zip, + title: bcf_filename, + mime_type: 'application/octet-stream' + end + + def bcf_filename + # We often have an internal query name that is not meant + # for public use or was given by a user. + if query.name.present? && query.name != '_' + return sane_filename("#{query.name}.bcfzip") + end + + sane_filename( + "#{Setting.app_title} #{I18n.t(:label_work_package_plural)} \ + #{format_time_as_date(Time.now, '%Y-%m-%d')}.bcfzip" + ) + end + + def zip_folder(dir, files) + zip_file = File.join(dir, bcf_filename) + + Zip::File.open(zip_file, Zip::File::CREATE) do |zip| + files.each do |file| + name = file.sub("#{dir}/", "") + zip.add name, file + end + end + + File.open(zip_file, 'r') + end + + def create_bcf!(bcf_folder) + manifest_file = write_manifest(bcf_folder) + files = [manifest_file] + + work_packages.find_each do |wp| + # Update or create the BCF issue from the given work package + issue = IssueWriter.update_from!(wp) + + # Create a folder for the issue + issue_folder = topic_folder_for(bcf_folder, issue) + + # Append the markup itself + files << topic_markup_file(issue_folder, issue) + + # Append any viewpoints + files.concat viewpoints_for(issue_folder, issue) + + # TODO additional files such as BIM snippets + end + + files + end + + ## + # Write the manifest file /bcf.version + def write_manifest(dir) + File.join(dir, "bcf.version").tap do |manifest_file| + dump_file manifest_file, manifest_xml + end + end + + ## + # Create and return the issue folder + # /dir// + def topic_folder_for(dir, issue) + File.join(dir, issue.uuid).tap do |issue_dir| + Dir.mkdir issue_dir + end + end + + ## + # Write each work package BCF + def topic_markup_file(issue_dir, issue) + File.join(issue_dir, 'markup.bcf').tap do |file| + dump_file file, issue.markup + end + end + + ## + # Write viewpoints + def viewpoints_for(issue_dir, issue) + [].tap do |files| + issue.viewpoints.find_each do |vp| + vp_file = File.join(issue_dir, vp.viewpoint_name) + snapshot_file = File.join(issue_dir, vp.snapshot.filename) + + # Copy the files + dump_file vp_file, vp.viewpoint + FileUtils.cp vp.snapshot.local_path, snapshot_file + + files << vp_file << snapshot_file + end + end + end + + def manifest_xml + Nokogiri::XML::Builder.new do |xml| + xml.comment created_by_comment + xml.Version "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema", "VersionId" => "2.1" do + xml.DetailedVersion "2.1" + end + end.to_xml + end + + def dump_file(path, content) + File.open(path, "w") do |f| + f.write content + end + end + + def created_by_comment + " Created by #{Setting.app_title} #{OpenProject::VERSION} at #{Time.now} " + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/file_entry.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/file_entry.rb new file mode 100644 index 00000000000..87de3c8abeb --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/file_entry.rb @@ -0,0 +1,14 @@ +## +# Helper class to provide uploads from IO streams. +module OpenProject::Bcf::BcfXml + class FileEntry < StringIO + + def initialize(stream, filename:) + super(stream.read) + @original_filename = filename + end + + attr_reader :original_filename + alias :path :original_filename + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb new file mode 100644 index 00000000000..849c98b728f --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb @@ -0,0 +1,67 @@ +require 'activerecord-import' +require_relative 'issue_reader' + +module OpenProject::Bcf::BcfXml + class Importer + + attr_reader :project, :zip, :current_user + + def initialize(project, current_user:) + @project = project + @current_user = current_user + end + + ## + # Get a list of issues contained in a BCF + # but do not perform the import + def get_extractor_list!(file) + Zip::File.open(file) do |zip| + yield_topic_entries(zip) + .map do |entry| + to_listing(MarkupExtractor.new(entry)) + end + end + end + + def import!(file) + Zip::File.open(file) do |zip| + # Extract all topics of the zip and save them + synchronize_topics(zip) + + # TODO: Extract documents + + # TODO: Extract BIM snippets + end + rescue => e + Rails.logger.error "Failed to import BCF Zip #{file}: #{e} #{e.message}" + Rails.logger.debug { e.backtrace.join("\n") } + raise e + end + + private + + def to_listing(extractor) + keys = %i[uuid title priority status description author assignee modified_author due_date] + Hash[keys.map { |k| [k, extractor.public_send(k)] }].tap do |attributes| + attributes[:viewpoint_count] = extractor.viewpoints.count + attributes[:comments_count] = extractor.comments.count + end + end + + def synchronize_topics(zip) + yield_topic_entries(zip) + .map do |entry| + issue = IssueReader.new(project, zip, entry, current_user: current_user).extract! + issue.save + end + .count + end + + ## + # Yields topic entries and their uuid from the ZIP files + # while skipping all other entries + def yield_topic_entries(zip) + zip.select { |entry| entry.name.end_with?('markup.bcf') } + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb new file mode 100644 index 00000000000..9eb17bc0abe --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb @@ -0,0 +1,201 @@ +## +# Extracts sections of a BCF markup file +# manually. If we want to extract the entire markup, +# this should be turned into a representable/xml decorator +require_relative 'file_entry' + +module OpenProject::Bcf::BcfXml + class IssueReader + + attr_reader :zip, :entry, :issue, :extractor, :project, :user, :type + + def initialize(project, zip, entry, current_user:) + @zip = zip + @entry = entry + @project = project + @user = current_user + @issue = find_or_initialize_issue + @extractor = MarkupExtractor.new(entry) + + # TODO fixed type + @type = ::Type.find_by(name: 'Issue [BCF]') + end + + def extract! + issue.markup = extractor.markup + + # Viewpoints will be extended on import + build_viewpoints + + # Synchronize with a work package + synchronize_with_work_package + + # Comments will be extended on import + build_comments + + issue + end + + private + + def synchronize_with_work_package + call = + if issue.work_package + update_work_package + else + create_work_package + end + + if call.success? + wp = call.result + issue.work_package = wp + create_comment(user, I18n.t('bcf.bcf_xml.import_update_comment')) unless wp.previous_changes.empty? + else + Rails.logger.error "Failed to synchronize BCF #{issue.uuid} with work package: #{call.errors.full_messages.join("; ")}" + end + end + + def create_work_package + wp = WorkPackage.new work_package_attributes + + CreateWorkPackageService + .new(user: user) + .call(wp, send_notifications: false) + end + + def update_work_package + WorkPackages::UpdateService + .new(user: user, work_package: issue.work_package) + .call(attributes: work_package_attributes, send_notifications: false) + end + + ## + # Get mapped and raw attributes from MarkupExtractor + # and return all values that are non-nil + def work_package_attributes + { + # Fixed attributes we know + project: project, + type: type, + + # Native attributes from the extractor + subject: extractor.title, + description: extractor.description, + due_date: extractor.due_date, + + # Mapped attributes + author: find_user_in_project(extractor.author), + assigned_to: find_user_in_project(extractor.assignee), + status_id: statuses.fetch(extractor.status, statuses[:default]), + priority_id: priorities.fetch(extractor.priority, priorities[:default]), + }.compact + end + + ## + # Extend comments with new or updated values from XML + def build_comments + extractor.comments.each do |data| + next if issue.comments.has_uuid?(data[:uuid]) + comment = issue.comments.build data.slice(:uuid) + + # Cannot link to a journal when no work package + next if issue.work_package.nil? + author = get_comment_author(data) + call = create_comment(author, data[:comment]) + + if call.success? + comment.journal = call.result + else + Rails.logger.error "Failed to create comment for BCF #{issue.uuid}: #{call.errors.full_messages.join("; ")}" + end + end + end + + ## + # Try to find an author with the given mail address + def get_comment_author(comment) + author = find_user_in_project(comment[:author]) + + # If none found, use the current user + return user if author.nil? + + # If found, check if the author can comment + return user unless author.allowed_to?(:add_work_package_notes, project) + + author + end + + ## + # Try to find the given user by mail in the project + def find_user_in_project(mail) + project.users.find_by(mail: mail) + end + + def create_comment(author, content) + ::AddWorkPackageNoteService + .new(user: author, work_package: issue.work_package) + .call(content) + end + + ## + # Extract viewpoints from XML + def build_viewpoints + extractor.viewpoints.each do |vp| + next if issue.viewpoints.has_uuid?(vp[:uuid]) + + issue.viewpoints.build( + issue: issue, + uuid: vp[:uuid], + + # Save the viewpoint as XML + viewpoint: read_entry(vp[:viewpoint]), + viewpoint_name: vp[:viewpoint], + + # Save the snapshot as file attachment + snapshot: as_file_entry(vp[:snapshot]) + ) + end + end + + ## + # Find existing issue or create new + def find_or_initialize_issue + ::Bcf::Issue.find_or_initialize_by(uuid: topic_uuid, project_id: project.id) + end + + ## + # Get the topic name of an entry + def topic_uuid + entry.name.split('/').first + end + + ## + # Get an entry within the uuid + def as_file_entry(filename) + entry = zip.find_entry [topic_uuid, filename].join('/') + + if entry + FileEntry.new(entry.get_input_stream, filename: filename) + end + end + + ## + # Read an entry as string + def read_entry(filename) + entry = zip.find_entry [topic_uuid, filename].join('/') + entry.get_input_stream.read + end + + ## + # Keep a hash map of current status ids for faster lookup + def statuses + @statuses ||= Hash[Status.pluck(:name, :id)].merge(default: Status.default.id) + end + + ## + # Keep a hash map of current status ids for faster lookup + def priorities + @priorities ||= Hash[IssuePriority.pluck(:name, :id)].merge(default: IssuePriority.default.try(:id)) + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb new file mode 100644 index 00000000000..4bf7662aa1e --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb @@ -0,0 +1,180 @@ +## +# Creates or updates a BCF issue and markup from a work package +module OpenProject::Bcf::BcfXml + class IssueWriter + + attr_reader :work_package, :issue, :markup_doc, :markup_node + + def self.update_from!(work_package) + writer = new(work_package) + writer.update + + writer.issue + end + + def initialize(work_package) + @work_package = work_package + @issue = find_or_initialize_issue + + # Read the existing markup XML or build an empty one + @markup_doc = build_markup_document + + # Remember root markup node for easier access + @markup_node = @markup_doc.at_xpath('/Markup') + end + + def update + + # Replace topic node + replace_topic + + # Override all current comments + replace_comments + + # Override all current Viewpoints + replace_viewpoints + + # Replace the markup XML + issue.markup = markup_doc.to_xml + + # Save issue and potential new associations + issue.save! + end + + private + + ## + # Get the nokogiri document from the markup xml + def build_markup_document + if issue.markup + Nokogiri::XML issue.markup + else + build_initial_markup_xml.doc + end + end + + ## + # Initial markup file as basic BCF compliant xml + def build_initial_markup_xml + Nokogiri::XML::Builder.new do |xml| + xml.comment created_by_comment + xml.Markup "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema" + end + end + + ## + # Replace the topic node, if any + def replace_topic + markup_node.xpath('./Topic').remove + + Nokogiri::XML::Builder.with(markup_node) do |xml| + topic xml + end + end + + ## + # Render the topic of the work package + def topic(xml) + xml.Topic "Guid" => issue.uuid, + "TopicType" => work_package.type.name, + "TopicStatus" => work_package.status.name do + xml.Title work_package.subject + xml.CreationDate to_bcf_datetime(work_package.created_at) + xml.ModifiedDate to_bcf_datetime(work_package.updated_at) + xml.Description work_package.description + xml.CreationAuthor work_package.author.mail + xml.ReferenceLink url_helpers.work_package_url(work_package) + + if priority = work_package.priority + xml.Priority priority.name + end + + if work_package.due_date + xml.DueDate to_bcf_date(work_package.due_date) + end + + if journal = work_package.journals.select(:user_id).last + xml.ModifiedAuthor journal.user.mail if journal.user_id + end + + if assignee = work_package.assigned_to + xml.AssignedTo assignee.mail + end + end + end + + def replace_comments + markup_node.xpath('./Comment').remove + + Nokogiri::XML::Builder.with(markup_node) do |xml| + comments xml + end + end + + def replace_viewpoints + markup_node.xpath('./Viewpoints').remove + + Nokogiri::XML::Builder.with(markup_node) do |xml| + viewpoints xml + end + end + + ## + # Render the comments of the work package as XML nodes + def comments(xml) + comments = issue.comments.group_by(&:journal_id) + work_package.journals.select(:id, :notes, :user_id, :created_at).map do |journal| + next if journal.notes.empty? + + # Create BCF comment reference for the journal + comment = comments[journal.id]&.first || issue.comments.build(issue_id: issue, journal_id: journal.id) + comment_node xml, comment.uuid, journal, work_package + end + end + + ## + # Create a single topic node + def comment_node(xml, uuid, journal, work_package) + xml.Comment "Guid" => uuid do + xml.Date to_bcf_datetime(journal.created_at) + xml.Author journal.user.mail if journal.user_id + xml.Comment journal.notes + end + end + + ## + # Write the current set of viewpoints + def viewpoints(xml) + issue.viewpoints.find_each do |vp| + xml.Viewpoints "Guid" => vp.uuid do + xml.Viewpoint vp.viewpoint_name + xml.Snapshot vp.snapshot.filename + end + end + end + + ## + # + def created_by_comment + " Created by #{Setting.app_title} #{OpenProject::VERSION} at #{Time.now} " + end + + ## + # Find existing issue or create new + def find_or_initialize_issue + ::Bcf::Issue.find_or_initialize_by(work_package: work_package, project_id: work_package.project_id) + end + + def to_bcf_datetime(date_time) + date_time.utc.iso8601 + end + + def to_bcf_date(date) + date.iso8601 + end + + def url_helpers + @url_helpers ||= OpenProject::StaticRouting::StaticUrlHelpers.new + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb new file mode 100644 index 00000000000..ac8b196d96e --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb @@ -0,0 +1,84 @@ +## +# Extracts sections of a BCF markup file +# manually. If we want to extract the entire markup, +# this should be turned into a representable/xml decorator + +module OpenProject::Bcf::BcfXml + class MarkupExtractor + + attr_reader :entry, :markup, :doc + + def initialize(entry) + @markup = entry.get_input_stream.read + @doc = Nokogiri::XML markup + end + + def uuid + extract_non_empty :@Guid, attribute: true + end + + def title + extract_non_empty :Title + end + + def priority + extract_non_empty :Priority + end + + def status + extract_non_empty :@TopicStatus, attribute: true + end + + def description + extract_non_empty :Description + end + + def author + extract_non_empty :CreationAuthor + end + + def assignee + extract_non_empty :AssignedTo + end + + def modified_author + extract_non_empty :ModifiedAuthor + end + + def due_date + date = extract_non_empty :DueDate + Date.iso8601(date) unless date.nil? + rescue ArgumentError + nil + end + + def viewpoints + doc.xpath('/Markup/Viewpoints').map do |node| + { + uuid: node['Guid'], + viewpoint: node.xpath('Viewpoint/text()').to_s, + snapshot: node.xpath('Snapshot/text()').to_s + } + end + end + + def comments + doc.xpath('/Markup/Comment').map do |node| + { + uuid: node['Guid'], + date: node.xpath('Date/text()').to_s, + author: node.xpath('Author/text()').to_s, + comment: node.xpath('Comment/text()').to_s + } + end + end + + private + + def extract_non_empty(path, prefix: '/Markup/Topic/'.freeze, attribute: false) + suffix = attribute ? '' : '/text()'.freeze + path = [prefix, path.to_s, suffix].join('') + doc.xpath(path).to_s.presence + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/engine.rb b/modules/bcf/lib/open_project/bcf/engine.rb new file mode 100644 index 00000000000..45d35b20ef9 --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/engine.rb @@ -0,0 +1,59 @@ +require 'open_project/plugins' + +module OpenProject::Bcf + class Engine < ::Rails::Engine + engine_name :openproject_bcf + + include OpenProject::Plugins::ActsAsOpEngine + + register 'openproject-bcf', + author_url: 'https://openproject.com', + settings: { + default: { + } + } do + + project_module :bcf do + permission :view_linked_issues, + 'bcf/linked_issues': :index + + permission :manage_bcf, + 'bcf/linked_issues': %i[index import prepare_import perform_import] + end + + menu :project_menu, + :bcf, + { controller: '/bcf/linked_issues', action: :index }, + caption: :'bcf.label_bcf', + param: :project_id, + icon: 'icon2 icon-backlogs', + badge: 'bcf.experimental_badge' + end + + assets %w(bcf/bcf.css) + + patches %i[WorkPackage] + + patch_with_namespace :BasicData, :SettingSeeder + + extend_api_response(:v3, :work_packages, :work_package_collection) do + require_relative 'patches/api/v3/export_formats' + + prepend Patches::Api::V3::ExportFormats + end + + initializer 'bcf.register_hooks' do + # don't use require_dependency to not reload hooks in development mode + require 'open_project/xls_export/hooks/work_package_hook.rb' + end + + initializer 'bcf.register_mimetypes' do + Mime::Type.register "application/octet-stream", :bcf unless Mime::Type.lookup_by_extension(:bcf) + end + + config.to_prepare do + WorkPackage::Exporter + .register_for_list(:bcf, OpenProject::Bcf::BcfXml::Exporter) + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/patches/api/v3/export_formats.rb b/modules/bcf/lib/open_project/bcf/patches/api/v3/export_formats.rb new file mode 100644 index 00000000000..6e7bb68c843 --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/patches/api/v3/export_formats.rb @@ -0,0 +1,12 @@ +module OpenProject::Bcf::Patches + module Api::V3::ExportFormats + def representation_formats + super + [representation_format_bcf] + end + + def representation_format_bcf + representation_format :bcf, + mime_type: 'application/octet-stream' + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/patches/setting_seeder_patch.rb b/modules/bcf/lib/open_project/bcf/patches/setting_seeder_patch.rb new file mode 100644 index 00000000000..e1ee7187a8b --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/patches/setting_seeder_patch.rb @@ -0,0 +1,36 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +#++ + +module OpenProject::Bcf::Patches::SettingSeederPatch + def self.included(base) # :nodoc: + base.prepend InstanceMethods + end + + module InstanceMethods + def data + original_data = super + + unless original_data['default_projects_modules'].include? 'bcf' + original_data['default_projects_modules'] << 'bcf' + end + + original_data + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/patches/work_package_patch.rb b/modules/bcf/lib/open_project/bcf/patches/work_package_patch.rb new file mode 100644 index 00000000000..e95eb531c9b --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/patches/work_package_patch.rb @@ -0,0 +1,44 @@ +#-- copyright +# OpenProject Backlogs Plugin +# +# Copyright (C)2013-2014 the OpenProject Foundation (OPF) +# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda +# Copyright (C)2010-2011 friflaj +# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns +# Copyright (C)2009-2010 Mark Maglana +# Copyright (C)2009 Joe Heck, Nate Lowrie +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 3. +# +# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs. +# The copyright follows: +# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj +# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require_dependency 'work_package' + +module OpenProject::Bcf::Patches::WorkPackagePatch + def self.included(base) + base.class_eval do + has_one :bcf_issue, class_name: 'Bcf::Issue', foreign_key: 'work_package_id' + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/version.rb b/modules/bcf/lib/open_project/bcf/version.rb new file mode 100644 index 00000000000..bbdebba05b9 --- /dev/null +++ b/modules/bcf/lib/open_project/bcf/version.rb @@ -0,0 +1,7 @@ +require 'open_project/version' + +module OpenProject + module Bcf + VERSION = ::OpenProject::VERSION.to_semver + end +end diff --git a/modules/bcf/lib/openproject-bcf.rb b/modules/bcf/lib/openproject-bcf.rb new file mode 100644 index 00000000000..bbf40eeb2f7 --- /dev/null +++ b/modules/bcf/lib/openproject-bcf.rb @@ -0,0 +1 @@ +require 'open_project/bcf' diff --git a/modules/bcf/openproject-bcf.gemspec b/modules/bcf/openproject-bcf.gemspec new file mode 100644 index 00000000000..10762cab8ad --- /dev/null +++ b/modules/bcf/openproject-bcf.gemspec @@ -0,0 +1,23 @@ +# encoding: UTF-8 +$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path("../../lib", __dir__) + +require "open_project/bcf/version" + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "openproject-bcf" + s.version = OpenProject::Bcf::VERSION + s.authors = "OpenProject GmbH" + s.email = "info@openproject.com" + s.homepage = "https://community.openproject.org/" + s.summary = "OpenProject BCF import/export" + s.description = "This OpenProject plugin introduces BCF functionality" + + s.files = Dir["{app,config,db,lib}/**/*", "CHANGELOG.md", "README.rdoc"] + s.test_files = Dir["spec/**/*"] + + s.add_dependency 'rails', '~> 5' + s.add_dependency 'rubyzip', '~> 1.2' + s.add_dependency 'activerecord-import' +end diff --git a/modules/bim_seeder/app/seeders/bim_seeder/basic_data/type_seeder.rb b/modules/bim_seeder/app/seeders/bim_seeder/basic_data/type_seeder.rb index fe6d04e406f..ceb4da0c61e 100644 --- a/modules/bim_seeder/app/seeders/bim_seeder/basic_data/type_seeder.rb +++ b/modules/bim_seeder/app/seeders/bim_seeder/basic_data/type_seeder.rb @@ -32,7 +32,7 @@ module BimSeeder module BasicData class TypeSeeder < ::BasicData::TypeSeeder def type_names - %i[task milestone phase building_model defect approval] + %i[task milestone phase building_model defect approval bcf_issue] end def type_table @@ -42,7 +42,8 @@ module BimSeeder phase: [3, true, :default_color_blue_dark, false, false, :default_type_phase], building_model: [4, true, :default_color_blue, true, false, 'seeders.bim.default_type_building_model'], defect: [5, true, :default_color_red, true, false, 'seeders.bim.default_type_defect'], - approval: [6, true, :default_color_grey_dark, true, false, 'seeders.bim.default_type_approval'] + approval: [6, true, :default_color_grey_dark, true, false, 'seeders.bim.default_type_approval'], + bcf_issue: [6, true, :default_color_grey_red, true, false, 'seeders.bim.default_type_bcf_issue'] } end end diff --git a/modules/bim_seeder/config/locales/en.seeders.bim.yml b/modules/bim_seeder/config/locales/en.seeders.bim.yml index 900df21a27f..94e2304bd01 100644 --- a/modules/bim_seeder/config/locales/en.seeders.bim.yml +++ b/modules/bim_seeder/config/locales/en.seeders.bim.yml @@ -31,6 +31,7 @@ en: default_type_building_model: Building model default_type_defect: Defect default_type_approval: Approval + default_type_bcf_issue: Issue [BCF] demo_data: welcome: title: "Welcome to OpenProject BIM Edition!" @@ -57,6 +58,7 @@ en: - work_package_tracking - news - wiki + - bcf news: - title: Welcome to your demo project summary: > @@ -70,6 +72,7 @@ en: - 'seeders.bim.default_type_building_model' - 'seeders.bim.default_type_defect' - 'seeders.bim.default_type_approval' + - 'seeders.bim.default_type_bcf_issue' categories: - Category 1 (to be changed in Project settings) queries: @@ -105,6 +108,9 @@ en: - type - status - assigned_to + - name: Issues [BCF] + status: open + type: 'seeders.bim.default_type_bcf_issue' work_packages: - subject: Project kick-off description: Plan and execute the project kick-off. @@ -181,10 +187,10 @@ en: Please activate further [Modules](%{base_url}/projects/demo-project/settings/modules) in the Project settings in order to have more features in your project. **You can:** - * add a Scrum module (Backlogs), * add time tracking, reporting, and budgets (Time Tracking, Cost Reports, Budgets), * add a wiki, * add meetings, + * add BCF import/export, * and more. **Visuals:** diff --git a/modules/bim_seeder/openproject-bim_seeder.gemspec b/modules/bim_seeder/openproject-bim_seeder.gemspec index a278d0579b4..84fbf7ef84c 100644 --- a/modules/bim_seeder/openproject-bim_seeder.gemspec +++ b/modules/bim_seeder/openproject-bim_seeder.gemspec @@ -15,4 +15,6 @@ Gem::Specification.new do |s| s.version = "1.0.0" s.files = Dir["{app,lib,config}/**/*"] + %w(CHANGELOG.md README.md) + + s.add_dependency "openproject-bcf" end diff --git a/modules/bim_seeder/spec/seeders/demo_data_seeder_spec.rb b/modules/bim_seeder/spec/seeders/demo_data_seeder_spec.rb index 3f56411ca05..f28c55bbecf 100644 --- a/modules/bim_seeder/spec/seeders/demo_data_seeder_spec.rb +++ b/modules/bim_seeder/spec/seeders/demo_data_seeder_spec.rb @@ -59,7 +59,7 @@ describe 'seeds' do expect(Project.count).to eq 1 expect(WorkPackage.count).to eq 18 expect(Wiki.count).to eq 1 - expect(Query.count).to eq 4 + expect(Query.count).to eq 5 ensure ActionMailer::Base.perform_deliveries = perform_deliveries end diff --git a/modules/xls_export/lib/open_project/xls_export/filename_helper.rb b/modules/xls_export/lib/open_project/xls_export/filename_helper.rb deleted file mode 100644 index 3d140027ab0..00000000000 --- a/modules/xls_export/lib/open_project/xls_export/filename_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -module OpenProject::XlsExport - class FilenameHelper - # Remove characters that could cause problems on popular OSses - # => A string that does not start with a space or dot and does not contain any of \/:*?"<>| - def self.sane_filename(str) - str.gsub(/^[ \.]/,"").gsub(/[\\\/:\*\?"<>|"]/, "_") - end - end -end diff --git a/modules/xls_export/lib/open_project/xls_export/work_package_xls_export.rb b/modules/xls_export/lib/open_project/xls_export/work_package_xls_export.rb index b488dec42f6..b007929729d 100644 --- a/modules/xls_export/lib/open_project/xls_export/work_package_xls_export.rb +++ b/modules/xls_export/lib/open_project/xls_export/work_package_xls_export.rb @@ -29,7 +29,7 @@ module OpenProject enable! WithDescription if with_descriptions enable! WithRelations if with_relations - success(spreadsheet.xls) + yield success(spreadsheet.xls) end def success(content) @@ -107,7 +107,7 @@ module OpenProject end def xls_export_filename - FilenameHelper.sane_filename( + sane_filename( "#{Setting.app_title} #{I18n.t(:label_work_package_plural)} \ #{format_time_as_date(Time.now, '%Y-%m-%d')}.xls" ) diff --git a/package.json b/package.json index 075b61ba732..cb151674fcc 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,8 @@ "private": true, "engines": { "node": "~8.12.0" + }, + "dependencies": { + "webfonts-generator": "^0.4.0" } }