From 314cf4df438d2b4c110487d86c6d365bf5728699 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Fri, 5 Apr 2024 13:50:30 -0400 Subject: [PATCH 01/77] =?UTF-8?q?=F0=9F=9A=A7=20Add=20files=20from=20PALs?= =?UTF-8?q?=20Hyku=20not=20in=20other=20repos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a first pass of the PALs work. There was a lot of head scratching and copying things into Hyku Prime. Together this is a rough process. Related to: - https://github.com/scientist-softserv/palni_palci_knapsack/commit/3595d6febba77918a0e5e6497aa58aaefb8e7d4d --- app/assets/images/loading-progress.gif | Bin 0 -> 10273 bytes app/assets/javascripts/admin_font_select.js | 6 + app/controllers/api/sushi_controller.rb | 52 ++ ...llections_controller_behavior_decorator.rb | 22 + .../admin/workflows_controller_decorator.rb | 17 + .../hyrax/my/works_controller_decorator.rb | 29 + .../workflow_actions_controller_decorator.rb | 20 + app/jobs/create_derivatives_job_decorator.rb | 17 + app/jobs/create_large_derivatives_job.rb | 16 + app/jobs/file_set_index_job.rb | 5 + app/jobs/prune_stale_guest_users_job.rb | 10 + app/jobs/work_index_job.rb | 5 + app/models/file_download_stat_decorator.rb | 25 + app/models/sushi.rb | 497 ++++++++++++++++++ app/models/sushi/error.rb | 123 +++++ app/models/sushi/item_report.rb | 181 +++++++ app/models/sushi/platform_report.rb | 221 ++++++++ app/models/sushi/platform_usage_report.rb | 160 ++++++ app/models/sushi/report_list.rb | 55 ++ app/models/sushi/server_status.rb | 22 + .../hyku/menu_presenter.rb/menu_presenter.rb | 56 ++ app/services/generate_counter_metrics.rb | 40 ++ ...lection_member_search_service_decorator.rb | 22 + .../hyrax/solr_query_service_decorator.rb | 13 + app/services/import_counter_metrics.rb | 92 ++++ .../location_decorator.rb | 39 ++ lib/tasks/counter_metrics.rake | 47 ++ lib/tasks/sidekiq.rake | 13 + lib/tasks/tenants.rake | 56 ++ lib/tasks/workflow_setup.rake | 6 + .../jobs/create_large_derivatives_job_spec.rb | 41 ++ spec/models/sushi/item_report_spec.rb | 251 +++++++++ spec/models/sushi/platform_report_spec.rb | 179 +++++++ .../sushi/platform_usage_report_spec.rb | 57 ++ spec/models/sushi/report_list_spec.rb | 48 ++ spec/models/sushi/server_status_spec.rb | 26 + spec/models/sushi_spec.rb | 277 ++++++++++ spec/requests/api/sushi_spec.rb | 94 ++++ spec/services/import_counter_metrics_spec.rb | 14 + 39 files changed, 2854 insertions(+) create mode 100644 app/assets/images/loading-progress.gif create mode 100644 app/assets/javascripts/admin_font_select.js create mode 100644 app/controllers/api/sushi_controller.rb create mode 100644 app/controllers/concerns/hyrax/collections_controller_behavior_decorator.rb create mode 100644 app/controllers/hyrax/admin/workflows_controller_decorator.rb create mode 100644 app/controllers/hyrax/my/works_controller_decorator.rb create mode 100644 app/controllers/hyrax/workflow_actions_controller_decorator.rb create mode 100644 app/jobs/create_derivatives_job_decorator.rb create mode 100644 app/jobs/create_large_derivatives_job.rb create mode 100644 app/jobs/file_set_index_job.rb create mode 100644 app/jobs/prune_stale_guest_users_job.rb create mode 100644 app/jobs/work_index_job.rb create mode 100644 app/models/file_download_stat_decorator.rb create mode 100644 app/models/sushi.rb create mode 100644 app/models/sushi/error.rb create mode 100644 app/models/sushi/item_report.rb create mode 100644 app/models/sushi/platform_report.rb create mode 100644 app/models/sushi/platform_usage_report.rb create mode 100644 app/models/sushi/report_list.rb create mode 100644 app/models/sushi/server_status.rb create mode 100644 app/presenters/hyku/menu_presenter.rb/menu_presenter.rb create mode 100644 app/services/generate_counter_metrics.rb create mode 100644 app/services/hyrax/collections/collection_member_search_service_decorator.rb create mode 100644 app/services/hyrax/solr_query_service_decorator.rb create mode 100644 app/services/import_counter_metrics.rb create mode 100644 lib/hyrax/controlled_vocabularies/location_decorator.rb create mode 100644 lib/tasks/counter_metrics.rake create mode 100644 lib/tasks/sidekiq.rake create mode 100644 lib/tasks/tenants.rake create mode 100644 lib/tasks/workflow_setup.rake create mode 100644 spec/jobs/create_large_derivatives_job_spec.rb create mode 100644 spec/models/sushi/item_report_spec.rb create mode 100644 spec/models/sushi/platform_report_spec.rb create mode 100644 spec/models/sushi/platform_usage_report_spec.rb create mode 100644 spec/models/sushi/report_list_spec.rb create mode 100644 spec/models/sushi/server_status_spec.rb create mode 100644 spec/models/sushi_spec.rb create mode 100644 spec/requests/api/sushi_spec.rb create mode 100644 spec/services/import_counter_metrics_spec.rb diff --git a/app/assets/images/loading-progress.gif b/app/assets/images/loading-progress.gif new file mode 100644 index 0000000000000000000000000000000000000000..77a5af85af6f598c84e1eac1c42370ce95608c12 GIT binary patch literal 10273 zcmeI2cU+U%w)c?^0)rq3iW)&ck=_%EQl*5RgpMQ-IsuZ1}$5V%= z$Dh7R`;?x3`Kn&L2}a?hZLqmB$I|FrTz>7+`oYrn0WQ0CXkioL6CadP3aLI&Lh6qAq^5tjgo13}^vfWKb6s zKUz>nV(ie)o>*rO6yU5yIKsmltHetl>7ORJd;YUo6h`!iIihwR?qYs$PcaEm@iUYD zQBY6sUl(K_oQp^~M@$g35A<4!0&bsmhsiBc@tOpw6;o*J$WFJC-Ae0H$ExBGNw zduwxJeQov0%JS0U!u;Iq%=Fac#Q50g$nel0X`sLFac@s|S7%3iTWd>m)1$_Q`nuYh z>Z;0$^0LyB;-bQW{Jh+p?5xa;bYfa+N-`lSF(Ez<9~%=L6^V-o4-0)55*!p5;P2<_ zp3NbM@GBnWF1MBK&Yu(q>P*+n` zxp!AtNl^g=l$Vp0k(QE_5El~_5xygI`<9>p|4lyL8-VNAuJT;D%*}O)lj9;g+XdEN zSeTj4GcwTA(b7;;QIZ|RjnC8!v=nzKgvq{w;>-5)E#RO z79E-7Wd<~bLQOI=;90pju?excpqmWTK}?YmVc^Jc7$gz_a<##k8a`-CZcYPhgZjZv z4b?-Wk(7EA2v9-A(r83%O3o%>Kvs(>YiUXCafF4Km8aFQ@rUucm&9qWokzx)r>5%G zM)bVc?OksFM#=-KjZfEU(^~th$-BkUuv|rZ9{E;UKEis`YeUMxwxi;|a`N*VbVXdV?i7lj&HhYWT| ze+}0L&!XH|sxVh{W(L~6u+a!(GPyIe})lBVXd zTy3oP9CYc!=JM;KVmte>?VM9-5Zepq&?S$zgD5Y$q0pru`#smaBL@b>4OGZ zQwNElq>-3$V52t(P))^JSFiXaZn8jOY-743dNkdqZ~W;Kr=G*TVZy87L?dz93*pN; z;0Ng?ra%w3mlFf*zV%NfWL$Wb2cDsB2!|H4fX3lrYGHv9?-}er#cj!j@(g&n(T!Y{ z(`CCJdsJ?G(0@yx4bUHRvVPYM1#k#Y;TLT9?)AlI{8d$aCXXy)A&8MEl3T&UJw}%p^;yp`=v6m&{K0;dS;lEU6ki_O+~eiBd2&rHO_<55FC(J`a+CFP z$w>RQnILwH;ZUKF^2VNfhCgWMh^1t4KDjW78n)f=x$d%bMyF+tE#|%$9_nCXh6F~h z*adpC$I8!1T+)z!h<&1?d@N&UVNr8|PLmOh4SF$yuv#m=$Y)vh!2&9zBWZr|W@#b* zx)Q4)PKkRB$Ln@ZZS`{g@tR|BRpUZ z(Vm93PO$h`bb=WShD|40WI-XBdD#jUXIKUEMO5kK#(-R{Op%YQ2@e`ufdL zLOmWh@OY3ggaHA{saUJ$bfTKEq-MiM%gt*^8}Y_av*E$GDoGY_Ci=!3@oB8ncT$jf?P4lB(_YMc-GBTAU)w3*d}i^5R^xq2p{KagmD1&5 zzttw5?uhFhTJ}dD2Bs?Re!`g->{RyIm3#qam|Z0VW{vkrJkc$W^^}ebRU_WGXplf4 zgx?N2y?5`z$tLjyfbztxb_}z=XE$gdTDr zE2d(t7%Pit>W)XW&)}0fi7~TrOVhJ3G8Q4@yN2+lrNiDvi>84t;H=w>XaBp?P9%_% zhi309crqFkvc|ruo6=*h5VRZqgvB;D>ili~fK&>VzBpxcM|Voug>=oVn-Z8(PjHqi z{cd4UvvZju7|3PY)4{ZL$8F|uOxod`z~v+C@e;*VP-A>#);H5^mJdV4+B@$cEk1ruO%0jRbX%L%7b2Wm~R zijWSugS&8jFBw3|)>W~&O!_!-3310+r2Rt`a~`b9;1bwGjR zMXcEiE^}i4tn-w)6Kue%;jKiC4;@St0xzXK1!_XI5BJNZfA{|NGQ_Et*XpwKlGi9j zz`yqrZlHoWen9&^)E`vCS<~xK^Prk*>I!Ds9P58ysd*eRU(q5#f09DFF>&&H4uzr_ z#KXlL?qLOWcaKeohbATyU}+x7XUhBWfhm*=3v`Ohw9CtjN~tU2B?s zwKImBO8d>~s5=8|59T+=Vs$^JUX3QfH#wu`JwmVrAd1c4yE;g%etZC6Q4^z8B;z(F zRFrCppd@8w4Np(2MRsK=mb4xXt8!9w2EKGdRQuObD`g9LT58A`o%9lyW;aD_MIc>4 zui~pzTi#F(H=Un@Es?C&6Ct@`PZ4k?Ry_ajIOnxUy|Nx1aO-&kFigaO=W85R%VtY| zf8xwtOga3nU*7ZU`?*Ei0$ z9>zy!b)Qm!O)>f7os(n54`tN;>@D;wQ=094#py{iSnh@qmfM4=o1guj{W9;fc87G8 zDn%~p#)ky`W%rhJ;TZXj2%Ot0fl|=-%xS!)_;l^M?#yS7IAP|I6fFa_&r3Pnz8@F2 z@v&nnqZGdXGh}X^4s1aEyIg%T_5&XEpe_lwIZPg$m|s2o5mB_r5oMt#WY)mg+s@S% zW@q;xDiRirkA)`K;Llj`<_pfu%7U1hIoO6;nHM2}C52_hg=SUd_GC1ORIJ%;?M4s> zS0~FF%c{Oao3NT8H!CwT6Mh}DbGhLa^`#N@!;8)}YwmrBGv6@xtRr`H@Cme}Y)PSX z=;R&Zq|l0i{_yu4MN@}5p0y9q_&LFdh@K17PU3V_A(_{tlgDs-Y;hr9`F|fevQ~n< zwFy4&e+W@X0SudsMZ7}{$BN%Tv%NS;6Gt(x(K9+UEHxI1ikuGPw50d%7L0Zw2i64W zxXKavEY!oHR`WIbbIwcQ%@$Vzs=?Xdmo&m{_6)p$fr*t$p@xbSbefCstA$5H^=0h$ z`&}$+rMRbwlaTwM9+Ulbj!fG`obJO0myi5O8)mNjB1tV9E@`!(5WWwK*$(mB5ivgZ zKy{{hVzv%irIqRxbYZ+R>KTjp0ME<<<_3Aa=cnG&-j>@#-1h|O|GlFxn5)FTI{y~i zQ^VOk>D_11AjdR<_dUpcVY1GJ^VHtGSGRHER{@Q?hVi$vvkeb(iiom_I+M$b4}8ub zMLSI=JuQ=%k&~AV%`MC?%77uvDoq_LEUO>XR)G+q4hkSZA{A??Ub=r@zj5Fr#Rg>P zXw^uFrIQuVlq?>0VC9K_c~H%8&5V8BRBK(6^_h00qt(|Yf!0IMYN~8LG<l6JU^QnyM(}rjmsB`FI4Q>G>atmIBnNzxvU%`!PZ=>8 zjyl@okN{EI`;{fSWF3bajw^!=j+Ajr`Hp^KZds z<_c$;qWFB`;uCxyfUGT(15*eV)>aN6KnxXYVo`C4wjUxO!;%=1YJm&@T9BplDsAsT z`(?tjs(}s}6{$aj>ztT0=&lPKBG#Fu*%ROl^ivxfrQL6LY7QUO;ohw+s#-pdi1+y%$p2=tQH;cOQ%&pLg<6R1UhnzQIB+<< zmJmL-m=#g;phg_AsS-{V7RzS`S{o(g4rW)E6X%md6R4#%BqJ<#&mGy11_*vWw~x~r zyHcd8TWmEh#&?J9c6Qqh!y(Qke0S|OPKF>XFXFVs4uZEhK508?`Q#@SBiGR7s)O2u?w(JMbJ^i4_a6#+s zSG3oZ2uX+A35j^7R{M`!el_6fX93-WUkbf zyEj>Wy>{<7G%Va%(bbh~H6kKDA;Cq_-;IYRF+IbZ0CEK}&}SAF*%ADcnTji`d?Rx} zK#}T4O+a96vPeNwSCLDAE0BrUJ(x}i2q2p`G!I z7g!*!D2SW9Nvf#dIQgw|T*-6HR}ttRfr~^+&|X7;F!oVNgk(CYt3Z$!COJFDpNkQb zkeFLiYAP5XT3J?8Yj^{c@vykArIwCSvC^)!r-q)GnbA9#%Y`rW8k!&o1$&{JCl`EJ zsg#(R(#ID!T?AKDZ4u#H&ydSDYLc3VuZ`&hrhjh0>HmG~1S%kNY0ofT}pzenq z*0t3t81vkSw;*xKxwT9n>BJYp(12rf=HC@%LJT^PL) z`Eas-wYXz(tCNm#eP-WvX8UYzM%LK)T z^nLfZ=1R^*`0nLPuoD{*8SG4vh3A&1rmM6$)ez=Rn)Ec>nxgcVe|=Y|6-NiAkpB4s z{ptCeKV`w$MD#sv^aG1x?$Fau0^HGL^cjMm%_eVF!ozNGT|_ZqpfGR$ z1Q^6$KO};fo)Ib#O~n$Z=;MbrQh=2jL9w8;%-Xto^4xfl=O)Ow5~|k)DTdY550FC1 z^HZ4bUH%x1_4C*6x9VS5wpke@aWPJqrc{^&=mZrhgru!5J~^^puzhoE_ZBYLQr=oR z(*d6FF2L#@BF{%8K27(h*{M2WQ>Cj9oP1A+D8F?*9rluig~#vSxx1m?v@}#d#{QxX zy%Nc=q(!^SNj(~%bn&BBa$-=DIznL-jpPm}P!47M7NG)@vtrkM7rrvaQx08fx#!-_ zYC{q;-Oqy$jBn%`{?Z-U#U@_1=9VVT^kP0Vtk=S$s9!`QBUvLq%qo9Nevd$qYxbn% zxnc}Sxy8-yAbpFr(wshI!;k?A1s8?b4^nwB5EG`Xak)hqoQN%$)b2Nd57xx2ByLwfmo2Q zLYArNrC4&{u@gw3VhL9C3&;cIh8RH&vU7a`n+i+2dt%7yV6yced5@$tLh5VAhnsrm z=IzPq)K=%D`#1Oo1o}Lj!7Us-<6>N!u3rn%38`y)S?k~R>{G8`D;Q|{)c9+w>^pY) zGU!dA{WrIHI8;aPnn69`=9~tT?RKv@U6{P@4JfNnZ@k>czSbGnB^)npjJ;=~be*{7 zP)+?3I*k`}|K>ct$Q0#ZyY@%#@0=H|_22Uo;-6n?uHxdTtRG9GPcgYu#eQdGDtWHT zgq|uu+LraLT|4|zw0;X|=9dbhjekDSX{37%MLb73y5MNMa;R74PQpGqzbxn;x8Wl@aU~maY z!G^3rQM-RgL0^V{RqgPIGg$DPsJym-;1GY@ zY)@qK=FW#5F2>>~L7gTKb*$D%iry2x{1;MDr) z6_Ju$R9~=4yI&DWD0_QO*f#~A!E;SzLKt%|OwFBE=_P3B8E0pD$W0AxsZNQ*sS^5; z&YKMQMSe?zKrhm2P13`}J<@3omgVnkd%51&=M7Fh>O5o`P1Le=c&%cuDENS2t{CS2f)ikR6$q!$? zekC#Tx2j{gI-@}7nOeandNDzte1@Gz%c>zLkZSB?DNa*-V>vgl7b!XQd8bDs?@oRc zUSeXa+1Y@0OVuO<63uDw}axog!)CiE)uU14Q@29d0|j zDG~=wyHI;0;t9M%(_=@|f1>Nn0SBY2;$Y-O{x&?(b~{H1{chvS$24F9|82y$+@s2o z#H72bXOV^~)WDoEKc2CCqS6*-9jxS5eR#L{ZB)K@cnhoI>iVZ|YW3dk6KiaB#w=!S zVpwfo-7D_*e*_^vAsg`KxL55)z*_9YHKEn6vUZ>?Z&M!N|3}*&2_c6!yRK(qiL`QL&_kH{zTv>)-~U%&NwS|O$|cRo=SW=EQ>!P`Eg6qkeY&}0qSZbH-q@Yyt%{1D z&}OBYE@cqKC8|#!U%0yX?gc2xL1zN<{2d#>ST~DOp^(D9&A3ARGj(&@`N2Ak9V3fM z6HXZu+Cltl+`Xsz#=n-D@5`*-988-u6NNhy_$@{52<|O(`?5!&pTL-D;_FgWRTi+! z>arFI+>6=%%6mGUO*|aKnK$x|bcpwd=NoTXRe6uTuKASG;`5D`6nc%B>#g+B8YXMH z&E+}RSvzm9?|N1hi&Ab!68mW6TM0+`4~2EO;%Q~ND&xQzB9rnrGgncwlfL+)3#uwn z$>+W5d{Pa~hxsh^W7h-=L|#$&uyJg~dWSMJzWn=4&d-7&^uOi?PbeODknfgFSy|In z#ysRX5e7eC2s?uz&Rx+d+$1_7<TQcfwvi5CTa@}B%m|yZ>B{=BIbHRx>UBB?ESEqX+r6vdOnKe*<@s$Y zJIahCdbZTos|mD>aVct9sy-!$wuX&|1vf0#&|y_T5yQLRJ@Z4>*GYARvEY^828@0s zu&4nATc*39bKpjmB%F5*DJjk{XnW+?1X%xKS`d7sh~!iq<#oH^ZM@ucWV3?mxRuZw zC+@0J7dk6^^&@2I9F6HWFZZ&S7q~{PLqFTKwkPvj`43*RFPmL2Ybi{XQIbfWKzeNU z!y&zTO|z*Gk4WM-v-5K)9rwSuFi4f309p5d=&gpPmkHMuRB1K6%rG(&Nw9VKZzzjQ z(VS1To}n}?uFD_a3!RENaVDPXmnfKq6ugVytF!;*qw!%rEt9gqVa((0->h1W4^p*r zGktTpD3VQ9dAHd#Xxr18^QiT`$gI>Hx3>}v;M!w z`v01BmY=x)-^ubK=batMTQaA_*yxlbhqJshnXH`ryf_7ncbW1JxZTkx*J*CQwbq;z1bgBO!<5K)Dm0oeA!p#NE|&v+=`cqY5RnvuAK^ z>02elHN0F!9UQ%?&$`kf%pfK82cjs4`lS(zx8nC`A*56U0W5Llqv{>9!{h zE5LUP>7t2->aslcGaM$xk3W6v#Q;lj9x-j48CUc?GUK{VT|^--$5dZpn`GzZ zi&}-Z8(xuQuYgU|9VIM;%|BOY?-<>;E37zux9ZYAHV)D`euu&(=cpM7onBUWrn($^ z%{pvYT4PLNCp(hmy@~KODm$^HCryBNyC2XIPFz1+(@kjZ$Lp&$#5_#4{W+I&c+Ja^ z)a+ND{5Dcxu`!J$1*`sMl;j!|TQpgRvtvHtgI PhW~w<;lDr4pzwbH*n3?? literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/admin_font_select.js b/app/assets/javascripts/admin_font_select.js new file mode 100644 index 0000000000..7fa0651089 --- /dev/null +++ b/app/assets/javascripts/admin_font_select.js @@ -0,0 +1,6 @@ +Blacklight.onLoad(function() { + if($("#admin_appearance_body_font").length > 0){ + $("#admin_appearance_body_font").fontselect({lookahead: 20}); + $("#admin_appearance_headline_font").fontselect({lookahead: 20}); + } +}); diff --git a/app/controllers/api/sushi_controller.rb b/app/controllers/api/sushi_controller.rb new file mode 100644 index 0000000000..392aceff82 --- /dev/null +++ b/app/controllers/api/sushi_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module API + class SushiController < ApplicationController + private + + ## + # We have encountered an error in their request, this might be an invalid date or a missing + # required parameter. The end user can adjust the request and try again. + # + # @param [Exception] + def render_sushi_exception(error) + render json: error, status: 422 + end + rescue_from Sushi::Error::Exception, with: :render_sushi_exception + + public + + ## + # needs to include the following filters: begin & end date, item ID + # + # + # When given an item_id parameter, filter the results to only that item_id. + # + # @note We should not need to find the record in ActiveFedora; hopefully we have all we need in + # the stats database. + def item_report + @report = Sushi::ItemReport.new(params, account: current_account) + render json: @report + end + + def platform_report + @report = Sushi::PlatformReport.new(params, account: current_account) + render json: @report + end + + def platform_usage + @report = Sushi::PlatformUsageReport.new(params, account: current_account) + render json: @report + end + + def server_status + @status = Sushi::ServerStatus.new(account: current_account).server_status + render json: @status + end + + def report_list + @report = Sushi::ReportList.new + render json: @report + end + end +end diff --git a/app/controllers/concerns/hyrax/collections_controller_behavior_decorator.rb b/app/controllers/concerns/hyrax/collections_controller_behavior_decorator.rb new file mode 100644 index 0000000000..4faa11c552 --- /dev/null +++ b/app/controllers/concerns/hyrax/collections_controller_behavior_decorator.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0 to sort subcollections by title + +module Hyrax + module CollectionsControllerBehaviorDecorator + def load_member_subcollections + super + return if @subcollection_docs.blank? + + @subcollection_docs.sort_by! { |doc| doc.title.first&.downcase } + end + + def show + params[:sort] ||= Hyrax::Collections::CollectionMemberSearchServiceDecorator::DEFAULT_SORT_FIELD + + super + end + end +end + +Hyrax::CollectionsControllerBehavior.prepend Hyrax::CollectionsControllerBehaviorDecorator diff --git a/app/controllers/hyrax/admin/workflows_controller_decorator.rb b/app/controllers/hyrax/admin/workflows_controller_decorator.rb new file mode 100644 index 0000000000..b1a44f0cc8 --- /dev/null +++ b/app/controllers/hyrax/admin/workflows_controller_decorator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax 3.6.0 to redirect back to the review submissions page after approving or rejecting a work +# when the user came from the review submissions page + +module Hyrax + module Admin + module WorkflowsControllerDecorator + def index + super + session[:from_admin_workflows] = true if request.fullpath.include?(admin_workflows_path) + end + end + end +end + +Hyrax::Admin::WorkflowsController.prepend(Hyrax::Admin::WorkflowsControllerDecorator) diff --git a/app/controllers/hyrax/my/works_controller_decorator.rb b/app/controllers/hyrax/my/works_controller_decorator.rb new file mode 100644 index 0000000000..a51885fc95 --- /dev/null +++ b/app/controllers/hyrax/my/works_controller_decorator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax 5.0.0 to add custom sort fields while in the dashboard for works + +module Hyrax + module My + module WorksControllerDecorator + def configure_facets + configure_blacklight do |config| + # clear facets copied from the CatalogController + config.sort_fields.clear + config.add_sort_field "date_uploaded_dtsi desc", label: "date uploaded \u25BC" + config.add_sort_field "date_uploaded_dtsi asc", label: "date uploaded \u25B2" + config.add_sort_field "date_modified_dtsi desc", label: "date modified \u25BC" + config.add_sort_field "date_modified_dtsi asc", label: "date modified \u25B2" + config.add_sort_field "system_create_dtsi desc", label: "date created \u25BC" + config.add_sort_field "system_create_dtsi asc", label: "date created \u25B2" + config.add_sort_field "depositor_ssi asc, title_ssi asc", label: "depositor (A-Z)" + config.add_sort_field "depositor_ssi desc, title_ssi desc", label: "depositor (Z-A)" + config.add_sort_field "creator_ssi asc, title_ssi asc", label: "creator (A-Z)" + config.add_sort_field "creator_ssi desc, title_ssi desc", label: "creator (Z-A)" + end + end + end + end +end + +Hyrax::My::WorksController.singleton_class.send(:prepend, Hyrax::My::WorksControllerDecorator) +Hyrax::My::WorksController.configure_facets diff --git a/app/controllers/hyrax/workflow_actions_controller_decorator.rb b/app/controllers/hyrax/workflow_actions_controller_decorator.rb new file mode 100644 index 0000000000..3ec77e6f5b --- /dev/null +++ b/app/controllers/hyrax/workflow_actions_controller_decorator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax 5.0.0 to redirect back to the review submissions page after approving or rejecting a work +# when the user came from the review submissions page + +module Hyrax + module WorkflowActionsControllerDecorator + private + + def after_update_response + respond_to do |wants| + redirect_path = session[:from_admin_workflows] ? admin_workflows_path : [main_app, curation_concern] + wants.html { redirect_to redirect_path, notice: "The #{curation_concern.class.human_readable_type} has been updated." } + wants.json { render 'hyrax/base/show', status: :ok, location: polymorphic_path([main_app, curation_concern]) } + end + end + end +end + +Hyrax::WorkflowActionsController.prepend(Hyrax::WorkflowActionsControllerDecorator) diff --git a/app/jobs/create_derivatives_job_decorator.rb b/app/jobs/create_derivatives_job_decorator.rb new file mode 100644 index 0000000000..88d66a3ec8 --- /dev/null +++ b/app/jobs/create_derivatives_job_decorator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v3.6.0 +# @see CreateLargeDerivativesJob +module CreateDerivativesJobDecorator + # OVERRIDE: Divert audio and video derivative + # creation to CreateLargeDerivativesJob. + def perform(file_set, file_id, filepath = nil) + return super if is_a?(CreateLargeDerivativesJob) + return super unless file_set.video? || file_set.audio? + + CreateLargeDerivativesJob.perform_later(*arguments) + true + end +end + +CreateDerivativesJob.prepend(CreateDerivativesJobDecorator) diff --git a/app/jobs/create_large_derivatives_job.rb b/app/jobs/create_large_derivatives_job.rb new file mode 100644 index 0000000000..c068d51520 --- /dev/null +++ b/app/jobs/create_large_derivatives_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# CreateLargeDerivativesJob is intended to be used for resource-intensive derivative +# generation (e.g. video processing). It is functionally similar to CreateDerivativesJob, +# except that it queues jobs in the :auxiliary queue. +# +# The worker responsible for processing jobs in the :auxiliary queue should be +# configured to have more resources dedicated to it, especially CPU. Otherwise, the +# `ffmpeg` commands that this job class eventually triggers could be throttled. +# +# @see CreateDerivativesJobDecorator +# @see Hydra::Derivatives::Processors::Ffmpeg +# @see https://github.com/scientist-softserv/palni-palci/issues/852 +class CreateLargeDerivativesJob < CreateDerivativesJob + queue_as :auxiliary +end diff --git a/app/jobs/file_set_index_job.rb b/app/jobs/file_set_index_job.rb new file mode 100644 index 0000000000..f8ba69ae7f --- /dev/null +++ b/app/jobs/file_set_index_job.rb @@ -0,0 +1,5 @@ +class FileSetIndexJob < Hyrax::ApplicationJob + def perform(file_set) + file_set&.update_index + end +end diff --git a/app/jobs/prune_stale_guest_users_job.rb b/app/jobs/prune_stale_guest_users_job.rb new file mode 100644 index 0000000000..ce47c4df22 --- /dev/null +++ b/app/jobs/prune_stale_guest_users_job.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class PruneStaleGuestUsersJob < ApplicationJob + non_tenant_job + repeat 'every week at 8am' # midnight PST + + def perform + RolesService.prune_stale_guest_users + end +end diff --git a/app/jobs/work_index_job.rb b/app/jobs/work_index_job.rb new file mode 100644 index 0000000000..95710c4239 --- /dev/null +++ b/app/jobs/work_index_job.rb @@ -0,0 +1,5 @@ +class WorkIndexJob < Hyrax::ApplicationJob + def perform(work) + work.update_index + end +end diff --git a/app/models/file_download_stat_decorator.rb b/app/models/file_download_stat_decorator.rb new file mode 100644 index 0000000000..b545d90d28 --- /dev/null +++ b/app/models/file_download_stat_decorator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax hyrax-v3.5.0 to require Hyrax::Download so the method below doesn't fail + +Hyrax::Download # rubocop:disable Lint/Void + +module FileDownloadStatClass + # Hyrax::Download is sent to Hyrax::Analytics.profile as #hyrax__download + # see Legato::ProfileMethods.method_name_from_klass + def ga_statistics(start_date, file) + profile = Hyrax::Analytics.profile + unless profile + Hyrax.logger.error("Google Analytics profile has not been established. Unable to fetch statistics.") + return [] + end + # OVERRIDE Hyrax hyrax-v3.5.0 + profile.hyrax__download(sort: 'date', + start_date: start_date, + end_date: Date.yesterday, + limit: 10_000) + .for_file(file.id) + end +end + +FileDownloadStat.singleton_class.send(:prepend, FileDownloadStatClass) diff --git a/app/models/sushi.rb b/app/models/sushi.rb new file mode 100644 index 0000000000..23b8476245 --- /dev/null +++ b/app/models/sushi.rb @@ -0,0 +1,497 @@ +# frozen_string_literal:true + +require 'sushi/error' + +module Sushi + mattr_accessor :info + + class << self + ## + # @param dates [Array] + # @raise [Sushi::InvalidParameterValue] when either of the given dates are not formatted correctly. + # + # @return [TrueClass] + # + # @note This is necessary because calling `to_date` on a string in 'MM-YYYY' format will assume that 'MM' is the day, + # and will set the current month as 'MM' instead. + # + # params.fetch(:begin_date) => "06-2023" + # params.fetch(:begin_date).to_date => Wed, 06 Sep 2023 + def validate_date_format(dates = []) + dates.each do |date| + match = date.match(/(^\d{4}-\d{2}$)|(^\d{4}-\d{2}-\d{2}$)/) + raise Sushi::Error::InvalidDateArgumentError.new(data: "The given date of \"#{date}\" is invalid. Please provide a date in YYYY-MM format") unless match + + # rubocop:disable Metrics/LineLength + info << Sushi::Info.new(data: date.to_s, message: "The day of the month is not taken into consideration when providing metrics. The date provided was amended to account for the full month.").as_json if match[2] + # rubocop:enable Metrics/LineLength + end + + true + end + + ## + # @param value [String, #to_date] + # + # @return [Date] + # @raise [Sushi::InvalidParameterValue] when we cannot coerce to a date. + def coerce_to_date(value) + value.to_date + rescue StandardError + begin + # We can't parse the original date, so lets attempt to coerce a "YYYY-MM" string (year-month). + # If it fails, we'll raise an exception on garbage input. + year, month, = value.split('-') + + # We want to set the date to the 1st of the month. + Date.new(year.to_i, month.to_i, 1) + rescue StandardError + raise Sushi::Error::InvalidDateArgumentError.new(data: "Unable to convert \"#{value}\" to a date.") + end + end + + ## + # The first day of the month of the earliest entry in {Hyrax::CounterMetric}, with some caveats. + # + # Namely if we only have one month of data, and that month happens to be the current month. + # + # @param current_date [Date] included as a dependency injection to ease testing. + # + # @return [Date] when we have data in the system + # @raise [Sushi::Error::UsageNotReadyForRequestedDatesError] when we don't have data in the + # system OR we only have data for the current month. + # + # @see {.last_month_available} + # + # @note Ultimately, the goal of these date ranges is for us to inform the consumer of the API + # about the complete months of data that we have. + def first_month_available(current_date: Time.zone.today) + raise Sushi::Error::UsageNotReadyForRequestedDatesError.new(data: "There is no available metrics data available at this time.") unless Hyrax::CounterMetric.any? + + # If, for some reason, we have only partial data for the earliest month, we'll assume that + # we have "all" that month's data. + earliest_entry_beginning_of_month_date = Hyrax::CounterMetric.order('date ASC').first.date.beginning_of_month + + beginning_of_month = current_date.beginning_of_month + + # In this case, the only data we have is data in the current month and since the current month + # isn't over we should not be reporting. + if earliest_entry_beginning_of_month_date == beginning_of_month + raise Sushi::Error::UsageNotReadyForRequestedDatesError.new(data: "The only data available is for #{beginning_of_month.strftime('%Y-%m')}; a month that is not yet over.") + end + + earliest_entry_beginning_of_month_date + end + + ## + # @see .first_month_available + # + # @return [String] when there is a valid first month, return the YYYY-MM format of that month. + # @return [NilClass] when there is not a valid first month + def rescued_first_month_available(*args) + first_month_available(*args).strftime("%Y-%m") + rescue Sushi::Error::UsageNotReadyForRequestedDatesError + nil + end + + ## + # The earlier of: + # + # - the last day of the prior month + # - the last day of the month of the latest entry in {Hyrax::CounterMetric} + # + # @param current_date [Date] included as a dependency injection to ease testing. An assumption + # is that the current_date will always be on or after the last {Hyrax::CounterMetric} + # entry's date. + # + # @return [Date] when we have data in the system + # @raise [Sushi::Error::UsageNotReadyForRequestedDatesError] when we don't have data in the + # system + # + # @see {.first_month_available} + # + # @note Ultimately, the goal of these date ranges is for us to inform the consumer of the API + # about the complete months of data that we have. + def last_month_available(current_date: Time.zone.today) + # This might raise an exception + first_month_available(current_date: current_date) + + # We're assuming that we have whole months, so we'll nudge the latest date towards that + # assumption. + latest_entry_end_of_month_date = Hyrax::CounterMetric.order('date DESC').first.date.end_of_month + + # We want to avoid partial months, we look at the month prior to the current_date + end_of_last_month = 1.month.ago(current_date).end_of_month + + return latest_entry_end_of_month_date if latest_entry_end_of_month_date < end_of_last_month + + end_of_last_month + end + + ## + # @see .last_month_available + # + # @return [String] when there is a valid last month, return the YYYY-MM format of that month. + # @return [NilClass] when there is not a valid last month + def rescued_last_month_available(*args) + last_month_available(*args).strftime("%Y-%m") + rescue Sushi::Error::UsageNotReadyForRequestedDatesError + nil + end + end + + ## + # This module provides a common interface for validating parameters. + # + # @param params [Hash, ActionController::Parameters] + # @param allowed_parameters [Array] a list of query parameters AND report filters that are allowed. + # + # @return [String] + # @raise [Sushi::Error::UnrecognizedParameterError] when any unrecognized params are given. + module ParameterValidation + def validate_paramaters(params = {}, allowed_parameters: []) + filtered_params = params.keys.map(&:to_s) - ['action', 'controller', 'format'] + return true if (filtered_params & allowed_parameters).length == filtered_params.length + + raise Sushi::Error::UnrecognizedParameterError.new(data: "The given parameter(s) are invalid: #{(filtered_params - allowed_parameters).join(', ')}.") + end + end + + ## + # This module accounts for date behavior that needs to be used across all reports. + # + # @see #coerce_dates + module DateCoercion + extend ActiveSupport::Concern + included do + attr_reader :begin_date + attr_reader :end_date + end + + ## + # @param params [Hash, ActionController::Parameters] + # @option params [String, NilClass] begin_date :: Either nil, YYYY-MM or YYYY-MM-DD format + # @option params [String, NilClass] end_date :: Either nil, YYYY-MM or YYYY-MM-DD format + # + # @raise [Sushi::Error::InvalidDateArgumentError] when begin date is after end date + # @raise [Sushi::Error::UsageNoLongerAvailableForRequestedDatesError] when given begin date + # (coerced to beginning of month) is before earliest reporting date. + # @raise [Sushi::Error::UsageNoLongerAvailableForRequestedDatesError] when given end date + # (coerced to end of month) is after latest reporting date. + # @raise [Sushi::Error::InsufficientInformationToProcessRequestError] when either end date or + # begin date is not given. + def coerce_dates(params = {}) + # TODO: We should also be considering available dates as well. + + Sushi.validate_date_format([params.fetch(:begin_date), params.fetch(:end_date)]) + + # Because we're receiving user input that is likely strings, we need to do some coercion. + @begin_date = Sushi.coerce_to_date(params.fetch(:begin_date)).beginning_of_month + @end_date = Sushi.coerce_to_date(params.fetch(:end_date)).end_of_month + + raise Sushi::Error::InvalidDateArgumentError.new(data: "Begin date #{params.fetch(:begin_date)} is after end date #{params.fetch(:end_date)}.") if @begin_date > @end_date + + earliest_date = Hyrax::CounterMetric.order(date: :asc).first.date + if @begin_date < earliest_date + # rubocop:disable Metrics/LineLength + raise Sushi::Error::UsageNoLongerAvailableForRequestedDatesError.new(data: "Unable to complete the request because the begin_date of #{params[:begin_date]} is for a month that has incomplete data. That month's data starts on #{earliest_date.iso8601}.") + # rubocop:enable Metrics/LineLength + end + + latest_date = Hyrax::CounterMetric.order(date: :desc).first.date + if @end_date > latest_date + # rubocop:disable Metrics/LineLength + raise Sushi::Error::UsageNotReadyForRequestedDatesError.new(data: "Unable to complete the request because the end_date of #{params[:end_date]} is for a month that has incomplete data. That month's data ends on #{latest_date.iso8601}.") + # rubocop:enable Metrics/LineLength + end + rescue ActionController::ParameterMissing, KeyError => e + raise Sushi::Error::InsufficientInformationToProcessRequestError.new(data: e.message) + end + end + + ## + # This module provides coercion of the :data_type parameter + # + # @see #coerce_data_types + module DataTypeCoercion + extend ActiveSupport::Concern + included do + attr_reader :data_types, :data_type_in_params + end + + ## + # @param params [Hash, ActionController::Parameters] + # @option params [String, NilClass] data_type :: Pipe separated string of one or more data_types. + def coerce_data_types(params = {}) + @data_type_in_params = params.key?(:data_type) + @data_types = Array.wrap(params[:data_type]&.split('|')).map { |dt| dt.strip.downcase } + end + end + + module MetricTypeCoercion + extend ActiveSupport::Concern + included do + attr_reader :metric_types, :metric_type_in_params + end + + def coerce_metric_types(params = {}, allowed_types: ALLOWED_METRIC_TYPES) + metric_types_from_params = Array.wrap(params[:metric_type]&.split('|')) + return @metric_types = allowed_types if metric_types_from_params.empty? + + @metric_type_in_params = metric_types_from_params.any? do |metric_type| + normalized_metric_type = metric_type.downcase + allowed_types.any? { |allowed_type| allowed_type.downcase == normalized_metric_type } + end + + unless metric_type_in_params + raise Sushi::Error::InvalidReportFilterValueError.given_value_does_not_match_allowed_values( + parameter_value: params[:metric_type], + parameter_name: :metric_type, + allowed_values: allowed_types + ) + end + + @metric_types = metric_types_from_params.map do |metric_type| + normalized_metric_type = metric_type.downcase + metric_type.titleize.tr(' ', '_') if allowed_types.any? { |allowed_type| allowed_type.downcase == normalized_metric_type } + end.compact + end + end + + module AccessMethodCoercion + extend ActiveSupport::Concern + included do + attr_reader :access_methods, :access_method_in_params + end + + ALLOWED_ACCESS_METHODS = ['regular'].freeze + + ## + # @param params [Hash, ActionController::Parameters] + # + # @return [Array] + # @raise [Sushi::InvalidParameterValue] when the access method is invalid. + def coerce_access_method(params = {}) + return true unless params.key?(:access_method) + allowed_access_methods_from_params = Array.wrap(params[:access_method].split('|')).map { |am| am.strip.downcase } & ALLOWED_ACCESS_METHODS + + unless allowed_access_methods_from_params.any? + raise Sushi::Error::InvalidReportFilterValueError.given_value_does_not_match_allowed_values( + parameter_value: params[:access_method], + parameter_name: :access_method, + allowed_values: ALLOWED_ACCESS_METHODS + ) + end + + @access_methods = allowed_access_methods_from_params + @access_method_in_params = true + end + end + + module ItemIDCoercion + extend ActiveSupport::Concern + included do + attr_reader :item_id, :item_id_in_params + end + + ## + # @param params [Hash, ActionController::Parameters] + # + # @return [String] + # @raise [Sushi::Error::NotFoundError] when the item id has no metrics. + def coerce_item_id(params = {}) + return true unless params.key?(:item_id) + + # rubocop:disable Metrics/LineLength + raise Sushi::Error::InvalidReportFilterValueError.new(data: "The given parameter `item_id=#{params[:item_id]}` does not exist. Please provide an existing item_id, or none at all.") unless Hyrax::CounterMetric.exists?(work_id: params[:item_id]) + # rubocop:enable Metrics/LineLength + + @item_id = params[:item_id] + @item_id_in_params = true + end + end + + module PlatformCoercion + extend ActiveSupport::Concern + included do + attr_reader :platform, :platform_in_params + end + + ## + # @param params [Hash, ActionController::Parameters] + # @param account [Account] + # + # @return [String] + # @raise [Sushi::InvalidParameterValue] when the platform is invalid. + def coerce_platform(params = {}, account = nil) + return true unless params.key?(:platform) + + unless params[:platform] == account.cname + raise Sushi::Error::InvalidReportFilterValueError.given_value_does_not_match_allowed_values( + parameter_value: params[:platform], + parameter_name: :platform, + allowed_values: [account.cname] + ) + end + + @platform = account.cname + @platform_in_params = true + end + end + + # This param specifies the granularity of the usage data to include in the report. + # Permissible values are Month (default) and Totals. + # For Totals, each Item_Performance element represents the aggregated usage for the reporting period. + # See https://cop5.projectcounter.org/en/5.1/03-specifications/03-counter-report-common-attributes-and-elements.html#report-filters-and-report-attributes for details + module GranularityCoercion + extend ActiveSupport::Concern + included do + attr_reader :granularity, :granularity_in_params + end + ALLOWED_GRANULARITY = ["Month", "Totals"].freeze + + def coerce_granularity(params = {}) + return true unless params.key?(:granularity) + @granularity_in_params = ALLOWED_GRANULARITY.include?(params[:granularity].to_s.capitalize) + + unless @granularity_in_params + raise Sushi::Error::InvalidReportFilterValueError.given_value_does_not_match_allowed_values( + parameter_value: params[:granularity], + parameter_name: :granularity, + allowed_values: ALLOWED_GRANULARITY + ) + end + + @granularity = params.fetch(:granularity).capitalize + end + end + + module AuthorCoercion + extend ActiveSupport::Concern + included do + attr_reader :author, :author_in_params + end + + ## + # Because we have the potential for multiple authors for a work, we want to separate those + # authors in the single field. We've chosen the pipe (e.g. `|`) as that delimiter. + DELIMITER = "|" + + ## + # Deserialize the Ruby array into a serialize author format. + # + # @param string [String] + # @param delimiter [String] + # + # @see .serialize + def self.deserialize(string, delimiter: DELIMITER) + string.to_s.split(delimiter).select(&:present?) + end + + ## + # Convert the Ruby array into a serialize author format. + # + # @param array [Array] + # @param delimiter [String] + # + # @see .deserialize + def self.serialize(array, delimiter: DELIMITER) + array = Array.wrap(array) + return nil if array.empty? + + "#{delimiter}#{array.join(delimiter)}#{delimiter}" + end + + ## + # Ensure the given param is valid. + # + # @param params [Hash, ActionController::Parameters] + # @option params [String] :author the named parameter we'll use to filter the report items by. + # + # @note: The sushi spec states that this value must be >=2 characters. We're only enforcing that the value exactly + # matches whatever data we have in the database. Which presumably is >=2 characters, but may not be. + # + # @see #author_as_where_parameters + def coerce_author(params = {}) + return true unless params.key?(:author) + @author = params[:author] + + # See https://github.com/scientist-softserv/palni-palci/issues/721#issuecomment-1734215004 for details of this little nuance + raise Sushi::Error::InvalidReportFilterValueError.new(data: "You may not query for multiple authors (as specified by the `#{DELIMITER}' delimiter.)") if @author.include?(DELIMITER) + + # rubocop:disable Metrics/LineLength + raise Sushi::Error::InvalidReportFilterValueError.new(data: "The given author #{author.inspect} was not found in the metrics.") unless Hyrax::CounterMetric.where(author_as_where_parameters).exists? + # rubocop:enable Metrics/LineLength + + @author_in_params = true + end + + ## + # @return [Array] The {ActiveRecord::Base#where} can take an array of parameters, using + # those to build the SQL statement. The returned values is conformant to that method's + # interface. + # + # @see .serialize + # @see .deserialize + def author_as_where_parameters + # NOTE: I've included both the serialized handler and the non-serialized version; that way + # when we send this code change up, we don't also need to perform a migration. + # + # TODO: Remove the "OR author = ?" and the 3rd element of the array once we've run the imports + # with the latest changes brought about by the commit that introduced this comment. + ["author LIKE ? OR author = ?", "%#{DELIMITER}#{author}#{DELIMITER}%", author] + end + end + + module YearOfPublicationCoercion + extend ActiveSupport::Concern + included do + attr_reader :yop_as_where_parameters, :yop + end + + DATE_RANGE_REGEXP = /^(\d+)\s*-\s*(\d+)$/ + + ## + # Convert the given params to parameters suitable for {ActiveRecord::Base.where} calls. + # + # @param params [Hash] + # @option params [String] :yop the named parameter from which we'll extract integers. + # + # @return [Array] when all of the parts are valid, we'll have an array with the + # first element being a string (that has position "?"s for SQL query building) and the + # remaining elements being integers. + # @raise [ArgumentError] when part of the given YOP could not be coerced to an integer. + # + # @note No special consideration is made for date ranges that start with a later date and end with + # an earlier date (e.g. "1999-1994" will be "date >= 1999 AND date <= 1994"; which will + # return no entries.) + def coerce_yop(params = {}) + return unless params.key?(:yop) + + # TODO: We might want to quote the column name and add the table name as well; this helps with + # any potential field name collisions while we assemble the SQL statement. + field_name = 'year_of_publication' + where_clauses = [] + where_values = [] + + params[:yop].split(/\s*\|\s*/).flat_map do |slug| + slug = slug.strip + match = DATE_RANGE_REGEXP.match(slug) + if match + where_clauses << "(#{field_name} >= ? AND #{field_name} <= ?)" + where_values << Integer(match[1]) + where_values << Integer(match[2]) + else + where_clauses << "(#{field_name} = ?)" + where_values << Integer(slug) + end + end + @yop = params[:yop] + @yop_as_where_parameters = ["(#{where_clauses.join(' OR ')})"] + where_values + rescue ArgumentError + # rubocop:disable Metrics/LineLength + raise Sushi::Error::InvalidDateArgumentError.new(data: "The given parameter `yop=#{yop}` was malformed. You can provide a range (e.g. 'YYYY-YYYY') or a single date (e.g. 'YYYY'). You can separate ranges/values with a '|'.") + # rubocop:enable Metrics/LineLength + end + end +end diff --git a/app/models/sushi/error.rb b/app/models/sushi/error.rb new file mode 100644 index 0000000000..2ec9689d48 --- /dev/null +++ b/app/models/sushi/error.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +module Sushi + ## + # A name space for the various errors that we have. + # + # @note I'd love for this to be Sushi::Error::Errors, but Rails autoload might have other opinions. + module Error + ## + # @see https://countermetrics.stoplight.io/docs/counter-sushi-api/sgmqulr0emsi6-exception + # + # @see .as_json + class Exception < StandardError + class_attribute :code, default: nil + class_attribute :help_url, default: nil + class_attribute :default_message, default: nil + + attr_reader :data + def initialize(data: nil) + @data = data + super("#{default_message}: #{data}") + end + + ## + # In Sushi's exception documentation there are two required properties: + # + # - Code :: an integer + # - Message :: the Message value is a terse string that "identifies" the conceptual + # + # The Data property is the further explanation of the Message. And the Help_URL property + # is a URL that explains the exception. By convention, we're setting the Help_URL to the + # Sushi documentation; this helps developers read more about what the details of the + # exceptions; and provides the end-user with further details about the Sushi "specification". + # + # @see https://countermetrics.stoplight.io/docs/counter-sushi-api/sgmqulr0emsi6-exception + def as_json(*) + hash = { + Code: code, + Message: default_message + } + + hash[:Data] = data if data.present? + hash[:Help_URL] = help_url if help_url.present? + hash + end + end + + class InsufficientInformationToProcessRequestError < Sushi::Error::Exception + self.code = 1030 + self.help_url = "https://countermetrics.stoplight.io/docs/counter-sushi-api/sgise2c2tmbrq-exception-1030" + self.default_message = "Insufficient Information to Process Request" + end + + class InvalidDateArgumentError < Sushi::Error::Exception + self.code = 3020 + self.help_url = "https://countermetrics.stoplight.io/docs/counter-sushi-api/3anarphof7zh5-exception-3020" + self.default_message = "Invalid Date Arguments" + end + + class NoUsageAvailableForRequestedDatesError < Sushi::Error::Exception + self.code = 3030 + self.help_url = "https://countermetrics.stoplight.io/docs/counter-sushi-api/2fhcc5gkzzkg3-exception-3030" + self.default_message = "No Usage Available for Requested Dates" + end + + # @todo This is likely a scenario we'll need to handle. + class UsageNotReadyForRequestedDatesError < Sushi::Error::Exception + self.code = 3031 + self.help_url = "https://countermetrics.stoplight.io/docs/counter-sushi-api/6lfhilgndgpde-exception-3031" + self.default_message = "Usage Not Ready for Requested Dates" + end + + # @todo This is likely a scenario we'll need to handle. + class UsageNoLongerAvailableForRequestedDatesError < Sushi::Error::Exception + self.code = 3032 + self.help_url = "https://countermetrics.stoplight.io/docs/counter-sushi-api/x2o44d5p9kdp3-exception-3032" + self.default_message = "Usage No Longer Available for Requested Dates" + end + + class UnrecognizedParameterError < Sushi::Error::Exception + self.code = 3050 + self.help_url = "https://countermetrics.stoplight.io/docs/counter-sushi-api/mvg1sf79k34ni-exception-3050" + self.default_message = "Parameter Not Recognized in this Context" + end + + class InvalidReportFilterValueError < Sushi::Error::Exception + self.code = 3060 + self.help_url = "https://countermetrics.stoplight.io/docs/counter-sushi-api/s2a2xb68jsnn6-exception-3060" + self.default_message = "Invalid Report Filter Value" + + ## + # @param parameter_value [#to_s] + # @param parameter_name [#to_s] + # @param allowed_values [Array<#to_s>] + def self.given_value_does_not_match_allowed_values(parameter_value:, parameter_name:, allowed_values:) + new(data: "None of the given values in `#{parameter_name}=#{parameter_value}` are supported at this time. " \ + "Please use an acceptable value, (#{allowed_values.join(', ')}) instead. " \ + "(Or do not pass the parameter at all, which will default to the acceptable value(s))") + end + end + end + + ## + # The Info exception differs from the Error exception in that it is returned in the Report_Header. + # The message nor data are standarized, and therefore are being required to be passed in. + class Info + attr_reader :data, :message + + def initialize(data:, message:) + @data = data + @message = message + end + + def as_json(*) + { + Code: 0, + Message: message, + Help_URL: "https://countermetrics.stoplight.io/docs/counter-sushi-api/jmelferytrixm-exception-0", + Data: data + } + end + end +end diff --git a/app/models/sushi/item_report.rb b/app/models/sushi/item_report.rb new file mode 100644 index 0000000000..987e3375e4 --- /dev/null +++ b/app/models/sushi/item_report.rb @@ -0,0 +1,181 @@ +# frozen_string_literal:true + +# counter compliant format for ItemReport: https://countermetrics.stoplight.io/docs/counter-sushi-api/5a6e9f5ddae3e-ir-item-report +# +# dates will be filtered where both begin & end dates are inclusive. +# any provided begin_date will be moved to the beginning of the month +# any provided end_date will be moved to the end of the month +module Sushi + class ItemReport + attr_reader :account, :attributes_to_show, :created + include Sushi + include Sushi::AccessMethodCoercion + include Sushi::AuthorCoercion + include Sushi::DateCoercion + include Sushi::DataTypeCoercion + include Sushi::DateCoercion + include Sushi::ItemIDCoercion + include Sushi::MetricTypeCoercion + include Sushi::ParameterValidation + include Sushi::PlatformCoercion + include Sushi::YearOfPublicationCoercion + + ALLOWED_REPORT_ATTRIBUTES_TO_SHOW = [ + 'Access_Method', + # These are all the counter compliant query attributes, they are not currently supported in this implementation. + # 'Institution_Name', + # 'Customer_ID', + # 'Country_Name', + # 'Country_Code', + # 'Subdivision_Name', + # 'Subdivision_Code', + # 'Attributed' + ].freeze + + ALLOWED_METRIC_TYPES = [ + 'Total_Item_Investigations', + 'Total_Item_Requests', + 'Unique_Item_Investigations', + 'Unique_Item_Requests' + ].freeze + + ALLOWED_PARAMETERS = [ + 'access_method', + 'access_type', + 'api_key', + 'attributed', + 'attributes_to_show', + 'author', + 'begin_date', + 'country_code', + 'customer_id', + 'data_type', + 'end_date', + 'granularity', + 'include_component_details', + 'include_parent_details', + 'item_id', + 'metric_type', + 'platform', + 'requestor_id', + 'subdivision_code', + 'yop' + ].freeze + + def initialize(params = {}, created: Time.zone.now, account:) + Sushi.info = [] + validate_paramaters(params, allowed_parameters: ALLOWED_PARAMETERS) + coerce_access_method(params) + coerce_author(params) + coerce_data_types(params) + coerce_dates(params) + coerce_item_id(params) + coerce_metric_types(params, allowed_types: ALLOWED_METRIC_TYPES) + coerce_platform(params, account) + coerce_yop(params) + @account = account + @created = created + + # We want to limit the available attributes to be a subset of the given attributes; the `&` is + # the intersection of the two arrays. + @attributes_to_show = params.fetch(:attributes_to_show, ['Access_Method']) & ALLOWED_REPORT_ATTRIBUTES_TO_SHOW + end + + def as_json(_options = {}) + report_hash = { + 'Report_Header' => { + 'Report_Name' => 'Item Report', + 'Report_ID' => 'IR', + 'Release' => '5.1', + 'Institution_Name' => account.institution_name, + 'Institution_ID' => account.institution_id_data, + 'Report_Filters' => { + 'Begin_Date' => begin_date.iso8601, + 'End_Date' => end_date.iso8601 + }, + 'Created' => created.rfc3339, # '2023-02-15T09:11:12Z' + 'Created_By' => account.institution_name, + 'Registry_Record' => '', + 'Report_Attributes' => { + 'Attributes_To_Show' => attributes_to_show + } + }, + 'Report_Items' => report_items + } + + raise Sushi::Error::NoUsageAvailableForRequestedDatesError.new(data: "No data within #{begin_date.iso8601} and #{end_date.iso8601} date range.") if report_items.blank? + + report_hash['Report_Header']['Report_Filters']['Access_Method'] = access_methods if access_method_in_params + report_hash['Report_Header']['Report_Filters']['Author'] = author if author_in_params + report_hash['Report_Header']['Report_Filters']['Data_Type'] = data_types if data_type_in_params + report_hash['Report_Header']['Report_Filters']['Item_ID'] = item_id if item_id_in_params + report_hash['Report_Header']['Report_Filters']['Metric_Type'] = metric_types if metric_type_in_params + report_hash['Report_Header']['Report_Filters']['Platform'] = platform if platform_in_params + report_hash['Report_Header']['Report_Filters']['YOP'] = yop if yop + report_hash["Report_Header"]["Exceptions"] = info if info.present? + + report_hash + end + + alias to_hash as_json + + def report_items + data_for_resource_types.map do |record| + { + 'Items' => [{ + 'Attribute_Performance' => [{ + 'Data_Type' => record.resource_type.titleize, + # We are only supporting the 'Regular' access method at this time. If that changes, we will need to update this. + 'Access_Method' => 'Regular', + 'Performance' => performance(record) + }], + 'Authors' => Sushi::AuthorCoercion.deserialize(record.author).map { |author| { 'Name:' => author } }, + 'Item' => record.work_id.to_s, + 'Publisher' => '', + 'Platform' => account.cname, + 'Item_ID' => { + 'Proprietary': record.work_id.to_s, + 'URI': "https://#{account.cname}/concern/#{record.worktype.underscore}s/#{record.work_id}" + } + }] + } + end + end + + def performance(record) + metric_types.each_with_object({}) do |metric_type, returning_hash| + returning_hash[metric_type] = record.performance.each_with_object({}) do |cell, hash| + hash[cell.fetch('year_month')] = cell.fetch(metric_type) + end + end + end + + def data_for_resource_types + relation = Hyrax::CounterMetric + .select(:work_id, :resource_type, :worktype, :author, + %((SELECT To_json(Array_agg(Row_to_json(t))) + FROM + (SELECT + -- The AS field_name needs to be double quoted so as to preserve case structure. + SUM(total_item_investigations) as "Total_Item_Investigations", + SUM(total_item_requests) as "Total_Item_Requests", + COUNT(DISTINCT CASE WHEN total_item_investigations IS NOT NULL THEN CONCAT(work_id, '_', date::text) END) as "Unique_Item_Investigations", + COUNT(DISTINCT CASE WHEN total_item_requests IS NOT NULL THEN CONCAT(work_id, '_', date::text) END) as "Unique_Item_Requests", + -- We need to coerce the month from a single digit to two digits (e.g. August's "8" into "08") + CONCAT(DATE_PART('year', date_trunc('month', date)), '-', to_char(DATE_PART('month', date_trunc('month', date)), 'fm00')) AS year_month + FROM hyrax_counter_metrics AS aggr + WHERE #{Hyrax::CounterMetric.sanitize_sql_for_conditions(['aggr.work_id = hyrax_counter_metrics.work_id AND date >= ? AND date <= ?', begin_date, end_date])} + GROUP BY date_trunc('month', date)) t) as performance)) + .where("date >= ? AND date <= ?", begin_date, end_date) + .order(resource_type: :asc, work_id: :asc) + .group(:work_id, :resource_type, :worktype, :author) + + relation = relation.where("(?) = work_id", item_id) if item_id + relation = relation.where(author_as_where_parameters) if author.present? + relation = relation.where(yop_as_where_parameters) if yop_as_where_parameters.present? + relation = relation.where("LOWER(resource_type) IN (?)", data_types) if data_types.any? + + relation + end + end +end diff --git a/app/models/sushi/platform_report.rb b/app/models/sushi/platform_report.rb new file mode 100644 index 0000000000..0103035bdc --- /dev/null +++ b/app/models/sushi/platform_report.rb @@ -0,0 +1,221 @@ +# frozen_string_literal:true + +# counter compliant format for the PlatformReport is found here: https://countermetrics.stoplight.io/docs/counter-sushi-api/e98e9f5cab5ed-pr-platform-report +# +# dates will be filtered where both begin & end dates are inclusive. +# any provided begin_date will be moved to the beginning of the month +# any provided end_date will be moved to the end of the month +module Sushi + class PlatformReport + attr_reader :created, :account, :attributes_to_show + include Sushi + include Sushi::AccessMethodCoercion + include Sushi::DataTypeCoercion + include Sushi::DateCoercion + include Sushi::GranularityCoercion + include Sushi::MetricTypeCoercion + include Sushi::ParameterValidation + include Sushi::PlatformCoercion + + ALLOWED_REPORT_ATTRIBUTES_TO_SHOW = [ + "Access_Method", + # These are all the counter compliant query attributes, they are not currently supported in this implementation. + # "Institution_Name", + # "Customer_ID", + # "Country_Name", + # "Country_Code", + # "Subdivision_Name", + # "Subdivision_Code", + # "Attributed" + ].freeze + + ALLOWED_METRIC_TYPES = [ + "Searches_Platform", + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + # Unique_Title metrics exist to count how many chapters or sections are accessed for Book resource types in a given user session. + # This implementation currently does not support historical data from individual chapters/sections of Books, + # so these metrics will not be shown. + # See https://cop5.projectcounter.org/en/5.1/03-specifications/03-counter-report-common-attributes-and-elements.html#metric-types for details + # "Unique_Title_Investigations", + # "Unique_Title_Requests" + ].freeze + + ALLOWED_PARAMETERS = [ + 'access_method', + 'api_key', + 'attributed', + 'attributes_to_show', + 'author', + 'begin_date', + 'country_code', + 'customer_id', + 'data_type', + 'end_date', + 'granularity', + 'include_component_details', + 'include_parent_details', + 'item_id', + 'metric_type', + 'platform', + 'requestor_id', + 'subdivision_code', + 'yop' + ].freeze + + def initialize(params = {}, created: Time.zone.now, account:) + Sushi.info = [] + validate_paramaters(params, allowed_parameters: ALLOWED_PARAMETERS) + coerce_access_method(params) + coerce_data_types(params) + coerce_dates(params) + coerce_granularity(params) + coerce_metric_types(params, allowed_types: ALLOWED_METRIC_TYPES) + coerce_platform(params, account) + @created = created + @account = account + + # We want to limit the available attributes to be a subset of the given attributes; the `&` is + # the intersection of the two arrays. + @attributes_to_show = params.fetch(:attributes_to_show, ["Access_Method"]) & ALLOWED_REPORT_ATTRIBUTES_TO_SHOW + end + + def as_json(_options = {}) + report_hash = { + "Report_Header" => { + "Release" => "5.1", + "Report_ID" => "PR", + "Report_Name" => "Platform Report", + "Created" => created.rfc3339, # "2023-02-15T09:11:12Z" + "Created_By" => account.institution_name, + "Institution_ID" => account.institution_id_data, + "Institution_Name" => account.institution_name, + "Registry_Record" => "", + "Report_Attributes" => { + "Attributes_To_Show" => attributes_to_show + }, + "Report_Filters" => { + # TODO: handle YYYY-MM format + "Begin_Date" => begin_date.iso8601, + "End_Date" => end_date.iso8601 + } + }, + "Report_Items" => { + "Platform" => account.cname, + "Attribute_Performance" => attribute_performance_for_resource_types + attribute_performance_for_platform + } + } + report_hash["Report_Header"]["Report_Filters"]["Access_Method"] = access_methods if access_method_in_params + report_hash["Report_Header"]["Report_Filters"]["Data_Type"] = data_types if data_type_in_params + report_hash["Report_Header"]["Report_Attributes"]["Granularity"] = granularity if granularity_in_params + report_hash["Report_Header"]["Report_Filters"]["Metric_Type"] = metric_types if metric_type_in_params + report_hash["Report_Header"]["Report_Filters"]["Platform"] = platform if platform_in_params + report_hash["Report_Header"]["Exceptions"] = info if info.present? + + report_hash + end + + alias to_hash as_json + + def attribute_performance_for_resource_types + # We want to consider "or" behavior for multiple metric_types. Namely if you specify any + # metric type (other than Searches_Platform) you're going to get results. + # + # See https://github.com/scientist-softserv/palni-palci/issues/686#issuecomment-1785326034 + return [] if metric_type_in_params && (metric_types & (ALLOWED_METRIC_TYPES - ['Searches_Platform'])).count.zero? + + data_for_resource_types.map do |record| + { + "Data_Type" => record.resource_type, + "Access_Method" => "Regular", + "Performance" => performance(record) + } + end + end + + def attribute_performance_for_platform + return [] if metric_type_in_params && metric_types.exclude?("Searches_Platform") + return [] if data_type_in_params && !data_types.find { |dt| dt.casecmp("Platform").zero? } + + [{ + "Data_Type" => "Platform", + "Access_Method" => "Regular", + "Performance" => { + "Searches_Platform" => if granularity == 'Totals' + total_for_platform = data_for_platform.sum(&:total_item_investigations) + { "Totals" => total_for_platform } + else + data_for_platform.each_with_object({}) do |record, hash| + hash[record.year_month.strftime("%Y-%m")] = record.total_item_investigations + hash + end + end + } + }] + end + + def performance(record) + metric_types.each_with_object({}) do |metric_type, returning_hash| + next if metric_type == "Searches_Platform" + + returning_hash[metric_type] = + if granularity == 'Totals' + { "Totals" => record.performance.sum { |hash| hash.fetch(metric_type) } } + else + record.performance.each_with_object({}) do |cell, hash| + hash[cell.fetch('year_month')] = cell.fetch(metric_type) + end + end + end + end + + ## + # @note the `date_trunc` SQL function is specific to Postgresql. It will take the date/time field + # value and return a date/time object that is at the exact start of the date specificity. + # + # For example, if we had "2023-01-03T13:14" and asked for the date_trunc of month, the + # query result value would be "2023-01-01T00:00" (e.g. the first moment of the first of the + # month). + # also, note that unique_item_requests and unique_item_investigations should be counted for Hyrax::CounterMetrics that have unique dates, and unique work IDs. + # see the docs for counting unique items here: https://cop5.projectcounter.org/en/5.1/07-processing/03-counting-unique-items.html + # rubocop:disable Metrics/LineLength + def data_for_resource_types + # We're capturing this relation/query because in some cases, we need to chain another where + # clause onto the relation. + relation = Hyrax::CounterMetric + .select(:resource_type, :worktype, + %((SELECT To_json(Array_agg(Row_to_json(t))) + FROM + (SELECT + -- The AS field_name needs to be double quoted so as to preserve case structure. + SUM(total_item_investigations) as "Total_Item_Investigations", + SUM(total_item_requests) as "Total_Item_Requests", + COUNT(DISTINCT CASE WHEN total_item_investigations IS NOT NULL THEN CONCAT(work_id, '_', date::text) END) as "Unique_Item_Investigations", + COUNT(DISTINCT CASE WHEN total_item_requests IS NOT NULL THEN CONCAT(work_id, '_', date::text) END) as "Unique_Item_Requests", + -- We need to coerce the month from a single digit to two digits (e.g. August's "8" into "08") + CONCAT(DATE_PART('year', date_trunc('month', date)), '-', to_char(DATE_PART('month', date_trunc('month', date)), 'fm00')) AS year_month + FROM hyrax_counter_metrics AS aggr + WHERE #{Hyrax::CounterMetric.sanitize_sql_for_conditions(['aggr.resource_type = hyrax_counter_metrics.resource_type AND date >= ? AND date <= ?', begin_date, end_date])} + GROUP BY date_trunc('month', date)) t) as performance)) + .where("date >= ? AND date <= ?", begin_date, end_date) + .order(resource_type: :asc) + .group(:resource_type, :worktype) + return relation if data_types.blank? + + relation.where("LOWER(resource_type) IN (?)", data_types) + end + # rubocop:enable Metrics/LineLength + + def data_for_platform + Hyrax::CounterMetric + .select("date_trunc('month', date) AS year_month", + "SUM(total_item_investigations) as total_item_investigations", + "SUM(total_item_requests) as total_item_requests") + .where("date >= ? AND date <= ?", begin_date, end_date) + .order("year_month") + .group("date_trunc('month', date)") + end + end +end diff --git a/app/models/sushi/platform_usage_report.rb b/app/models/sushi/platform_usage_report.rb new file mode 100644 index 0000000000..0e73fb47a6 --- /dev/null +++ b/app/models/sushi/platform_usage_report.rb @@ -0,0 +1,160 @@ +# frozen_string_literal:true + +# counter compliant format for the Platform Usage Report is found here: https://countermetrics.stoplight.io/docs/counter-sushi-api/82bd896d1dd60-pr-p1-platform-usage +# +# dates will be filtered by including the begin_date, and excluding the end_date +# e.g. if you want to full month, begin_date should be the first day of that month, and end_date should be the first day of the following month. +module Sushi + class PlatformUsageReport + attr_reader :created, :account + include Sushi + include Sushi::DataTypeCoercion + include Sushi::DateCoercion + include Sushi::MetricTypeCoercion + include Sushi::ParameterValidation + + # the platform usage report only contains requests. see https://countermetrics.stoplight.io/docs/counter-sushi-api/mgu8ibcbgrwe0-pr-p1-performance-other for details + ALLOWED_METRIC_TYPES = [ + "Searches_Platform", + "Total_Item_Requests", + "Unique_Item_Requests" + ].freeze + + ALLOWED_PARAMETERS = [ + 'access_method', + 'api_key', + 'begin_date', + 'customer_id', + 'end_date', + 'metric_type', + 'platform', + 'requestor_id' + ].freeze + + def initialize(params = {}, created: Time.zone.now, account:) + Sushi.info = [] + validate_paramaters(params, allowed_parameters: ALLOWED_PARAMETERS) + coerce_data_types(params) + coerce_dates(params) + coerce_metric_types(params, allowed_types: ALLOWED_METRIC_TYPES) + + @created = created + @account = account + end + + def as_json(_options = {}) + report_hash = { + "Report_Header" => { + "Release" => "5.1", + "Report_ID" => "PR_P1", + "Report_Name" => "Platform Usage", + "Created" => created.rfc3339, # "2023-02-15T09:11:12Z" + "Created_By" => account.institution_name, + "Institution_ID" => account.institution_id_data, + "Institution_Name" => account.institution_name, + "Registry_Record" => "", + "Report_Filters" => { + "Begin_Date" => begin_date.iso8601, + "End_Date" => end_date.iso8601, + "Access_Method" => [ + "Regular" + ] + } + }, + "Report_Items" => { + "Platform" => account.cname, + "Attribute_Performance" => attribute_performance_for_resource_types + attribute_performance_for_platform + } + } + report_hash["Report_Header"]["Report_Filters"]["Data_Type"] = data_types if data_type_in_params + report_hash["Report_Header"]["Report_Filters"]["Metric_Type"] = metric_types if metric_type_in_params + report_hash["Report_Header"]["Exceptions"] = info if info.present? + + report_hash + end + alias to_hash as_json + + def attribute_performance_for_resource_types + return [] if metric_type_in_params && metric_types.include?("Searches_Platform") + + data_for_resource_types.map do |record| + { + "Data_Type" => record.resource_type, + "Access_Method" => "Regular", + "Performance" => performance(record) + } + end + end + + def performance(record) + metric_types.each_with_object({}) do |metric_type, returning_hash| + next if metric_type == "Searches_Platform" + + returning_hash[metric_type] = record.performance.each_with_object({}) do |cell, hash| + hash[cell.fetch('year_month')] = cell.fetch(metric_type) + end + end + end + + def attribute_performance_for_platform + return [] if metric_type_in_params && metric_types.exclude?("Searches_Platform") + + [{ + "Data_Type" => "Platform", + "Access_Method" => "Regular", + "Performance" => { + "Searches_Platform" => data_for_platform.each_with_object({}) do |record, hash| + hash[record.year_month.strftime("%Y-%m")] = record.total_item_investigations + hash + end + } + }] + end + + ## + # @note the `date_trunc` SQL function is specific to Postgresql. It will take the date/time field + # value and return a date/time object that is at the exact start of the date specificity. + # + # For example, if we had "2023-01-03T13:14" and asked for the date_trunc of month, the + # query result value would be "2023-01-01T00:00" (e.g. the first moment of the first of the + # month). + # rubocop:disable Metrics/LineLength + def data_for_resource_types + # We're capturing this relation/query because in some cases, we need to chain another where + # clause onto the relation. + relation = Hyrax::CounterMetric + .select(:resource_type, :worktype, + %((SELECT To_json(Array_agg(Row_to_json(t))) + FROM + (SELECT + -- The AS field_name needs to be double quoted so as to preserve case structure. + SUM(total_item_investigations) as "Total_Item_Investigations", + SUM(total_item_requests) as "Total_Item_Requests", + COUNT(DISTINCT CASE WHEN total_item_investigations IS NOT NULL THEN CONCAT(work_id, '_', date::text) END) as "Unique_Item_Investigations", + COUNT(DISTINCT CASE WHEN total_item_requests IS NOT NULL THEN CONCAT(work_id, '_', date::text) END) as "Unique_Item_Requests", + -- We need to coerce the month from a single digit to two digits (e.g. August's "8" into "08") + CONCAT(DATE_PART('year', date_trunc('month', date)), '-', to_char(DATE_PART('month', date_trunc('month', date)), 'fm00')) AS year_month + FROM hyrax_counter_metrics AS aggr + WHERE #{Hyrax::CounterMetric.sanitize_sql_for_conditions(['aggr.resource_type = hyrax_counter_metrics.resource_type AND date >= ? AND date <= ?', begin_date, end_date])} + GROUP BY date_trunc('month', date)) t) as performance)) + .where("date >= ? AND date <= ?", begin_date, end_date) + .order(resource_type: :asc) + .group(:resource_type, :worktype) + + return relation if data_types.blank? + + relation.where("LOWER(resource_type) IN (?)", data_types) + end + # rubocop:enable Metrics/LineLength + + def data_for_platform + Hyrax::CounterMetric + .select("date_trunc('month', date) AS year_month", + "SUM(total_item_investigations) as total_item_investigations", + "SUM(total_item_requests) as total_item_requests") + .where("date >= ? AND date <= ?", begin_date, end_date) + .order("year_month") + .group("date_trunc('month', date)") + end + end +end diff --git a/app/models/sushi/report_list.rb b/app/models/sushi/report_list.rb new file mode 100644 index 0000000000..a12a303cbf --- /dev/null +++ b/app/models/sushi/report_list.rb @@ -0,0 +1,55 @@ +# frozen_string_literal:true + +# counter compliant format for ReportList: https://countermetrics.stoplight.io/docs/counter-sushi-api/af75bac10e789-report-list +module Sushi + class ReportList + # rubocop:disable Metrics/MethodLength, Metrics/LineLength + def as_json(_options = nil) + [ + { + "Report_Name" => "Server Status", + "Report_ID" => "status", + "Release" => "5.1", + "Report_Description" => "This resource returns the current status of the reporting service supported by this API.", + "Path" => "/api/sushi/r51/status" + }, + { + "Report_Name" => "Report List", + "Report_ID" => "reports", + "Release" => "5.1", + "Report_Description" => "This resource returns a list of reports supported by the API for a given application.", + "Path" => "/api/sushi/r51/reports" + }, + { + "Report_Name" => "Platform Report", + "Report_ID" => "pr", + "Release" => "5.1", + "Report_Description" => "This resource returns COUNTER 'Platform Master Report' [PR]. A customizable report summarizing activity across a provider’s platforms that allows the user to apply filters and select other configuration options for the report.", + "Path" => "/api/sushi/r51/reports/pr", + "First_Month_Available" => Sushi.rescued_first_month_available, + "Last_Month_Available" => Sushi.rescued_last_month_available + }, + { + "Report_Name" => "Platform Usage", + "Report_ID" => "pr_p1", + "Release" => "5.1", + "Report_Description" => "This resource returns COUNTER 'Platform Usage' [pr_p1]. This is a Standard View of the Package Master Report that presents usage for the overall Platform broken down by Metric_Type.", + "Path" => "/api/sushi/r51/reports/pr_p1", + "First_Month_Available" => Sushi.rescued_first_month_available, + "Last_Month_Available" => Sushi.rescued_last_month_available + }, + { + "Report_Name" => "Item Report", + "Report_ID" => "ir", + "Release" => "5.1", + "Report_Description" => "This resource returns COUNTER 'Item Master Report' [IR].", + "Path" => "/api/sushi/r51/reports/ir", + "First_Month_Available" => Sushi.rescued_first_month_available, + "Last_Month_Available" => Sushi.rescued_last_month_available + } + ] + end + alias to_hash as_json + # rubocop:enable Metrics/MethodLength, Metrics/LineLength + end +end diff --git a/app/models/sushi/server_status.rb b/app/models/sushi/server_status.rb new file mode 100644 index 0000000000..60dc584e6d --- /dev/null +++ b/app/models/sushi/server_status.rb @@ -0,0 +1,22 @@ +# frozen_string_literal:true + +# counter compliant format for the Server Status Endpoint is found here: https://countermetrics.stoplight.io/docs/counter-sushi-api/f0dd30f814944-server-status +module Sushi + class ServerStatus + attr_reader :account + + def initialize(account:) + @account = account + end + + def server_status + [ + { + "Description" => "COUNTER Usage Reports for #{account.cname} platform.", + "Service_Active" => true, + "Registry_Record" => "" + } + ] + end + end +end diff --git a/app/presenters/hyku/menu_presenter.rb/menu_presenter.rb b/app/presenters/hyku/menu_presenter.rb/menu_presenter.rb new file mode 100644 index 0000000000..807c67c451 --- /dev/null +++ b/app/presenters/hyku/menu_presenter.rb/menu_presenter.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Hyku + # view-model for the admin menu + class MenuPresenter < Hyrax::MenuPresenter + # Returns true if the current controller happens to be one of the controllers that deals + # with settings. This is used to keep the parent section on the sidebar open. + def settings_section? + %w[appearances content_blocks labels features pages].include?(controller_name) + end + + # Returns true if the current controller happens to be one of the controllers that deals + # with roles and permissions. This is used to keep the parent section on the sidebar open. + def roles_and_permissions_section? + # we're using a case here because we need to differentiate UsersControllers + # in different namespaces (Hyrax & Admin) + case controller + when Hyrax::Admin::UsersController, ::Admin::GroupsController + true + else + false + end + end + + # Returns true if the current controller happens to be one of the controllers that deals + # with repository activity This is used to keep the parent section on the sidebar open. + def repository_activity_section? + %w[admin dashboard status].include?(controller_name) + end + + # Returns true if we ought to show the user the 'Configuration' section + # of the menu + def show_configuration? + super || + can?(:manage, Site) || + can?(:manage, User) || + can?(:manage, Hyrax::Group) + end + + def display_workflow_roles_menu_item_in_admin_dashboard_sidebar? + Flipflop.show_workflow_roles_menu_item_in_admin_dashboard_sidebar? + end + + # Returns true if we ought to show the user Admin-only areas of the menu + def show_admin_menu_items? + can?(:read, :admin_dashboard) + end + + def show_task? + can?(:review, :submissions) || + can?(:read, User) || + can?(:read, Hyrax::Group) || + can?(:read, :admin_dashboard) + end + end +end diff --git a/app/services/generate_counter_metrics.rb b/app/services/generate_counter_metrics.rb new file mode 100644 index 0000000000..9b427254ad --- /dev/null +++ b/app/services/generate_counter_metrics.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Creates counter metrics for a single tenant. +class GenerateCounterMetrics + def self.generate_counter_metrics(ids: :all, limit: :all) + fsq = "has_model_ssim: (#{Bulkrax.curation_concerns.join(' OR ')})" + fsq += " AND id:(\"" + Array.wrap(ids).join('" OR "') + "\")" if ids.present? && ids != :all + options = { fl: "id, has_model_ssim, resource_type_tesim, date_ssi, creator_tesim, publisher_tesim, title_tesim", method: :post } + options[:rows] = limit if limit.is_a?(Numeric) + ActiveFedora::SolrService.query(fsq, options).each do |work| + work_type = work.fetch('has_model_ssim').first + work_id = work.fetch('id') + resource_type = work.fetch('resource_type_tesim').first + year_of_publication = work.fetch('date_ssi') + author = Sushi::AuthorCoercion.serialize(work.fetch('creator_tesim')) + publisher = work.fetch('publisher_tesim')&.first if work['publisher_tesim'] + title = work.fetch('title_tesim')&.first if work['title_tesim'] + (0...90).each do |i| + date = i.days.ago.to_date + total_item_investigations = rand(1..10) + total_item_requests = rand(1..10) + + Hyrax::CounterMetric.create!( + worktype: work_type, + work_id: work_id, + resource_type: resource_type, + date: date, + year_of_publication: year_of_publication, + author: author, + publisher: publisher, + title: title, + total_item_investigations: total_item_investigations, + total_item_requests: total_item_requests + ) + end + end + message = "#{self.class}.generate_counter_metrics data for IDs = #{ids.inspect} with Limit = #{limit.inspect}." + Rails.logger.info message + end +end diff --git a/app/services/hyrax/collections/collection_member_search_service_decorator.rb b/app/services/hyrax/collections/collection_member_search_service_decorator.rb new file mode 100644 index 0000000000..1fa07cc1a5 --- /dev/null +++ b/app/services/hyrax/collections/collection_member_search_service_decorator.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0 to sort subcollections by title +module Hyrax + module Collections + module CollectionMemberSearchServiceDecorator + DEFAULT_SORT_FIELD = 'title_ssi asc' + + def available_member_works + sort_field = user_params[:sort] || DEFAULT_SORT_FIELD + response, _docs = search_results do |builder| + builder.search_includes_models = :works + builder.merge(sort: sort_field) + builder + end + response + end + end + end +end + +Hyrax::Collections::CollectionMemberSearchService.prepend Hyrax::Collections::CollectionMemberSearchServiceDecorator diff --git a/app/services/hyrax/solr_query_service_decorator.rb b/app/services/hyrax/solr_query_service_decorator.rb new file mode 100644 index 0000000000..bae21e26da --- /dev/null +++ b/app/services/hyrax/solr_query_service_decorator.rb @@ -0,0 +1,13 @@ +# OVERRIDE: Hyrax 5.0.0 changes GET request to POST to allow for larger query size + +# frozen_string_literal: true + +module Hyrax + module SolrQueryServiceDecorator + def get(*args) + solr_service.post(build, *args) + end + end +end + +Hyrax::SolrQueryService.prepend(Hyrax::SolrQueryServiceDecorator) diff --git a/app/services/import_counter_metrics.rb b/app/services/import_counter_metrics.rb new file mode 100644 index 0000000000..549fd67a84 --- /dev/null +++ b/app/services/import_counter_metrics.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +# Import counter views and downloads for a given tenant +# Currently, this only imports the first resource type. Additional work would need to be done for works with multiple resource types. +# The resource_type field in the counter_metrics table is single value and not an array. +class ImportCounterMetrics + # rubocop:disable Metrics/MethodLength, BlockLength + def self.import_investigations(csv_path) + CSV.foreach(csv_path, headers: true) do |row| + work = ActiveFedora::Base.where(bulkrax_identifier_tesim: row['eprintid']).first + next if work.nil? + worktype = work.class + work_id = work.id + resource_type = work.resource_type&.first + date = row['datestamp'] + year_of_publication = work.date + author = Sushi::AuthorCoercion.serialize(work.creator) + publisher = work.publisher&.first + title = work.title&.first + total_item_investigations = row['count'] + counter_investigation = Hyrax::CounterMetric.find_by(work_id: work_id, date: date) + if counter_investigation.present? + counter_investigation.update( + worktype: worktype, + work_id: work_id, + resource_type: resource_type, + date: date, + total_item_investigations: total_item_investigations, + year_of_publication: year_of_publication, + author: author, + publisher: publisher, + title: title + ) + else + Hyrax::CounterMetric.create!( + worktype: worktype, + work_id: work_id, + resource_type: resource_type, + date: date, + total_item_investigations: total_item_investigations, + year_of_publication: year_of_publication, + author: author, + publisher: publisher, + title: title + ) + end + end + end + + def self.import_requests(csv_path) + CSV.foreach(csv_path, headers: true) do |row| + work = ActiveFedora::Base.where(bulkrax_identifier_tesim: row['eprintid']).first + next if work.nil? + worktype = work.class + work_id = work.id + resource_type = work.resource_type.first + date = row['datestamp'] + year_of_publication = work.date + author = Sushi::AuthorCoercion.serialize(work.creator) + publisher = work.publisher&.first + title = work.title&.first + total_item_requests = row['count'] + counter_request = Hyrax::CounterMetric.find_by(work_id: work_id, date: date) + if counter_request.present? + counter_request.update( + worktype: worktype, + work_id: work_id, + resource_type: resource_type, + date: date, + total_item_requests: total_item_requests, + year_of_publication: year_of_publication, + author: author, + publisher: publisher, + title: title + ) + else + Hyrax::CounterMetric.create!( + worktype: worktype, + work_id: work_id, + resource_type: resource_type, + date: date, + total_item_requests: total_item_requests, + year_of_publication: year_of_publication, + author: author, + publisher: publisher, + title: title + ) + end + end + end + # rubocop:enable Metrics/MethodLength, Metrics/BlockLength +end diff --git a/lib/hyrax/controlled_vocabularies/location_decorator.rb b/lib/hyrax/controlled_vocabularies/location_decorator.rb new file mode 100644 index 0000000000..9dff981148 --- /dev/null +++ b/lib/hyrax/controlled_vocabularies/location_decorator.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Hyrax + module ControlledVocabularies + module LocationDecorator + # the Location class throws an error that split is undefined since it is an enumerator, but this error is coming from deeply nested dependencies. + # This causes 500 errors for works with their Location field filled in. + # defining split just for the purpose of getting past this error seemed to work and allow locations to save correctly with no adverse effects. + # See https://github.com/scientist-softserv/palni-palci/issues/530 for more details + def split(*) + [] + end + + def present? + return true if id + + false + end + + def rdf_label + return [id.gsub('http://fake#', '').gsub('%20', ' ')] if fake_location? + + super + end + + def full_label + return rdf_label.first.to_s if fake_location? + + super + end + + def fake_location? + id.start_with?('http://fake#') + end + end + end +end + +Hyrax::ControlledVocabularies::Location.prepend(Hyrax::ControlledVocabularies::LocationDecorator) diff --git a/lib/tasks/counter_metrics.rake b/lib/tasks/counter_metrics.rake new file mode 100644 index 0000000000..213662c7a6 --- /dev/null +++ b/lib/tasks/counter_metrics.rake @@ -0,0 +1,47 @@ +namespace :counter_metrics do + # bundle exec rake counter_metrics:import_investigations['pittir.hykucommons.org','spec/fixtures/csv/pittir-views.csv'] + desc 'import historical counter requests' + task 'import_requests', [:tenant_cname, :csv_path] => [:environment] do |_cmd, args| + raise ArgumentError, 'A tenant cname is required: `rake counter_metrics:import_requests[tenant_cname]`' if args.tenant_cname.blank? + raise ArgumentError, "Tenant not found: #{args.tenant_cname}. Are you sure this is the correct tenant cname?" unless Account.where(cname: args.tenant_cname).first + AccountElevator.switch!(args.tenant_cname) + puts "Importing historical requests for #{args.tenant_cname}" + ImportCounterMetrics.import_requests(args.csv_path) + end + + # bundle exec rake counter_metrics:import_requests['pittir.hykucommons.org','spec/fixtures/csv/pittir-downloads.csv'] + desc 'import historical counter investigations' + task 'import_investigations', [:tenant_cname, :csv_path] => [:environment] do |_cmd, args| + raise ArgumentError, 'A tenant cname is required: `rake counter_metrics:import_investigations[tenant_cname]`' if args.tenant_cname.blank? + raise ArgumentError, "Tenant not found: #{args.tenant_cname}. Are you sure this is the correct tenant cname?" unless Account.where(cname: args.tenant_cname).first + AccountElevator.switch!(args.tenant_cname) + puts "Importing historical investigations for #{args.tenant_cname}" + ImportCounterMetrics.import_investigations(args.csv_path) + end + + # You can optionally pass work IDs to have hyrax counter metrics created for specific works. + # Or, you can pass a limit for the number of CounterMetric entries you would like to create. currently they are randomly created. + # + # Example for pitt tenant in dev without ids: + # bundle exec rake counter_metrics:generate_staging_metrics[pitt.hyku.test] + # + # Example with ids: + # bundle exec rake "counter_metrics:generate_staging_metrics[pitt.hyku.test, ab3c1f9d-684a-4c14-93b1-75586ec05f7a|891u493hdfhiu939]" + # + # Example with limit of 1: + # bundle exec rake "counter_metrics:generate_staging_metrics[pitt.hyku.test, , 1]" + # + # NOTE: this should never be run in prod. + # It generates fake data, so running in prod would risk contaminating prod data. + desc 'generate counter metric test data for staging' + task 'generate_staging_metrics', [:tenant_cname, :ids, :limit] => [:environment] do |_cmd, args| + raise ArgumentError, 'A tenant cname is required: `rake counter_metrics:generate_staging_metrics[tenant_cname]`' if args.tenant_cname.blank? + kwargs = {} + kwargs[:ids] = args.ids.split("|") if args.ids.present? + kwargs[:limit] = Integer(args.limit) if args.limit.present? + AccountElevator.switch!(args.tenant_cname) + puts "Creating test counter metric data for #{args.tenant_cname}" + GenerateCounterMetrics.generate_counter_metrics(**kwargs) + puts 'Test data created successfully' + end +end diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake new file mode 100644 index 0000000000..693118dd5e --- /dev/null +++ b/lib/tasks/sidekiq.rake @@ -0,0 +1,13 @@ +namespace :sidekiq do + desc "Health check for sidekiq worker process." + task status: [:environment] do + require "socket" + + hostname = Socket.gethostname + if Sidekiq::ProcessSet.new.any? { |ps| ps["hostname"] == hostname } + exit 0 + else + exit 1 + end + end +end diff --git a/lib/tasks/tenants.rake b/lib/tasks/tenants.rake new file mode 100644 index 0000000000..cb4c061e2f --- /dev/null +++ b/lib/tasks/tenants.rake @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +namespace :tenants do + # how much space, works, files, per each tenant? + task calculate_usage: :environment do + @results = [] + Account.where(search_only: false).find_each do |account| + if account.cname.present? + AccountElevator.switch!(account.cname) + puts "---------------#{account.cname}-------------------------" + models = Hyrax.config.curation_concerns.map { |m| "\"#{m}\"" } + works = ActiveFedora::SolrService.query("has_model_ssim:(#{models.join(' OR ')})", rows: 100_000) + if works&.any? + puts "#{works.count} works found" + @tenant_file_sizes = [] + works.each do |work| + document = SolrDocument.find(work.id) + files = document._source["file_set_ids_ssim"] + if files&.any? + file_sizes = [] + files.each do |file| + f = SolrDocument.find(file.to_s) + if file + file_sizes.push(f.to_h['file_size_lts']) unless f.to_h['file_size_lts'].nil? + else + files_sizes.push(0) + end + end + if file_sizes.any? + file_sizes_total_bytes = file_sizes.inject(0, :+) + file_size_total = (file_sizes_total_bytes / 1.0.megabyte).round(2) + else + file_size_total = 0 + end + @tenant_file_sizes.push(file_size_total) + else + @tenant_file_sizes.push(0) + end + end + if @tenant_file_sizes + tenant_file_sizes_total_megabytes = @tenant_file_sizes.inject(0, :+) + @results.push("#{account.cname}: #{tenant_file_sizes_total_megabytes} Total MB / #{works.count} Works") + else + @results.push("#{account.cname}: 0 Total MB / #{works.count} Works") + end + else + @results.push("#{account.cname}: 0 Total MB / 0 Works") + end + puts "==================================================================" + @results.each do |result| + puts result + end + end + end + end +end diff --git a/lib/tasks/workflow_setup.rake b/lib/tasks/workflow_setup.rake new file mode 100644 index 0000000000..b9796fd2c3 --- /dev/null +++ b/lib/tasks/workflow_setup.rake @@ -0,0 +1,6 @@ +namespace :workflow do + desc 'Load new workflow so it is available to existing admin sets' + task setup: :environment do + Hyrax::Workflow::WorkflowImporter.load_workflows + end +end diff --git a/spec/jobs/create_large_derivatives_job_spec.rb b/spec/jobs/create_large_derivatives_job_spec.rb new file mode 100644 index 0000000000..1ed67ef2c0 --- /dev/null +++ b/spec/jobs/create_large_derivatives_job_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.describe CreateLargeDerivativesJob, type: :job do + let(:id) { '123' } + let(:file_set) { FileSet.new } + let(:file) do + Hydra::PCDM::File.new.tap do |f| + f.content = 'foo' + f.original_name = 'video.mp4' + f.save! + end + end + + before do + allow(FileSet).to receive(:find).with(id).and_return(file_set) + allow(file_set).to receive(:id).and_return(id) + # Short-circuit irrelevant logic + allow(file_set).to receive(:reload) + allow(file_set).to receive(:update_index) + end + + it 'runs in the :auxiliary queue' do + expect { described_class.perform_later(file_set, file.id) } + .to have_enqueued_job(described_class) + .on_queue('auxiliary') + end + + # @see CreateDerivativesJobDecorator#perform + it "doesn't schedule itself infinitly" do + expect(described_class).not_to receive(:perform_later) + + described_class.perform_now(file_set, file.id) + end + + it 'successfully calls the logic in CreateDerivativesJob' do + allow(file_set).to receive(:mime_type).and_return('video/mp4') + expect(Hydra::Derivatives::VideoDerivatives).to receive(:create) + + described_class.perform_now(file_set, file.id) + end +end diff --git a/spec/models/sushi/item_report_spec.rb b/spec/models/sushi/item_report_spec.rb new file mode 100644 index 0000000000..76f276ea37 --- /dev/null +++ b/spec/models/sushi/item_report_spec.rb @@ -0,0 +1,251 @@ +# frozen_string_literal:true + +RSpec.describe Sushi::ItemReport do + subject { described_class.new(params, created: created, account: account).as_json } + + let(:account) { double(Account, institution_name: 'Pitt', institution_id_data: {}, cname: 'pitt.hyku.test') } + let(:created) { Time.zone.now } + let(:required_parameters) do + { + begin_date: '2022-01', + end_date: '2023-08' + } + end + + before { create_hyrax_countermetric_objects } + + describe '#as_json' do + let(:params) do + { + **required_parameters, + attributes_to_show: ['Access_Method', 'Fake_Value'] + } + end + + it 'has the expected properties' do + expect(subject).to be_key('Report_Header') + expect(subject.dig('Report_Header', 'Report_Name')).to eq('Item Report') + expect(subject.dig('Report_Header', 'Created')).to eq(created.rfc3339) + expect(subject.dig('Report_Header', 'Report_Attributes', 'Attributes_To_Show')).to eq(['Access_Method']) + expect(subject.dig('Report_Header', 'Report_Filters', 'Begin_Date')).to eq('2022-01-01') + expect(subject.dig('Report_Header', 'Report_Filters', 'End_Date')).to eq('2023-08-31') + expect(subject.dig('Report_Items', 0, 'Items', 0, 'Attribute_Performance', 0, 'Performance', 'Total_Item_Investigations', '2023-08')).to eq(2) + expect(subject.dig('Report_Items', 0, 'Items', 0, 'Attribute_Performance', 0, 'Data_Type')).to eq('Article') + expect(subject.dig('Report_Items', 0, 'Items', 0, 'Attribute_Performance', 0, 'Performance', 'Unique_Item_Investigations', '2023-08')).to eq(1) + expect(subject.dig('Report_Items', 2, 'Items', 0, 'Attribute_Performance', 0, 'Performance', 'Unique_Item_Investigations', '2022-01')).to eq(1) + end + + describe 'yop parameter' do + context 'when provided' do + let(:params) do + { + **required_parameters, + yop: '1900-2023' + } + end + + it 'has a Report_Header > Report_Filters > YOP property' do + expect(subject.dig("Report_Header", "Report_Filters", "YOP")).to eq('1900-2023') + end + end + + context 'when not provided' do + it 'does not have a Report_Header > Report_Filters > YOP property' do + expect(subject.dig("Report_Header", "Report_Filters")).not_to have_key("YOP") + end + end + end + end + + describe 'with an access_method parameter' do + context 'that is fully valid' do + let(:params) do + { + **required_parameters, + access_method: 'regular' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Access_Method')).to eq(['regular']) + expect(subject.dig('Report_Items', 0, 'Items', 0, 'Attribute_Performance', 0, 'Access_Method')).to eq('Regular') + end + end + + context 'that is partially valid' do + let(:params) do + { + **required_parameters, + access_method: 'regular|tdm' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Access_Method')).to eq(['regular']) + expect(subject.dig('Report_Items', 0, 'Items', 0, 'Attribute_Performance', 0, 'Access_Method')).to eq('Regular') + end + end + + context 'that is invalid' do + let(:params) do + { + **required_parameters, + access_method: 'other' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe 'with a yop parameter' do + let(:params) { { yop: "2022", begin_date: '2022-01-03', end_date: '2022-12-31' } } + + # NOTE: We're already bombarding the yop coercion; this is ensuring that the SQL is valid. + it 'filters based on that value' do + expect(subject).to be_a(Hash) + end + end + + describe 'with an item_id parameter' do + context 'that is valid' do + context 'and metrics during the dates specified for that id' do + let(:params) do + { + item_id: '54321', + begin_date: '2022-01-03', + end_date: '2022-04-09' + } + end + + it 'returns one report item' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Item_ID')).to eq('54321') + expect(subject.dig('Report_Items', 0, 'Items', 0, 'Item')).to eq('54321') + end + end + + context 'and no metrics during the dates specified for that id' do + let(:params) do + { + item_id: '54321', + begin_date: '2023-06-05', + end_date: '2023-08-09' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::NoUsageAvailableForRequestedDatesError) + end + end + end + + context 'that is invalid' do + let(:params) do + { + item_id: 'qwerty123', + begin_date: '2022-01-03', + end_date: '2023-08-09' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe 'with an author parameter' do + context 'that is valid' do + let(:params) do + { + **required_parameters, + author: 'Tubman, Harriet' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Author')).to eq('Tubman, Harriet') + end + end + + context 'that is invalid' do + let(:params) do + { + **required_parameters, + author: 'Tubman,Harriet' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe 'with a platform parameter' do + context 'that is valid' do + let(:params) do + { + **required_parameters, + platform: 'pitt.hyku.test' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Platform')).to eq('pitt.hyku.test') + end + end + + context 'that is invalid' do + let(:params) do + { + **required_parameters, + platform: 'another-tenant' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe 'with a metric_type parameter' do + context 'that is valid' do + let(:params) do + { + **required_parameters, + metric_type: 'total_item_investigations' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Metric_Type')).to eq(["Total_Item_Investigations"]) + expect(subject.dig('Report_Items', 0, 'Items', 0, 'Attribute_Performance', 0, 'Performance').length).to eq(1) + end + end + + context 'that is invalid' do + let(:params) do + { + **required_parameters, + metric_type: 'some_other_metric' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe 'with an unrecognized parameter' do + let(:params) { { other: 'nope' } } + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::UnrecognizedParameterError) + end + end +end diff --git a/spec/models/sushi/platform_report_spec.rb b/spec/models/sushi/platform_report_spec.rb new file mode 100644 index 0000000000..af0526e216 --- /dev/null +++ b/spec/models/sushi/platform_report_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal:true + +RSpec.describe Sushi::PlatformReport do + subject { described_class.new(params, created: created, account: account).to_hash } + + let(:account) { double(Account, institution_name: 'Pitt', institution_id_data: {}, cname: 'pitt.hyku.test') } + let(:created) { Time.zone.now } + let(:required_parameters) do + { + begin_date: '2022-01', + end_date: '2022-02' + } + end + + before { create_hyrax_countermetric_objects } + + describe '#as_json' do + context 'with only required params' do + let(:params) { required_parameters } + + it 'has the expected keys' do + expect(subject).to be_key('Report_Header') + expect(subject.dig('Report_Header', 'Created')).to eq(created.rfc3339) + expect(subject.dig('Report_Header', 'Report_Filters', 'Begin_Date')).to eq('2022-01-01') + expect(subject.dig('Report_Header', 'Report_Filters', 'End_Date')).to eq('2022-02-28') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance', 'Total_Item_Investigations', '2022-01')).to eq(6) + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance', 'Total_Item_Requests', '2022-01')).to eq(19) + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance', 'Unique_Item_Investigations', '2022-01')).to eq(3) + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance', 'Unique_Item_Requests', '2022-01')).to eq(3) + expect(subject.dig('Report_Items', 'Attribute_Performance').find { |o| o["Data_Type"] == "Platform" }.dig('Performance', 'Searches_Platform', '2022-01')).to eq(6) + # It should not include that 2021 data. + expect(subject.dig('Report_Items', 'Attribute_Performance', 0, 'Performance', 'Total_Item_Investigations')).to eq("2022-01" => 6) + end + end + + context 'when given metric_types searches_platform AND total_item_requests' do + let(:params) do + { + begin_date: '2023-08', + end_date: '2023-09', + metric_type: 'total_item_requests|searches_platform' + } + end + + it 'includes the platform data type and the article data type (which is the only data type within the date range' do + expect(subject.dig('Report_Items', 'Attribute_Performance').map { |ap| ap['Data_Type'] }.sort).to match_array(['Article', 'Platform']) + end + end + context 'when given a non-platform data_type and omit searches platform' do + let(:params) do + { + begin_date: '2023-08', + end_date: '2023-09', + data_type: 'article', + attributes_to_show: ['Access_Method', 'Fake_Value'], + granularity: 'totals' + } + end + + it 'omits the platform data type' do + expect(subject.dig('Report_Items', 'Attribute_Performance').detect { |dt| dt['Data_Type']['Platform'] }).to be_nil + end + end + + context 'with additional params that are not required' do + let(:params) do + { + begin_date: '2023-08', + end_date: '2023-09', + metric_type: 'total_item_investigations|unique_item_investigations|fake_value', + data_type: 'article', + attributes_to_show: ['Access_Method', 'Fake_Value'], + granularity: 'totals' + } + end + + it "only shows the requested metric types, and does not include metric types that aren't allowed" do + expect(subject.dig('Report_Header', 'Report_Filters', 'Metric_Type')).to eq(['Total_Item_Investigations', 'Unique_Item_Investigations']) + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).to have_key('Total_Item_Investigations') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).to have_key('Unique_Item_Investigations') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).not_to have_key('Total_Item_Requests') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).not_to have_key('Unique_Item_Requests') + end + + it "includes the correct attributes to show, and does not include attributes that aren't allowed" do + expect(subject.dig('Report_Header', 'Report_Attributes', 'Attributes_To_Show')).to eq(['Access_Method']) + end + + it 'does not show title requests/investigations for non-book resource types' do + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).not_to have_key('Unique_Title_Requests') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).not_to have_key('Unique_Title_Investigations') + end + + it 'sums the totals for each metric type' do + expect(subject.dig('Report_Header', 'Report_Attributes', 'Granularity')).to eq('Totals') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance', 'Total_Item_Investigations', 'Totals')).to be_a(Integer) + end + end + end + + describe 'with an access_method parameter' do + context 'that is fully valid' do + let(:params) do + { + **required_parameters, + access_method: 'regular' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Access_Method')).to eq(['regular']) + expect(subject.dig('Report_Items', 'Attribute_Performance', 0, 'Access_Method')).to eq('Regular') + end + end + + context 'that is partially valid' do + let(:params) do + { + **required_parameters, + access_method: 'regular|tdm' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Access_Method')).to eq(['regular']) + expect(subject.dig('Report_Items', 'Attribute_Performance', 0, 'Access_Method')).to eq('Regular') + end + end + + context 'that is invalid' do + let(:params) do + { + **required_parameters, + access_method: 'other' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe 'with a platform parameter' do + context 'that is valid' do + let(:params) do + { + **required_parameters, + platform: 'pitt.hyku.test' + } + end + + it 'returns the item report' do + expect(subject.dig('Report_Header', 'Report_Filters', 'Platform')).to eq('pitt.hyku.test') + end + end + + context 'that is invalid' do + let(:params) do + { + **required_parameters, + platform: 'another-tenant' + } + end + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe 'with an unrecognized parameter' do + let(:params) { { other: 'nope' } } + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::UnrecognizedParameterError) + end + end +end diff --git a/spec/models/sushi/platform_usage_report_spec.rb b/spec/models/sushi/platform_usage_report_spec.rb new file mode 100644 index 0000000000..225b5ea6b7 --- /dev/null +++ b/spec/models/sushi/platform_usage_report_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal:true + +RSpec.describe Sushi::PlatformUsageReport do + let(:account) { double(Account, institution_name: 'Pitt', institution_id_data: {}, cname: "pitt.edu") } + let(:created) { Time.zone.now } + let(:required_parameters) do + { + begin_date: '2022-01', + end_date: '2022-02' + } + end + + describe '#as_json' do + before { create_hyrax_countermetric_objects } + + subject { described_class.new(params, created: created, account: account).as_json } + + context 'with only required params' do + let(:params) { required_parameters } + + it 'has the expected keys' do + expect(subject).to be_key('Report_Header') + expect(subject.dig('Report_Header', 'Created')).to eq(created.rfc3339) + expect(subject.dig('Report_Header', 'Report_Filters', 'Begin_Date')).to eq('2022-01-01') + expect(subject.dig('Report_Header', 'Report_Filters', 'End_Date')).to eq('2022-02-28') + expect(subject.dig('Report_Items', 'Attribute_Performance').find { |o| o["Data_Type"] == "Platform" }.dig('Performance', 'Searches_Platform', '2022-01')).to eq(6) + end + end + + context 'with additional params that are not required' do + let(:params) do + { + **required_parameters, + metric_type: 'total_item_investigations|total_item_requests|fake_value', + access_method: ['Regular'] + } + end + + # Platform usage report should NOT show investigations, even if it is passed in the params. + it "only shows the requested metric types, and does not include metric types that aren't allowed" do + expect(subject.dig('Report_Header', 'Report_Filters', 'Metric_Type')).to eq(['Total_Item_Requests']) + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).to have_key('Total_Item_Requests') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).not_to have_key('Unique_Item_Requests') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).not_to have_key('Total_Item_Investigations') + expect(subject.dig('Report_Items', 'Attribute_Performance').first.dig('Performance')).not_to have_key('Unique_Item_Investigations') + end + end + end + + describe 'with an unrecognized parameter' do + let(:params) { { other: 'nope' } } + + it 'raises an error' do + expect { described_class.new(params, created: created, account: account).as_json }.to raise_error(Sushi::Error::UnrecognizedParameterError) + end + end +end diff --git a/spec/models/sushi/report_list_spec.rb b/spec/models/sushi/report_list_spec.rb new file mode 100644 index 0000000000..466185a3a5 --- /dev/null +++ b/spec/models/sushi/report_list_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +RSpec.describe Sushi::ReportList do + describe '#as_json' do + subject { described_class.new.as_json } + + it 'returns the correct format' do + expect(subject).to be_an_instance_of(Array) + expect(subject.length).to eq(5) + end + + it 'has the expected keys' do + expect(subject.last).to have_key('Report_Name') + expect(subject.last).to have_key('Report_ID') + expect(subject.last).to have_key('Release') + expect(subject.last).to have_key('Report_Description') + expect(subject.last).to have_key('Path') + expect(subject.last).to have_key('First_Month_Available') + expect(subject.last).to have_key('Last_Month_Available') + end + + context 'with data in the Hyrax::CounterMetric table' do + before { create_hyrax_countermetric_objects } + + it 'returns the expected values' do + expect(subject.last['Report_Name']).to eq('Item Report') + expect(subject.last['Report_ID']).to eq('ir') + expect(subject.last['Release']).to eq('5.1') + expect(subject.last['Report_Description']).to eq("This resource returns COUNTER 'Item Master Report' [IR].") + expect(subject.last['Path']).to eq('/api/sushi/r51/reports/ir') + expect(subject.last['First_Month_Available']).to be_a(String) + expect(subject.last['Last_Month_Available']).to be_a(String) + end + end + + context 'without data in the Hyrax::CounterMetric table' do + it 'returns the expected values' do + expect(subject.last['Report_Name']).to eq('Item Report') + expect(subject.last['Report_ID']).to eq('ir') + expect(subject.last['Release']).to eq('5.1') + expect(subject.last['Report_Description']).to eq("This resource returns COUNTER 'Item Master Report' [IR].") + expect(subject.last['Path']).to eq('/api/sushi/r51/reports/ir') + expect(subject.last['First_Month_Available']).to be_nil + expect(subject.last['Last_Month_Available']).to be_nil + end + end + end +end diff --git a/spec/models/sushi/server_status_spec.rb b/spec/models/sushi/server_status_spec.rb new file mode 100644 index 0000000000..7e899ecbb3 --- /dev/null +++ b/spec/models/sushi/server_status_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal:true + +RSpec.describe Sushi::ServerStatus do + describe '#server_status' do + subject { described_class.new(account: account).server_status } + + let(:account) { double(Account, cname: 'pitt.hyku.test') } + + it 'returns the correct format' do + expect(subject).to be_an_instance_of(Array) + expect(subject.length).to eq(1) + end + + it 'has the expected keys' do + expect(subject.first).to have_key('Description') + expect(subject.first).to have_key('Service_Active') + expect(subject.first).to have_key('Registry_Record') + end + + it 'returns the expected values' do + expect(subject.first['Description']).to eq("COUNTER Usage Reports for #{account.cname} platform.") + expect(subject.first['Service_Active']).to eq(true) + expect(subject.first['Registry_Record']).to eq("") + end + end +end diff --git a/spec/models/sushi_spec.rb b/spec/models/sushi_spec.rb new file mode 100644 index 0000000000..835e3d3821 --- /dev/null +++ b/spec/models/sushi_spec.rb @@ -0,0 +1,277 @@ +# frozen_string_literal:true + +RSpec.describe Sushi do + describe '#coerce_to_date' do + subject { described_class.coerce_to_date(given_date) } + + context 'with 2023-02-01' do + let(:given_date) { '2023-02-01' } + + it { is_expected.to eq(Date.new(2023, 2, 1)) } + end + context 'with 2023-05' do + let(:given_date) { '2023-05' } + + it { is_expected.to eq(Date.new(2023, 5, 1)) } + end + context 'with 2023' do + let(:given_date) { '2023' } + + it 'will raise an error' do + expect { subject }.to raise_exception(Sushi::Error::InvalidDateArgumentError) + end + end + end + + describe '.first_month_available' do + subject { described_class.first_month_available } + + let(:entry) do + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Book', + work_id: '12345', + date: entry_date, + total_item_investigations: 1, + total_item_requests: 10 + ) + end + + context 'when first entry date is middle of the month and current date is end of this month' do + let(:current_date) { Time.zone.today.end_of_month } + let(:entry_date) { 10.days.ago(current_date) } + + before { entry } + + it 'raises an error' do + expect { subject }.to raise_error(Sushi::Error::UsageNotReadyForRequestedDatesError) + end + end + + context 'when first entry is the middle of two months ago and current date is middle of this month' do + let(:current_date) { 10.days.ago(Time.zone.today.end_of_month) } + let(:entry_date) { 10.days.ago(2.months.ago(Time.zone.today.end_of_month)) } + + before { entry } + + it 'will return the beginning of the month of the earliest entry' do + expect(subject).to eq(entry_date.beginning_of_month) + end + end + + context 'when there are no entries' do + it 'raises an error' do + expect { subject }.to raise_error(Sushi::Error::UsageNotReadyForRequestedDatesError) + end + end + end + + describe '.last_month_available' do + subject { described_class.last_month_available } + + let(:entry) do + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Book', + work_id: '12345', + date: entry_date, + total_item_investigations: 1, + total_item_requests: 10 + ) + end + let(:create_older_entry) do + # Because sometimes we nee + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Book', + work_id: '12345', + date: 2.years.ago, + total_item_investigations: 1, + total_item_requests: 10 + ) + end + + context 'when we have one month of data and the current date is within that month' do + let(:current_date) { Time.zone.today.end_of_month } + let(:entry_date) { 5.days.ago(Time.zone.today.end_of_month) } + + before { entry } + + it 'raises an error' do + expect { subject }.to raise_error(Sushi::Error::UsageNotReadyForRequestedDatesError) + end + end + + context 'when last entry date is middle of the month and current date is end of this month' do + let(:current_date) { Time.zone.today.end_of_month } + let(:entry_date) { 10.days.ago(current_date) } + + before do + create_older_entry + entry + end + it 'will use the end of the previous current month' do + expect(subject).to eq(1.month.ago(current_date).end_of_month) + end + end + + context 'when last entry is the middle of two months ago and current date is middle of this month' do + let(:current_date) { 10.days.ago(Time.zone.today.end_of_month) } + let(:entry_date) { 10.days.ago(2.months.ago(Time.zone.today.end_of_month)) } + + before do + create_older_entry + entry + end + + it 'will return the end of the month of the last entry' do + expect(subject).to eq(entry_date.end_of_month) + end + end + + context 'when last entry date is beginning of the month and current entry date is beginning of the month' do + let(:current_date) { Time.zone.today.beginning_of_month } + let(:entry_date) { Time.zone.today.beginning_of_month } + + before do + create_older_entry + entry + end + + it 'will return the end of the month prior to the last entry' do + expect(subject).to eq(1.month.ago(current_date).end_of_month) + end + end + + context 'when there are no entries' do + it 'raises an error' do + expect { subject }.to raise_error(Sushi::Error::UsageNotReadyForRequestedDatesError) + end + end + end + + describe '#validate_date_format' do + context 'when the begin_date or end_date are in MM-YYYY format' do + let(:dates) { ['06-2023', '08-2023'] } + + it 'raises an error' do + expect { subject.validate_date_format(dates) }.to raise_error(Sushi::Error::InvalidDateArgumentError) + end + end + + context 'when the begin_date or end_date are in YYYY-MM-DD format' do + let(:dates) { ['2023-06-05', '2023-08-09'] } + + before { Sushi.info = [] } + + it 'stores the exception' do + expect(Sushi.info).to be_empty + subject.validate_date_format(dates) + expect(Sushi.info).not_to be_empty + expect(Sushi.info.first).to be_a(Hash) + end + end + end + + describe "AuthorCoercion" do + describe '.deserialize' do + subject { Sushi::AuthorCoercion.deserialize(string) } + + [ + ["|Hello|World|", ["Hello", "World"]], + ["|Hello|", ["Hello"]], + ["||", []], + ["", []], + [nil, []] + ].each do |given_authors, expected_array| + context "with #{given_authors.inspect}" do + let(:string) { given_authors } + + it { is_expected.to match_array(expected_array) } + end + end + end + + describe '.serialize' do + subject { Sushi::AuthorCoercion.serialize(array) } + + [ + [["Hello", "World"], "|Hello|World|"], + [["Hello"], "|Hello|"], + [[], nil] + ].each do |given_authors, expected| + context "with #{given_authors.inspect}" do + let(:array) { given_authors } + + it { is_expected.to eq(expected) } + end + end + end + + describe '#coerce_author' do + let(:klass) do + Class.new do + include Sushi::AuthorCoercion + def initialize(params = {}) + coerce_author(params) + end + end + end + + it "raises a 3060 error when given a #{Sushi::AuthorCoercion::DELIMITER} character" do + expect { klass.new(author: "Hello#{Sushi::AuthorCoercion::DELIMITER}World") }.to raise_error(Sushi::Error::InvalidReportFilterValueError) + end + end + end + + describe "YearOfPublicationCoercion" do + subject(:instance) { klass.new(params) } + + let(:klass) do + Class.new do + include Sushi::YearOfPublicationCoercion + def initialize(params = {}) + coerce_yop(params) + end + end + end + + let(:params) { {} } + + it { is_expected.to respond_to(:yop_as_where_parameters) } + + context 'when no :yop is provided' do + its(:yop_as_where_parameters) { is_expected.to be_falsey } + end + + # rubocop:disable Metrics/LineLength + [ + ['2003', ["((year_of_publication = ?))", 2003]], + ['2003-2005', ["((year_of_publication >= ? AND year_of_publication <= ?))", 2003, 2005]], + ['2003a-2005', Sushi::Error::InvalidDateArgumentError], + ['-1', ["((year_of_publication = ?))", -1]], + ['a-1', Sushi::Error::InvalidDateArgumentError], + ['a1', Sushi::Error::InvalidDateArgumentError], + ['1-2 3', Sushi::Error::InvalidDateArgumentError], + ['1 2', Sushi::Error::InvalidDateArgumentError], + ['1996-1994', ["((year_of_publication >= ? AND year_of_publication <= ?))", 1996, 1994]], + ['1996-1994 | 1999-2003|9a', Sushi::Error::InvalidDateArgumentError], + ['1996-1994 | 1299-2003|9-12', ["((year_of_publication >= ? AND year_of_publication <= ?) OR (year_of_publication >= ? AND year_of_publication <= ?) OR (year_of_publication >= ? AND year_of_publication <= ?))", 1996, 1994, 1299, 2003, 9, 12]], + ['1994-1996 | 1989', ["((year_of_publication >= ? AND year_of_publication <= ?) OR (year_of_publication = ?))", 1994, 1996, 1989]] + ].each do |given_yop, expected| + context "when given :yop parameter is #{given_yop.inspect}" do + let(:params) { { yop: given_yop } } + + if expected.is_a?(Array) + its(:yop) { is_expected.to eq(given_yop) } + its(:yop_as_where_parameters) { is_expected.to match_array(expected) } + else + it "raises an #{expected}" do + expect { subject }.to raise_error(expected) + end + end + end + end + # rubocop:enable Metrics/LineLength + end +end diff --git a/spec/requests/api/sushi_spec.rb b/spec/requests/api/sushi_spec.rb new file mode 100644 index 0000000000..1bccfec4e2 --- /dev/null +++ b/spec/requests/api/sushi_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +RSpec.describe 'api/sushi/r51', type: :request, singletenant: true do + before { create_hyrax_countermetric_objects } + let(:required_parameters) do + { + begin_date: '2022-01', + end_date: '2022-02' + } + end + + RSpec.shared_examples 'without required parameters' do |endpoint| + it 'returns a 422 unprocessable entity' do + get "/api/sushi/r51/reports/#{endpoint}" + expect(response).to have_http_status(422) + end + end + + describe 'GET /api/sushi/r51/reports/ir' do + it_behaves_like 'without required parameters', 'ir' + + it 'returns a 200 with correct response for item report' do + get '/api/sushi/r51/reports/ir', params: required_parameters + expect(response).to have_http_status(200) + parsed_body = JSON.parse(response.body) + expect(parsed_body.dig('Report_Header', 'Report_Name')).to eq('Item Report') + end + + context 'with a valid item_id parameter' do + it 'returns a 200 status report for the given item' do + get '/api/sushi/r51/reports/ir', params: { **required_parameters, item_id: '54321' } + expect(response).to have_http_status(200) + parsed_body = JSON.parse(response.body) + expect(parsed_body['Report_Items']).to be_instance_of(Array) + end + end + + context 'with an invalid item_id parameter' do + it 'returns a 422 status report for the given item' do + get '/api/sushi/r51/reports/ir', params: { **required_parameters, item_id: 'qwerty123' } + expect(response).to have_http_status(422) + parsed_body = JSON.parse(response.body) + + expect(parsed_body['Code']).to eq(3060) + end + end + end + + describe 'GET /api/sushi/r51/reports/pr (e.g. platform report)' do + it_behaves_like 'without required parameters', 'pr' + + it 'returns a 200 status' do + get '/api/sushi/r51/reports/pr', params: required_parameters + expect(response).to have_http_status(200) + parsed_body = JSON.parse(response.body) + expect(parsed_body.dig('Report_Header', 'Report_Name')).to eq('Platform Report') + end + end + + describe 'GET /api/sushi/r51/reports/pr_p1 (e.g. platform usage report)' do + it_behaves_like 'without required parameters', 'pr_p1' + + it 'returns a 200 status' do + get '/api/sushi/r51/reports/pr_p1', params: required_parameters + expect(response).to have_http_status(200) + parsed_body = JSON.parse(response.body) + expect(parsed_body.dig('Report_Header', 'Report_Name')).to eq('Platform Usage') + end + end + + describe 'GET /api/sushi/r51/status (e.g. platform status)' do + it 'returns a 200 status' do + get '/api/sushi/r51/status' + expect(response).to have_http_status(200) + parsed_body = JSON.parse(response.body) + expect(parsed_body.dig(0, 'Description')).to include('COUNTER Usage Reports') + end + end + + describe 'GET /api/sushi/r51/reports (e.g. reports list)' do + it 'returns a 200 status' do + get '/api/sushi/r51/reports' + expect(response).to have_http_status(200) + parsed_body = JSON.parse(response.body) + expect(parsed_body.first).to have_key('Report_Name') + expect(parsed_body.first).to have_key('Report_ID') + expect(parsed_body.first).to have_key('Release') + expect(parsed_body.first).to have_key('Report_Description') + expect(parsed_body.first).to have_key('Path') + expect(parsed_body.last).to have_key('First_Month_Available') + expect(parsed_body.last).to have_key('Last_Month_Available') + end + end +end diff --git a/spec/services/import_counter_metrics_spec.rb b/spec/services/import_counter_metrics_spec.rb new file mode 100644 index 0000000000..3d1052f6d9 --- /dev/null +++ b/spec/services/import_counter_metrics_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe ImportCounterMetrics do + # TODO: Write more robust tests for this service + it 'creates Hyrax::CounterMetrics with investigations' do + ImportCounterMetrics.import_investigations('spec/fixtures/csv/pittir-views.csv') + expect(Hyrax::CounterMetric.count).not_to be_nil + end + + it 'creates Hyrax::CounterMetrics with requests' do + ImportCounterMetrics.import_requests('spec/fixtures/csv/pittir-downloads.csv') + expect(Hyrax::CounterMetric.count).not_to be_nil + end +end From d0c451d714644225f758b0225a44bbcabb80d37a Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Tue, 9 Apr 2024 15:36:35 -0400 Subject: [PATCH 02/77] =?UTF-8?q?=F0=9F=9A=A7=20Partial=20add=20of=20PALs?= =?UTF-8?q?=20featuers=20to=20Hyku=20Prime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/javascripts/application.js | 4 +- app/assets/stylesheets/single_signon.scss | 33 +++++--- .../themes/cultural_repository.scss | 10 +-- .../themes/neutral_repository.scss | 4 +- app/assets/stylesheets/variables.scss | 3 +- app/controllers/application_controller.rb | 3 +- .../profiles_controller_decorator.rb | 5 ++ .../identity_providers_controller.rb | 12 +++ .../proprietor/accounts_controller.rb | 2 + .../proprietor/users_controller.rb | 6 ++ .../users/omniauth_callbacks_controller.rb | 9 ++- app/forms/video_embed_form_behavior.rb | 2 - app/helpers/google_tag_manager_helper.rb | 4 +- app/helpers/hyku_helper.rb | 2 + app/helpers/hyrax/iiif_helper.rb | 33 ++++++++ app/helpers/hyrax_helper.rb | 26 ++++++ app/helpers/pdf_js_helper.rb | 2 +- app/indexers/app_indexer.rb | 43 ++++++++++ app/indexers/collection_indexer.rb | 5 +- app/models/account.rb | 22 ++++++ app/models/concerns/video_embed_behavior.rb | 25 +++--- app/models/featured_collection_list.rb | 2 + app/models/nil_site.rb | 4 + app/models/solr_document.rb | 31 ++++++++ app/presenters/hyku/work_show_presenter.rb | 4 +- app/search_builders/adv_search_builder.rb | 3 + .../hyrax/workflow/permission_grantor.rb | 2 + app/views/admin/work_types/edit.html.erb | 4 +- .../_range_limit_panel.html.erb | 4 +- .../omniauth_callbacks/complete.html.erb | 2 +- .../appearances/_banner_image_form.html.erb | 69 ++++++++++------ app/views/hyrax/base/_work_description.erb | 1 + .../_featured_collection_fields.html.erb | 2 +- app/views/proprietor/users/_form.html.erb | 4 +- app/views/single_signon/index.html.erb | 6 ++ .../catalog/_search_form.html.erb | 3 + .../homepage/_explore_collections.html.erb | 2 +- .../hyrax/homepage/_featured_fields.html.erb | 2 +- .../hyrax/homepage/_recent_document.html.erb | 9 ++- .../layouts/hyrax.html.erb | 4 +- .../hyrax/base/_work_title.html.erb | 4 +- .../image_show/hyrax/base/show.html.erb | 3 + .../_user_util_links.html.erb | 1 + .../catalog/_search_form.html.erb | 3 + .../homepage/_explore_collections.html.erb | 2 +- .../layouts/hyrax.html.erb | 4 +- .../_featured_carousel.html.erb | 2 +- .../homepage/_explore_collections.html.erb | 2 +- .../hyrax/base/_work_title.html.erb | 4 +- .../metadata_format/hyku_dublin_core.rb | 34 +++++--- lib/tasks/collection_type_global_id.rake | 31 ++++++++ lib/tasks/index.rake | 36 ++------- .../hyrax/my/works_controller_spec.rb | 26 ++++++ spec/features/oai_pmh_spec.rb | 17 ++-- spec/indexers/app_indexer_spec.rb | 58 ++++++++++++++ spec/jobs/create_derivatives_job_spec.rb | 79 +++++++++++++++++++ spec/models/account_spec.rb | 2 + spec/models/featured_collection_list_spec.rb | 22 ++++++ spec/models/redis_endpoint_spec.rb | 2 + spec/models/site_spec.rb | 31 ++++++++ spec/support/helpers.rb | 76 ++++++++++++++++++ 61 files changed, 723 insertions(+), 129 deletions(-) create mode 100644 app/controllers/hyrax/dashboard/profiles_controller_decorator.rb create mode 100644 app/helpers/hyrax/iiif_helper.rb create mode 100644 app/views/hyrax/base/_work_description.erb create mode 100644 lib/tasks/collection_type_global_id.rake create mode 100644 spec/controllers/hyrax/my/works_controller_spec.rb create mode 100644 spec/jobs/create_derivatives_job_spec.rb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 3461d31c65..cc77aba01a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -19,6 +19,9 @@ //= require jquery.dataTables //= require dataTables.bootstrap4 //= require cropper.min + +//= require dataTables/jquery.dataTables +//= require dataTables/bootstrap/3/jquery.dataTables.bootstrap //= require stat_slider //= require turbolinks //= require cocoon @@ -64,5 +67,4 @@ //= require blacklight_range_limit/range_limit_slider //= require bootstrap-slider //= require jquery.flot.js - //= require tinymce diff --git a/app/assets/stylesheets/single_signon.scss b/app/assets/stylesheets/single_signon.scss index b5cd1fd62f..c71359d26d 100644 --- a/app/assets/stylesheets/single_signon.scss +++ b/app/assets/stylesheets/single_signon.scss @@ -21,24 +21,39 @@ } .loader { - -webkit-animation: spin 2s linear infinite; // Safari - animation: spin 2s linear infinite; - border-radius: 50%; - border-top: 16px solid #0a1f61; border: 16px solid #f3f3f3; - color: #f3f3f3; - font-size: 11px; - height: 120px; - margin: 55px auto 150px; - text-indent: -99999em; + border-radius: 50%; + border-top: 16px solid #284a75; width: 120px; + height: 120px; + -webkit-animation: spin 2s linear infinite; /* Safari */ + animation: spin 2s linear infinite; } // Safari @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + +#splash { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 1); + z-index: 10000; + justify-content: center; + align-items: center; + flex-direction: column; + + &.active { + display: flex !important; + } +} diff --git a/app/assets/stylesheets/themes/cultural_repository.scss b/app/assets/stylesheets/themes/cultural_repository.scss index 811e8e7dc7..fc451c0c74 100644 --- a/app/assets/stylesheets/themes/cultural_repository.scss +++ b/app/assets/stylesheets/themes/cultural_repository.scss @@ -14,7 +14,7 @@ body.dashboard { padding-top: 100px !important; - @media (max-width: 992px) { + @media (max-width: $screen-md-min) { padding-top: 0 !important; } @@ -162,7 +162,7 @@ .facet-panel-background-color { background-color: #ffffff; - @media (max-width: 992px) { + @media (max-width: $screen-md-min) { background-color: transparent; } @@ -240,14 +240,14 @@ ////// Media Queries ////// - @media (min-width: 480px) and (max-width: 1199px) { + @media (min-width: $screen-xs-min) and (max-width: $screen-md-max) { .mt-reverse-mb { margin-bottom: 40px; margin-top: 0; } } - @media (max-width: 991px) { + @media (max-width: $screen-sm-max) { div.home_page_text.homepage-text-container { height: 0; margin-bottom: 30px; @@ -255,7 +255,7 @@ } } - @media screen and (max-width: 992px) { + @media screen and (max-width: $screen-md-min) { div.recently-uploaded:nth-of-type(2n+3), div.featured-works-6-column-layout:nth-of-type(2n+3), div.featured-works-4-column-layout:nth-of-type(2n+3) { diff --git a/app/assets/stylesheets/themes/neutral_repository.scss b/app/assets/stylesheets/themes/neutral_repository.scss index 26639ca41a..ec037fa896 100644 --- a/app/assets/stylesheets/themes/neutral_repository.scss +++ b/app/assets/stylesheets/themes/neutral_repository.scss @@ -152,13 +152,13 @@ ////// Media Queries ////// - @media (max-width: 992px) { + @media (max-width: $screen-md-min) { div.neutral-repository-collections:nth-of-type(2n+3) { clear: left; } } - @media (min-width: 992px) { + @media (min-width: $screen-md-min) { div.neutral-repository-collections:nth-of-type(3n+4) { clear: left; } diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss index 78d355674f..11210710e3 100644 --- a/app/assets/stylesheets/variables.scss +++ b/app/assets/stylesheets/variables.scss @@ -1,6 +1,7 @@ -@import url(https://fonts.googleapis.com/css?family=Lato:400,700,300); +@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Montserrat&display=swap'); $primary-font-family: "Lato", Helvetica, sans-serif; +$splash-font-family: "Montserrat", "Lato", Helvetica, sans-serif; $jumbotron-heading-font-size: 2.5em; $jumbotron-heading-color: #30373b; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4faea728e2..7b6145d9b7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base - include HykuHelper # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception, prepend: true @@ -17,6 +16,8 @@ class ApplicationController < ActionController::Base include Hyrax::ThemedLayoutController with_themed_layout '1_column' + include HykuHelper + helper_method :current_account, :admin_host?, :home_page_theme, :show_page_theme, :search_results_theme before_action :authenticate_if_needed before_action :require_active_account!, if: :multitenant? diff --git a/app/controllers/hyrax/dashboard/profiles_controller_decorator.rb b/app/controllers/hyrax/dashboard/profiles_controller_decorator.rb new file mode 100644 index 0000000000..896bd9d9ec --- /dev/null +++ b/app/controllers/hyrax/dashboard/profiles_controller_decorator.rb @@ -0,0 +1,5 @@ +# OVERRIDE FILE from Hyrax v5.0.0 + +## +# Ensure the current user matches the requested URL. +Hyrax::Dashboard::ProfilesController.before_action :users_match!, only: %i[show edit update] diff --git a/app/controllers/identity_providers_controller.rb b/app/controllers/identity_providers_controller.rb index 7656e9c6eb..6cac0d0052 100644 --- a/app/controllers/identity_providers_controller.rb +++ b/app/controllers/identity_providers_controller.rb @@ -37,6 +37,12 @@ def create format.json { render json: @identity_provider.errors, status: :unprocessable_entity } end end + rescue JSON::ParserError => e + @identity_provider.add_error(:options, "Invalid JSON #{e.message}") + respond_to do |format| + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @identity_provider.errors, status: :unprocessable_entity } + end end # PATCH/PUT /identity_providers/1 or /identity_providers/1.json @@ -53,6 +59,12 @@ def update format.json { render json: @identity_provider.errors, status: :unprocessable_entity } end end + rescue JSON::ParserError => e + @identity_provider.add_error(:options, "Invalid JSON #{e.message}") + respond_to do |format| + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @identity_provider.errors, status: :unprocessable_entity } + end end # DELETE /identity_providers/1 or /identity_providers/1.json diff --git a/app/controllers/proprietor/accounts_controller.rb b/app/controllers/proprietor/accounts_controller.rb index b801aad8d3..4b552f7c28 100644 --- a/app/controllers/proprietor/accounts_controller.rb +++ b/app/controllers/proprietor/accounts_controller.rb @@ -17,6 +17,8 @@ def index # GET /accounts/1 # GET /accounts/1.json def show + @users = User.accessible_by(current_ability) + add_breadcrumb t(:'hyrax.controls.home'), root_path add_breadcrumb t(:'hyrax.admin.sidebar.accounts'), proprietor_accounts_path add_breadcrumb @account.tenant, edit_proprietor_account_path(@account) diff --git a/app/controllers/proprietor/users_controller.rb b/app/controllers/proprietor/users_controller.rb index a3ba263b64..12fb974c5f 100644 --- a/app/controllers/proprietor/users_controller.rb +++ b/app/controllers/proprietor/users_controller.rb @@ -81,6 +81,12 @@ def destroy end end + # method uses user's id, not their user_key + def become + bypass_sign_in(User.find(params[:id])) + redirect_to root_url, notice: 'User changed successfully' + end + private def ensure_admin! diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 0509c6e408..4925b3caca 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -5,6 +5,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_before_action :verify_authenticity_token def callback + logger.info("=@=@=@=@ auth: #{request.env['omniauth.auth']}, params: #{params.inspect}") # Here you will need to implement your logic for processing the callback # for example, finding or creating a user @user = User.from_omniauth(request.env['omniauth.auth']) @@ -30,11 +31,11 @@ def callback alias saml callback def passthru - render status: :not_found, plain: 'Not found. Authentication passthru.' + render status: 404, plain: 'Not found. Authentication passthru.' end - # def failure - # #redirect_to root_path - # end + def failure + redirect_to root_path, flash: { error: 'Authentication Failed. Something is wrong with the SSO configuration.' } + end end end diff --git a/app/forms/video_embed_form_behavior.rb b/app/forms/video_embed_form_behavior.rb index 42725f00e6..f1b4843517 100644 --- a/app/forms/video_embed_form_behavior.rb +++ b/app/forms/video_embed_form_behavior.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # frozen_sting_literal: true module VideoEmbedFormBehavior diff --git a/app/helpers/google_tag_manager_helper.rb b/app/helpers/google_tag_manager_helper.rb index 87518a045d..6a9f6f70c3 100644 --- a/app/helpers/google_tag_manager_helper.rb +++ b/app/helpers/google_tag_manager_helper.rb @@ -5,7 +5,7 @@ def render_gtm_head(_host) return '' if current_account.gtm_id.blank? # rubocop:disable Rails/OutputSafety - <<~HTML.html_safe + <<-HTML.strip_heredoc.html_safe diff --git a/app/views/hyrax/base/_work_description.erb b/app/views/hyrax/base/_work_description.erb new file mode 100644 index 0000000000..d16592016e --- /dev/null +++ b/app/views/hyrax/base/_work_description.erb @@ -0,0 +1 @@ +<%# A Place for Overrides in Downstream Implementations; assumes that we receive the `presenter` local variable. %> diff --git a/app/views/hyrax/homepage/_featured_collection_fields.html.erb b/app/views/hyrax/homepage/_featured_collection_fields.html.erb index 70b1cfc490..50f6384752 100644 --- a/app/views/hyrax/homepage/_featured_collection_fields.html.erb +++ b/app/views/hyrax/homepage/_featured_collection_fields.html.erb @@ -10,7 +10,7 @@
<%= link_to [hyrax, featured] do %> - <%= featured.title.first %> + <%= markdown(featured.title.first) %> <% end %>
diff --git a/app/views/proprietor/users/_form.html.erb b/app/views/proprietor/users/_form.html.erb index 9809be55b8..f3a902d101 100644 --- a/app/views/proprietor/users/_form.html.erb +++ b/app/views/proprietor/users/_form.html.erb @@ -1,8 +1,8 @@
<%= f.input :display_name, label: 'Display Name' %> <%= f.input :email, required: true, hint: '' %> - <%= f.input :superadmin?, as: :boolean %> - <%= f.input :password, required: f.object.new_record? ? true : false %> + <%= f.input :isuperadmin?, as: :boolean %> + <%= f.input :password, required: f.object.new_record? ? true : false, hint: 'Only if creating or changing' %> <%= f.input :facebook_handle %> <%= f.input :twitter_handle %> <%= f.input :googleplus_handle %> diff --git a/app/views/single_signon/index.html.erb b/app/views/single_signon/index.html.erb index 4858ef235a..ce1bf103ad 100644 --- a/app/views/single_signon/index.html.erb +++ b/app/views/single_signon/index.html.erb @@ -1,3 +1,8 @@ +
+

Please wait while we redirect you...

+
+
+

Select a Single Sign On Provider

<% if devise_mapping.omniauthable? %> @@ -19,6 +24,7 @@ <% if @identity_providers.count == 1 %> <% end %> diff --git a/app/views/themes/cultural_repository/catalog/_search_form.html.erb b/app/views/themes/cultural_repository/catalog/_search_form.html.erb index df1b8dc81d..845a759035 100644 --- a/app/views/themes/cultural_repository/catalog/_search_form.html.erb +++ b/app/views/themes/cultural_repository/catalog/_search_form.html.erb @@ -16,11 +16,14 @@ + <%# OVERRIDE here to include the advanced search button in the search bar %> + <%= link_to "Advanced", "/advanced", class: 'btn btn-default', id: 'advanced-top-button' %> <% if current_user %>
- <%= render 'shared/footer' %> + <% unless controller.controller_name == 'splash' %> + <%= render 'shared/footer' %> + <% end %> <%= render '/shared/select_work_type_modal', create_work_presenter: @presenter&.create_work_presenter if @presenter&.draw_select_work_modal? %> <%= render 'shared/modal' %> diff --git a/app/views/themes/cultural_show/hyrax/base/_work_title.html.erb b/app/views/themes/cultural_show/hyrax/base/_work_title.html.erb index b083f85110..7cc53ddffa 100644 --- a/app/views/themes/cultural_show/hyrax/base/_work_title.html.erb +++ b/app/views/themes/cultural_show/hyrax/base/_work_title.html.erb @@ -2,11 +2,11 @@
<% if index == 0 %> -

<%= title %> +

<%= markdown(title) %> <%= presenter.permission_badge %> <%= presenter.workflow.badge %>

<% else %> -

<%= title %>

+

<%= markdown(title) %>

<% end %>
diff --git a/app/views/themes/image_show/hyrax/base/show.html.erb b/app/views/themes/image_show/hyrax/base/show.html.erb index 17475eaba9..23f0705723 100644 --- a/app/views/themes/image_show/hyrax/base/show.html.erb +++ b/app/views/themes/image_show/hyrax/base/show.html.erb @@ -29,6 +29,9 @@
<%= render 'pdf_js', file_set_presenter: pdf_file_set_presenter(@presenter) %>
+
+ <%= render 'work_description', presenter: @presenter %> +
<% else %>
<%= render 'representative_media', presenter: @presenter, viewer: false unless @presenter.universal_viewer? || @presenter.show_pdf_viewer? %> diff --git a/app/views/themes/institutional_repository/_user_util_links.html.erb b/app/views/themes/institutional_repository/_user_util_links.html.erb index 954b66c89b..b27812e576 100644 --- a/app/views/themes/institutional_repository/_user_util_links.html.erb +++ b/app/views/themes/institutional_repository/_user_util_links.html.erb @@ -11,6 +11,7 @@ <%= t("hyrax.toolbar.profile.sr_target") %> + <% end %> - <%= render 'shared/footer' %> + <% unless controller.controller_name == 'splash' %> + <%= render 'shared/footer' %> + <% end %> <%= render 'shared/modal' %> diff --git a/app/views/themes/neutral_repository/_featured_carousel.html.erb b/app/views/themes/neutral_repository/_featured_carousel.html.erb index a4fc53985b..a8522a3d86 100644 --- a/app/views/themes/neutral_repository/_featured_carousel.html.erb +++ b/app/views/themes/neutral_repository/_featured_carousel.html.erb @@ -17,7 +17,7 @@ diff --git a/app/views/themes/neutral_repository/hyrax/homepage/_explore_collections.html.erb b/app/views/themes/neutral_repository/hyrax/homepage/_explore_collections.html.erb index 1815009398..807a4e3574 100644 --- a/app/views/themes/neutral_repository/hyrax/homepage/_explore_collections.html.erb +++ b/app/views/themes/neutral_repository/hyrax/homepage/_explore_collections.html.erb @@ -7,7 +7,7 @@ <% end %>
<%= link_to [hyrax, featured_collection] do %> -

<%= featured_collection.title.first %>

+

<%= markdown(featured_collection.title.first) %>

<% end %>
diff --git a/app/views/themes/scholarly_show/hyrax/base/_work_title.html.erb b/app/views/themes/scholarly_show/hyrax/base/_work_title.html.erb index 3ecf7061b5..998be1d711 100644 --- a/app/views/themes/scholarly_show/hyrax/base/_work_title.html.erb +++ b/app/views/themes/scholarly_show/hyrax/base/_work_title.html.erb @@ -2,11 +2,11 @@
<% if index == 0 %> -

<%= title %> +

<%= markdown(title) %> <%= presenter.permission_badge %> <%= presenter.workflow.badge %>

<% else %> -

<%= title %>

+

<%= markdown(title) %>

<% end %>
diff --git a/lib/oai/provider/metadata_format/hyku_dublin_core.rb b/lib/oai/provider/metadata_format/hyku_dublin_core.rb index 20040267c6..8b96c677c7 100644 --- a/lib/oai/provider/metadata_format/hyku_dublin_core.rb +++ b/lib/oai/provider/metadata_format/hyku_dublin_core.rb @@ -4,6 +4,13 @@ module OAI module Provider module MetadataFormat class HykuDublinCore < OAI::Provider::Metadata::Format + class_attribute :fields, default: %i[ + abstract access_right alternative_title based_near bibliographic_citation + contributor creator date_created date_modified date_uploaded depositor + description identifier keyword language license owner publisher related_url + resource_type rights_notes rights_statement source subject title + ], instance_accessor: false + # rubocop:disable Lint/MissingSuper def initialize # rubocop:enable Lint/MissingSuper @@ -12,14 +19,11 @@ def initialize @namespace = 'http://purl.org/dc/terms/' @element_namespace = 'hyku' - # Dublin Core Terms Fields - # For new fields, add here first then add to #map_oai_hyku - @fields = %i[ - abstract access_right alternative_title based_near bibliographic_citation - contributor creator date_created date_modified date_uploaded depositor - description identifier keyword language license owner publisher related_url - resource_type rights_notes rights_statement source subject title - ] + # Dublin Core Terms Fields For new fields, add here first then add to + # #map_oai_hyku on the model + # + # TODO: Can we derive these from the schema files? + @fields = self.class.fields end # Override to strip namespace and header out @@ -40,7 +44,9 @@ def encode(model, record) xml.tag! field.to_s, values end end + add_repository(xml, record) add_public_file_urls(xml, record) + add_thumbnail_url(xml, record) end xml.target! end @@ -66,7 +72,17 @@ def add_public_file_urls(xml, record) xml.tag! 'file_url', file_download_path end end - # rubocop:enable Metrics/MethodLength + + def add_thumbnail_url(xml, record) + return if record[:thumbnail_path_ss].blank? + thumbnail_url = "https://#{Site.instance.account.cname}#{record[:thumbnail_path_ss]}" + xml.tag! 'thumbnail_url', thumbnail_url + end + + def add_repository(xml, record) + repo_name = Site.application_name || record[:account_cname_tesim].first + xml.tag! 'repository', repo_name + end def header_specification { diff --git a/lib/tasks/collection_type_global_id.rake b/lib/tasks/collection_type_global_id.rake new file mode 100644 index 0000000000..3aeff5c517 --- /dev/null +++ b/lib/tasks/collection_type_global_id.rake @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +namespace :hyrax do + namespace :collections do + desc 'Update CollectionType global id references for Hyrax 3.0.0' + # Note: the definition of collection_type_gid= changed in Hyrax 5.0. This + # rake task is known to work for updates to versions prior to Hyrax 5. + # Use for later versions is unknown. + task update_collection_type_global_ids: :environment do + Account.find_each do |account| + next if account.search_only.eql? true + + puts "🎪🎪 Updating collection -> collection type GlobalId references within '#{account.name}' tenant" + AccountElevator.switch!(account.cname) + + count = 0 + + Collection.find_each do |collection| + next if collection.collection_type_gid == collection.collection_type.to_global_id.to_s + + collection.public_send(:collection_type_gid=, collection.collection_type.to_global_id, force: true) + + collection.save && + count += 1 + end + + puts "💯💯 Updated #{count} collections within '#{account.name}' tenant" + end + end + end +end diff --git a/lib/tasks/index.rake b/lib/tasks/index.rake index c02885af2d..8a105b3f7e 100644 --- a/lib/tasks/index.rake +++ b/lib/tasks/index.rake @@ -4,49 +4,29 @@ require 'ruby-progressbar' desc "reindex just the works in the background" task index_works: :environment do - Account.find_each do |account| - puts "=============== #{account.name}============" - next if account.name == "search" - switch!(account) - in_each_account do - ReindexWorksJob.perform_later - end + in_each_account do + ReindexWorksJob.perform_later end end desc "reindex just the collections in the background" task index_collections: :environment do - Account.find_each do |account| - puts "=============== #{account.name}============" - next if account.name == "search" - switch!(account) - in_each_account do - ReindexCollectionsJob.perform_later - end + in_each_account do + ReindexCollectionsJob.perform_later end end desc "reindex just the admin_sets in the background" task index_admin_sets: :environment do - Account.find_each do |account| - puts "=============== #{account.name}============" - next if account.name == "search" - switch!(account) - in_each_account do - ReindexAdminSetsJob.perform_later - end + in_each_account do + ReindexAdminSetsJob.perform_later end end desc "reindex just the file_sets in the background" task index_file_sets: :environment do - Account.find_each do |account| - puts "=============== #{account.name}============" - next if account.name == "search" - switch!(account) - in_each_account do - ReindexFileSetsJob.perform_later - end + in_each_account do + ReindexFileSetsJob.perform_later end end diff --git a/spec/controllers/hyrax/my/works_controller_spec.rb b/spec/controllers/hyrax/my/works_controller_spec.rb new file mode 100644 index 0000000000..52874d159e --- /dev/null +++ b/spec/controllers/hyrax/my/works_controller_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.describe Hyrax::My::WorksController, type: :controller do + describe "#configure_facets" do + subject { controller.blacklight_config.sort_fields.keys } + + let(:expected_sort_fields) do + [ + "date_uploaded_dtsi desc", + "date_uploaded_dtsi asc", + "date_modified_dtsi desc", + "date_modified_dtsi asc", + "system_create_dtsi desc", + "system_create_dtsi asc", + "depositor_ssi asc, title_ssi asc", + "depositor_ssi desc, title_ssi desc", + "creator_ssi asc, title_ssi asc", + "creator_ssi desc, title_ssi desc" + ] + end + + it "configures the custom sort fields" do + expect(subject).to match_array(expected_sort_fields) + end + end +end diff --git a/spec/features/oai_pmh_spec.rb b/spec/features/oai_pmh_spec.rb index 99b9eaeb91..cc7ecb6742 100644 --- a/spec/features/oai_pmh_spec.rb +++ b/spec/features/oai_pmh_spec.rb @@ -6,6 +6,12 @@ let(:identifier) { work.id } before do + # We use Site.instance.account.cname to build the download links. + # In the test ENV, Site.instance.account is nil. + account = Account.create(name: 'test', cname: 'test.example.com') + account.sites << Site.instance + account.save + login_as(user, scope: :user) work end @@ -21,19 +27,19 @@ context "with the #{metadata_prefix} prefix" do it 'retrieves a list of records' do visit oai_catalog_path(verb: 'ListRecords', metadataPrefix: metadata_prefix) - expect(page).to have_content("hyku:#{identifier}") + expect(page).to have_content("#{Settings.oai.prefix}:#{identifier}") expect(page).to have_content(work.title.first) end it 'retrieves a single record' do - visit oai_catalog_path(verb: 'GetRecord', metadataPrefix: metadata_prefix, identifier:) - expect(page).to have_content("hyku:#{identifier}") + visit oai_catalog_path(verb: 'GetRecord', metadataPrefix: metadata_prefix, identifier: identifier) + expect(page).to have_content("#{Settings.oai.prefix}:#{identifier}") expect(page).to have_content(work.title.first) end it 'retrieves a list of identifiers' do visit oai_catalog_path(verb: 'ListIdentifiers', metadataPrefix: metadata_prefix) - expect(page).to have_content("hyku:#{identifier}") + expect(page).to have_content("#{Settings.oai.prefix}:#{identifier}") expect(page).not_to have_content(work.title.first) end end @@ -49,7 +55,8 @@ work.save visit oai_catalog_path(verb: 'ListRecords', metadataPrefix: metadata_prefix) - expect(page).to have_content("oai:hyku:#{identifier}") + + expect(page).to have_content("#{Settings.oai.prefix}:#{identifier}") expect(page).to have_content(work.title.first) expect(page).to have_content('asdf') expect(page).to have_content('fdsa') diff --git a/spec/indexers/app_indexer_spec.rb b/spec/indexers/app_indexer_spec.rb index 6bab43f46d..b1850b5cbc 100644 --- a/spec/indexers/app_indexer_spec.rb +++ b/spec/indexers/app_indexer_spec.rb @@ -24,4 +24,62 @@ expect(solr_document.fetch("account_cname_tesim")).to eq(account.cname) end end + + describe "#generate_solr_document" do + context "when given a date with a YYYY-MM-DD format" do + it "indexes date_ssi in YYYY-MM-DD format" do + work.date_created = ["2024-01-01"] + expect(solr_document.fetch("date_ssi")).to eq("2024-01-01") + end + end + + context "when given a date with a YYYY-MM format" do + it "indexes date_ssi in YYYY-MM format" do + work.date_created = ["2024-01"] + expect(solr_document.fetch("date_ssi")).to eq("2024-01") + end + end + + context "when given a date with a YYYY format" do + it "indexes date_ssi in YYYY format" do + work.date_created = ["2024"] + expect(solr_document.fetch("date_ssi")).to eq("2024") + end + end + + context "when given a date with a YYYY-M-D format" do + it "converts the date to YYYY-MM-DD format and indexes date_ssi" do + work.date_created = ["2024-1-1"] + expect(solr_document.fetch("date_ssi")).to eq("2024-01-01") + end + end + + context "when given a date with a YYYY-M format" do + it "converts the date to YYYY-MM format and indexes date_ssi" do + work.date_created = ["2024-1"] + expect(solr_document.fetch("date_ssi")).to eq("2024-01") + end + end + + context "when given a date with a YYYY-MM-D format" do + it "converts the date to YYYY-MM-DD format and indexes date_ssi" do + work.date_created = ["2024-01-1"] + expect(solr_document.fetch("date_ssi")).to eq("2024-01-01") + end + end + + context "when given a date with a YYYY-M-DD format" do + it "converts the date to YYYY-M-DD format and indexes date_ssi" do + work.date_created = ["2024-1-01"] + expect(solr_document.fetch("date_ssi")).to eq("2024-01-01") + end + end + + context "when given a date with an invalid format" do + it "indexes the given date" do + work.date_created = ["Jan 1, 2024"] + expect(solr_document.fetch("date_ssi")).to eq("Jan 1, 2024") + end + end + end end diff --git a/spec/jobs/create_derivatives_job_spec.rb b/spec/jobs/create_derivatives_job_spec.rb new file mode 100644 index 0000000000..d2ff943b4c --- /dev/null +++ b/spec/jobs/create_derivatives_job_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.describe CreateDerivativesJob do + around do |example| + ffmpeg_enabled = Hyrax.config.enable_ffmpeg + Hyrax.config.enable_ffmpeg = true + example.run + Hyrax.config.enable_ffmpeg = ffmpeg_enabled + end + + describe 'recreating self as a CreateLargeDerivativesJob' do + let(:id) { '123' } + let(:file_set) { FileSet.new } + let(:file) do + Hydra::PCDM::File.new.tap do |f| + f.content = 'foo' + f.original_name = filename + f.save! + end + end + + before do + allow(FileSet).to receive(:find).with(id).and_return(file_set) + allow(file_set).to receive(:mime_type).and_return(mime_type) + allow(file_set).to receive(:id).and_return(id) + # Short-circuit irrelevant logic + allow(file_set).to receive(:reload) + allow(file_set).to receive(:update_index) + end + + context 'with an image file' do + let(:mime_type) { 'image/jpeg' } + let(:filename) { 'picture.jpg' } + + before do + # Short-circuit irrelevant logic + allow(Hydra::Derivatives::ImageDerivatives).to receive(:create) + end + + it 'does not recreate as a CreateLargeDerivativesJob' do + expect(CreateLargeDerivativesJob).not_to receive(:perform_later) + + described_class.perform_now(file_set, file.id) + end + end + + context 'with an video file' do + let(:mime_type) { 'video/mp4' } + let(:filename) { 'video.mp4' } + + before do + # Short-circuit irrelevant logic + allow(Hydra::Derivatives::VideoDerivatives).to receive(:create) + end + + it 'recreates as a CreateLargeDerivativesJob' do + expect(CreateLargeDerivativesJob).to receive(:perform_later) + + described_class.perform_now(file_set, file.id) + end + end + + context 'with an audio file' do + let(:mime_type) { 'audio/x-wav' } + let(:filename) { 'audio.wav' } + + before do + # Short-circuit irrelevant logic + allow(Hydra::Derivatives::AudioDerivatives).to receive(:create) + end + + it 'recreates as a CreateLargeDerivativesJob' do + expect(CreateLargeDerivativesJob).to receive(:perform_later) + + described_class.perform_now(file_set, file.id) + end + end + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 9fddb01934..977bf28a0a 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -82,6 +82,7 @@ allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with('HOST').and_return('system-host') expect(described_class.admin_host).to eq 'system-host' + allow(ENV).to receive(:[]).and_call_original # "un-stub" ENV end it 'falls back to localhost' do @@ -90,6 +91,7 @@ allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with('HOST').and_return(nil) expect(described_class.admin_host).to eq 'localhost' + allow(ENV).to receive(:[]).and_call_original # "un-stub" ENV end end diff --git a/spec/models/featured_collection_list_spec.rb b/spec/models/featured_collection_list_spec.rb index bc0bc6806c..fafa60d05f 100644 --- a/spec/models/featured_collection_list_spec.rb +++ b/spec/models/featured_collection_list_spec.rb @@ -3,6 +3,8 @@ require 'rails_helper' RSpec.describe FeaturedCollectionList, type: :model do + subject { described_class.new } + let(:user) { create(:user).tap { |u| u.add_role(:admin, Site.instance) } } let(:account) { create(:account) } let(:collection1) { create(:collection, user:) } @@ -35,6 +37,26 @@ expect(presenter.id).to eq collection2.id end end + + context 'when sorting the featured collections' do + let(:instance) { described_class.new } + + context 'when the featured collections have not been manually ordered' do + it 'is sorted by title' do + allow(instance).to receive(:manually_ordered?).and_return(false) + + expect(instance.featured_collections.map(&:presenter).map(&:title).flatten).to eq [collection1.title.first, collection2.title.first] + end + end + + context 'when the featured collections have been manually ordered' do + it 'is not sorted by title' do + allow(instance).to receive(:manually_ordered?).and_return(true) + + expect(instance.featured_collections.map(&:presenter).map(&:title).flatten).to eq [collection2.title.first, collection1.title.first] + end + end + end end describe '#featured_collections_attributes=' do diff --git a/spec/models/redis_endpoint_spec.rb b/spec/models/redis_endpoint_spec.rb index ea877b6db9..9ada227064 100644 --- a/spec/models/redis_endpoint_spec.rb +++ b/spec/models/redis_endpoint_spec.rb @@ -10,6 +10,8 @@ subject { described_class.new(namespace:) } describe '.options' do + subject { described_class.new namespace: namespace } + it 'uses the configured application settings' do expect(subject.options[:namespace]).to eq namespace end diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb index 4664911332..1b66f77924 100644 --- a/spec/models/site_spec.rb +++ b/spec/models/site_spec.rb @@ -91,4 +91,35 @@ end end end + + describe '#institution_label' do + let(:site) { FactoryBot.create(:site) } + + before do + allow(Site).to receive(:instance).and_return(site) + end + + context 'when institution_name is present' do + before do + allow(site).to receive(:institution_name).and_return('My University') + end + + it 'returns the custom institution label' do + expect(site.institution_label).to eq 'My University' + end + end + + context 'when institution_name is not present' do + let(:account) { instance_double("Account", cname: 'myuniversity.edu') } + + before do + allow(site).to receive(:institution_name).and_return(nil) + allow(site).to receive(:account).and_return(account) + end + + it 'returns the cname of the associated account' do + expect(site.institution_label).to eq 'myuniversity.edu' + end + end + end end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index 8b0544652b..b6d4edcfa5 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -26,3 +26,79 @@ def expect_additional_fields assert_select "input[name=?]", "user[arkivo_subscription]" assert_select "input[name=?]", "user[preferred_locale]" end + +# rubocop:disable Metrics/MethodLength +def create_hyrax_countermetric_objects + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Book', + work_id: '12345', + date: '2021-01-05', + author: '|Tubman, Harriet|', + year_of_publication: 2022, + total_item_investigations: 1, + total_item_requests: 10 + ) + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Book', + work_id: '12345', + date: '2022-01-05', + author: '|Tubman, Harriet|', + year_of_publication: 2022, + total_item_investigations: 1, + total_item_requests: 10 + ) + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Book', + work_id: '54321', + date: '2022-01-05', + author: '|X, Malcolm|', + year_of_publication: 2022, + total_item_investigations: 3, + total_item_requests: 5 + ) + # used to test the case where a hyrax countermetric has a unique date, but same work ID. + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Book', + work_id: '54321', + date: '2022-01-06', + author: '|X, Malcolm|', + year_of_publication: 2022, + total_item_investigations: 2, + total_item_requests: 4 + ) + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Article', + work_id: '98765', + date: '2023-08-09', + author: '|Washington, Booker T.|', + year_of_publication: 1999, + total_item_investigations: 2, + total_item_requests: 8 + ) + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Article', + work_id: '99999', + date: '2023-08-09', + author: '|Douglas, Frederick|', + year_of_publication: 1997, + total_item_investigations: 4, + total_item_requests: 3 + ) + Hyrax::CounterMetric.create( + worktype: 'GenericWork', + resource_type: 'Article', + work_id: '99999', + date: '2023-10-09', + author: '|Douglas, Frederick|', + year_of_publication: 1997, + total_item_investigations: 4, + total_item_requests: 3 + ) +end +# rubocop:enable Metrics/MethodLength From 212d6880e55417ff1e94d90cf50504be83b9d771 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Tue, 9 Apr 2024 16:54:19 -0400 Subject: [PATCH 03/77] =?UTF-8?q?=F0=9F=8E=81=20Add=20feature=20to=20treat?= =?UTF-8?q?=20some=20text=20as=20markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/application_helper.rb | 4 ++++ config/features.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f78f8de059..d4174f3011 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -48,6 +48,10 @@ def missing_translation(value, _options = {}) end def markdown(text) + return text unless FlipFlop.treat_some_user_inputs_as_markdown? + + # Consider extracting these options to a Hyku::Application + # configuration/class attribute. options = %i[ hard_wrap autolink no_intra_emphasis tables fenced_code_blocks disable_indented_code_blocks strikethrough lax_spacing space_after_headers diff --git a/config/features.rb b/config/features.rb index 5680699195..7db5d84ab8 100644 --- a/config/features.rb +++ b/config/features.rb @@ -33,4 +33,8 @@ feature :show_login_link, default: true, description: "Show General Login Link at Top Right of Page." + + feature :treat_some_user_inputs_as_markdown, + default: true, + description: "Treat some user inputs (e.g. titles and descriptions) as markdown." end From 1b873018ce679ab5b8f490666e51114db55cd0e0 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Wed, 10 Apr 2024 10:12:52 -0400 Subject: [PATCH 04/77] =?UTF-8?q?=F0=9F=8E=81=20Adding=20markdown=20render?= =?UTF-8?q?ing=20for=20many=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blacklight/facets_helper_behavior.rb | 12 ++++++ .../render_constraints_helper_behavior.rb | 12 ++++++ .../renderers/attribute_renderer_decorator.rb | 25 ++++++++++++ .../_index_header_list_collection.html.erb | 2 +- .../_index_header_list_default.html.erb | 9 ++--- .../catalog/_index_list_default.html.erb | 39 +++++++++++++++++++ .../appearances/_banner_image_form.html.erb | 2 +- app/views/hyrax/base/_work_title.erb | 15 +++++++ .../_collection_description.html.erb | 5 +++ app/views/hyrax/collections/show.html.erb | 2 +- .../collections/_collection_title.html.erb | 25 ++++++++++++ .../collections/_list_collections.html.erb | 2 +- .../_show_document_list_row.html.erb | 2 +- .../dashboard/works/_list_works.html.erb | 2 +- .../homepage/_explore_collections.html.erb | 2 +- .../hyrax/homepage/_featured_fields.html.erb | 16 +++++--- .../hyrax/homepage/_recent_document.html.erb | 32 ++++++++------- .../my/collections/_list_collections.html.erb | 2 +- app/views/hyrax/my/works/_list_works.html.erb | 22 +++++------ app/views/identity_providers/_form.html.erb | 2 +- .../hyrax/homepage/_recent_document.html.erb | 2 +- .../hyrax/base/_work_title.html.erb | 18 +++++++++ .../hyrax/homepage/_recent_document.html.erb | 10 +++-- .../hyrax/homepage/_recent_document.html.erb | 13 ++++--- config/features.rb | 8 ++++ 25 files changed, 224 insertions(+), 57 deletions(-) create mode 100644 app/helpers/blacklight/facets_helper_behavior.rb create mode 100644 app/helpers/blacklight/render_constraints_helper_behavior.rb create mode 100644 app/renderers/hyrax/renderers/attribute_renderer_decorator.rb create mode 100644 app/views/catalog/_index_list_default.html.erb create mode 100644 app/views/hyrax/base/_work_title.erb create mode 100644 app/views/hyrax/collections/_collection_description.html.erb create mode 100644 app/views/hyrax/dashboard/collections/_collection_title.html.erb create mode 100644 app/views/themes/image_show/hyrax/base/_work_title.html.erb diff --git a/app/helpers/blacklight/facets_helper_behavior.rb b/app/helpers/blacklight/facets_helper_behavior.rb new file mode 100644 index 0000000000..efc5f01452 --- /dev/null +++ b/app/helpers/blacklight/facets_helper_behavior.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# OVERRIDE Blacklight 6.7.0 to enable markdown in the facets + +require_dependency Blacklight::Engine.root.join('app', 'helpers', 'blacklight', 'facets_helper_behavior').to_s + +Blacklight::FacetsHelperBehavior.class_eval do + # OVERRIDE to enable markdown in the facet list + def render_facet_limit_list(paginator, facet_field, wrapping_element = :li) + safe_join(paginator.items.map { |item| markdown(render_facet_item(facet_field, item)) }.compact.map { |item| content_tag(wrapping_element, item) }) + end +end diff --git a/app/helpers/blacklight/render_constraints_helper_behavior.rb b/app/helpers/blacklight/render_constraints_helper_behavior.rb new file mode 100644 index 0000000000..976eda729f --- /dev/null +++ b/app/helpers/blacklight/render_constraints_helper_behavior.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# OVERRIDE Blacklight 6.7.0 to enable markdown for the facet constraints + +require_dependency Blacklight::Engine.root.join('app', 'helpers', 'blacklight', 'render_constraints_helper_behavior').to_s + +Blacklight::RenderConstraintsHelperBehavior.class_eval do + # OVERRIDE to enable markdown in the facet constraints + def render_constraint_element(label, value, options = {}) + render(partial: "catalog/constraints_element", locals: { label: label, value: markdown(value), options: options }) + end +end diff --git a/app/renderers/hyrax/renderers/attribute_renderer_decorator.rb b/app/renderers/hyrax/renderers/attribute_renderer_decorator.rb new file mode 100644 index 0000000000..0ffa5eff85 --- /dev/null +++ b/app/renderers/hyrax/renderers/attribute_renderer_decorator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.1 Enable markdown rendering on work show page metadata + +module Hyrax + module Renderers + module AttributeRendererDecorator + include ApplicationHelper + + private + + def attribute_value_to_html(value) + if field.to_s == 'abstract' + markdown(value) + elsif microdata_value_attributes(field).present? + "#{markdown(li_value(value))}" + else + markdown(li_value(value)) + end + end + end + end +end + +Hyrax::Renderers::AttributeRenderer.prepend(Hyrax::Renderers::AttributeRendererDecorator) diff --git a/app/views/catalog/_index_header_list_collection.html.erb b/app/views/catalog/_index_header_list_collection.html.erb index 62a809d258..290358347c 100644 --- a/app/views/catalog/_index_header_list_collection.html.erb +++ b/app/views/catalog/_index_header_list_collection.html.erb @@ -2,7 +2,7 @@
<%# OVERRIDE begin %> -

<%= link_to document.title_or_label, generate_work_url(document.to_h, request) %>

+

<%= link_to markdown(document.title_or_label), generate_work_url(document.to_h, request) %>

<%# OVERRIDE end %> <%= Hyrax::CollectionPresenter.new(document, current_ability).collection_type_badge %>
diff --git a/app/views/catalog/_index_header_list_default.html.erb b/app/views/catalog/_index_header_list_default.html.erb index bad9d9e9dc..2942252025 100644 --- a/app/views/catalog/_index_header_list_default.html.erb +++ b/app/views/catalog/_index_header_list_default.html.erb @@ -1,10 +1,7 @@ -<%# OVERRIDE Hyrax v5.0.0rc2 to support shared search %> -<% model = document.hydra_model %> +<%# OVERRIDE Hyrax v5.0.1 to collection badge and markdown of links %>
- <% if model == Hyrax::PcdmCollection || model < Hyrax::PcdmCollection %> -

<%= link_to document.title_or_label, generate_work_url(document, request) %>

+

<%= link_to markdown(document.title_or_label), generate_work_url(document, request) %>

+ <% if document.hydra_model == Hyrax.collection_model_class || document.hydra_model < Hyrax.collection_model_class %> <%= Hyrax::CollectionPresenter.new(document, current_ability).collection_type_badge %> - <% else %> -

<%= link_to document.title_or_label, generate_work_url(document, request) %>

<% end %>
diff --git a/app/views/catalog/_index_list_default.html.erb b/app/views/catalog/_index_list_default.html.erb new file mode 100644 index 0000000000..fd6bbb29b4 --- /dev/null +++ b/app/views/catalog/_index_list_default.html.erb @@ -0,0 +1,39 @@ +<%# OVERRIDE Hyrax 5.0.1 to enable markdown for index field values in search results %> +<%# OVERRIDE Hyrax 5.0.1 to handle search only accounts %> + +
+ +
+<% if document.collection? %> + <% collection_presenter = Hyrax::CollectionPresenter.new(document, current_ability) %> +
+
+
+ <%= collection_presenter.total_viewable_collections %>Collections +
+
+ <%= collection_presenter.total_viewable_works %>Works +
+
+
+<% end %> + diff --git a/app/views/hyrax/admin/appearances/_banner_image_form.html.erb b/app/views/hyrax/admin/appearances/_banner_image_form.html.erb index 9ffd0a23d3..fc2c82634f 100644 --- a/app/views/hyrax/admin/appearances/_banner_image_form.html.erb +++ b/app/views/hyrax/admin/appearances/_banner_image_form.html.erb @@ -8,7 +8,7 @@
- " alt="Banner Image Preview Area" class="img-responsive" /> + " alt="Banner Image Preview Area" class="img-fluid" />
<%= link_to collection_presenter.show_path do %> <%= t("hyrax.dashboard.my.sr.show_label") %> - <%= collection_presenter.title_or_label %> + <%= markdown(collection_presenter.title_or_label) %> <% end %> <%# Expand arrow %> diff --git a/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb b/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb index 63e156bad3..843448a66d 100644 --- a/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb +++ b/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb @@ -15,7 +15,7 @@ <% end %>

- <%= link_to document.title_or_label, [main_app, document], id: "src_copy_link#{id}", class: "#{'document-title' if document.title_or_label == document.label}", data: { turbolinks: block_valkyrie_redirect? } %> + <%= link_to markdown(document.title_or_label), [main_app, document], id: "src_copy_link#{id}", class: "#{'document-title' if document.title_or_label == document.label}", data: { turbolinks: block_valkyrie_redirect? } %>

<%= render_other_collection_links(document, @presenter.id) %> diff --git a/app/views/hyrax/dashboard/works/_list_works.html.erb b/app/views/hyrax/dashboard/works/_list_works.html.erb index 7baed509c9..d39ec50233 100644 --- a/app/views/hyrax/dashboard/works/_list_works.html.erb +++ b/app/views/hyrax/dashboard/works/_list_works.html.erb @@ -23,7 +23,7 @@ <%= t("hyrax.dashboard.my.sr.show_label") %> - <%= document.title_or_label %> + <%= markdown(document.title_or_label) %> <% end %>
diff --git a/app/views/hyrax/homepage/_explore_collections.html.erb b/app/views/hyrax/homepage/_explore_collections.html.erb index 77e343781e..f76355c3a8 100644 --- a/app/views/hyrax/homepage/_explore_collections.html.erb +++ b/app/views/hyrax/homepage/_explore_collections.html.erb @@ -14,7 +14,7 @@
<%= link_to [hyrax, featured_collection] do %> - <%= featured_collection.title.first %> + <%= markdown(featured_collection.title.first) %> <% end %>
diff --git a/app/views/hyrax/homepage/_featured_fields.html.erb b/app/views/hyrax/homepage/_featured_fields.html.erb index 181e4eb53c..b254cddbfe 100644 --- a/app/views/hyrax/homepage/_featured_fields.html.erb +++ b/app/views/hyrax/homepage/_featured_fields.html.erb @@ -10,10 +10,14 @@ <% end %>
- - + <% if FlipFlop.home_page_recent_document_show_depositor? %> + + <% end %> + <% if FlipFlop.home_page_recent_document_show_keyword? %> + + <% end %> diff --git a/app/views/hyrax/homepage/_recent_document.html.erb b/app/views/hyrax/homepage/_recent_document.html.erb index abb4047375..f796fc940d 100644 --- a/app/views/hyrax/homepage/_recent_document.html.erb +++ b/app/views/hyrax/homepage/_recent_document.html.erb @@ -1,18 +1,24 @@ -<%# OVERRIDE Hyrax 3.4.1 to allow links to support shared search and add ENV to control turbolinks %> +<%# OVERRIDE Hyrax 5.0.01 to allow links to support shared search %> +<%# OVERRIDE add conditional for controlling turbolinks %>
  • -
    -
    - <%= t('hyrax.homepage.recently_uploaded.document.title_label') %> -

    - <%= render_thumbnail_tag recent_document, {}, {suppress_link: true} %> - <%= link_to recent_document.title_or_label, generate_work_url(recent_document.to_h, request), data: { turbolinks: block_valkyrie_redirect? } %> -

    -

    +

    +
    + <%= t('hyrax.homepage.recently_uploaded.document.title_label') %> +

    + <%= link_to generate_work_url(recent_document.to_h, request), data: { turbolinks: block_valkyrie_redirect? } do %> + <%= markdown("#{render_thumbnail_tag(recent_document, {width: 90}, {suppress_link: true})} #{recent_document.title_or_label}") %> + <% end %> +

    + <% if FlipFlop.home_page_recent_document_show_depositor? %> +

    <%= t('hyrax.homepage.recently_uploaded.document.depositor_label') %>: <%= link_to_profile recent_document.depositor(t('hyrax.homepage.recently_uploaded.document.depositor_missing')) %> -

    -

    +

    + <% end %> + <% if FlipFlop.home_page_recent_document_show_keyword? %> +

    <%= t('hyrax.homepage.recently_uploaded.document.keyword_label') %>: <%= link_to_facet_list(recent_document.keyword, 'keyword', t('hyrax.homepage.recently_uploaded.document.keyword_missing')).html_safe %> -

    -
    +

    + <% end %>
    +
  • diff --git a/app/views/hyrax/my/collections/_list_collections.html.erb b/app/views/hyrax/my/collections/_list_collections.html.erb index 31efaec0fa..109c3208e7 100644 --- a/app/views/hyrax/my/collections/_list_collections.html.erb +++ b/app/views/hyrax/my/collections/_list_collections.html.erb @@ -34,7 +34,7 @@ <%= link_to collection_presenter.show_path, id: "src_copy_link#{id}" do %> <%= t("hyrax.dashboard.my.sr.show_label") %> - <%= collection_presenter.title_or_label %> + <%= markdown(collection_presenter.title_or_label) %> <% end %> <%# Expand arrow %> diff --git a/app/views/hyrax/my/works/_list_works.html.erb b/app/views/hyrax/my/works/_list_works.html.erb index 4631113911..5454040581 100644 --- a/app/views/hyrax/my/works/_list_works.html.erb +++ b/app/views/hyrax/my/works/_list_works.html.erb @@ -1,12 +1,10 @@ <%# OVERRIDE Hyrax v5.0.0rc2 to add appropriate alt tag and add ENV to control turbolinks%> - <%= render 'hyrax/batch_select/add_button', document: document %>  -
    <%# OVERRIDE begin %> @@ -18,23 +16,23 @@
    <%# OVERRIDE begin %> <%= link_to [main_app, document], id: "src_copy_link#{document.id}", class: 'document-title', data: { turbolinks: block_valkyrie_redirect? } do %> - <%# OVERRIDE end %> - - <%= t("hyrax.dashboard.my.sr.show_label") %> - - <%= document.title_or_label %> - <% end %> + <%# OVERRIDE end %> + + <%= t("hyrax.dashboard.my.sr.show_label") %> + + <%= markdown(document.title_or_label) %> + <% end %> -
    - <%= render_collection_links(document) %> +
    + <%= render_collection_links(document) %>
    <%= document.date_modified %> - + + <%= render_visibility_link document %> - <%= render 'work_action_menu', document: document %> diff --git a/app/views/identity_providers/_form.html.erb b/app/views/identity_providers/_form.html.erb index 499cc0fbe5..95553d56b4 100644 --- a/app/views/identity_providers/_form.html.erb +++ b/app/views/identity_providers/_form.html.erb @@ -44,7 +44,7 @@ <%# Upload Logo Image %> <%= f.input :logo_image, label: t('hyku.identity_provider.label.logo_image'), as: :file, wrapper: :vertical_file_input, hint: t('hyrax.admin.appearances.show.forms.logo_image.hint') %> <%= f.input :logo_image_text, label: t('hyku.identity_provider.label.logo_image_alt_text'), as: :text %> - <%= image_tag f.object.logo_image.url(:medium), class: "img-responsive", alt: f.object.logo_image_text if f.object.logo_image? %> + <%= image_tag f.object.logo_image.url(:medium), class: "img-fluid", alt: f.object.logo_image_text if f.object.logo_image? %> diff --git a/app/views/themes/cultural_repository/hyrax/homepage/_recent_document.html.erb b/app/views/themes/cultural_repository/hyrax/homepage/_recent_document.html.erb index 23fb317152..25484324e7 100644 --- a/app/views/themes/cultural_repository/hyrax/homepage/_recent_document.html.erb +++ b/app/views/themes/cultural_repository/hyrax/homepage/_recent_document.html.erb @@ -1,7 +1,7 @@ <%# OVERRIDE Hyrax v5.0.0rc2 template for client theming and shared search %>
    <%= link_to [main_app, recent_document] do %> - <%= render_thumbnail_tag(recent_document, {suppress_link: true}, class: 'img-responsive center-block') %> + <%= render_thumbnail_tag(recent_document, {suppress_link: true}, class: 'img-fluid center-block') %> <% end %>
    <%= link_to [main_app, recent_document] do %> diff --git a/app/views/themes/image_show/hyrax/base/_work_title.html.erb b/app/views/themes/image_show/hyrax/base/_work_title.html.erb new file mode 100644 index 0000000000..2678821a42 --- /dev/null +++ b/app/views/themes/image_show/hyrax/base/_work_title.html.erb @@ -0,0 +1,18 @@ +<% presenter.title.each_with_index do |title, index| %> +
    +
    + <% if index == 0 %> +

    <%= markdown(title) %> + <%= presenter.permission_badge %> <%= presenter.workflow.badge %> +

    + <% else %> +

    <%= markdown(title) %>

    + <% end %> +
    +
    + <% if index == 0 %> + <%= render "show_actions", presenter: presenter %> + <% end %> +
    +
    +<% end %> diff --git a/app/views/themes/institutional_repository/hyrax/homepage/_recent_document.html.erb b/app/views/themes/institutional_repository/hyrax/homepage/_recent_document.html.erb index 431647c0d6..b7e4758ad9 100644 --- a/app/views/themes/institutional_repository/hyrax/homepage/_recent_document.html.erb +++ b/app/views/themes/institutional_repository/hyrax/homepage/_recent_document.html.erb @@ -1,8 +1,10 @@ <%# OVERRIDE Hyrax v5.0.0rc2 template for client theming and shared search %>

    <%= t('hyrax.homepage.recently_uploaded.document.title_label') %>

    -
    - <%= render_thumbnail_tag recent_document, {class: 'img-fluid mx-auto d-block'}, {suppress_link: true} %> -
    - <%= link_to recent_document.title_or_label, generate_work_url(recent_document.to_h, request) %> +<%= link_to generate_work_url(recent_document.to_h, request) do %> +
    + <%= render_thumbnail_tag recent_document, {class: 'img-fluid mx-auto d-block'}, {suppress_link: true} %> +
    +

    <%= markdown(recent_document.title_or_label) %>

    +
    diff --git a/app/views/themes/neutral_repository/hyrax/homepage/_recent_document.html.erb b/app/views/themes/neutral_repository/hyrax/homepage/_recent_document.html.erb index b1f9747320..a3a0660bff 100644 --- a/app/views/themes/neutral_repository/hyrax/homepage/_recent_document.html.erb +++ b/app/views/themes/neutral_repository/hyrax/homepage/_recent_document.html.erb @@ -1,8 +1,9 @@ -<%# OVERRIDE Hyrax v5.0.0rc2 template for client theming and shared search %>

    <%= t('hyrax.homepage.recently_uploaded.document.title_label') %>

    -
    - <%= render_thumbnail_tag recent_document, {class: 'img-fluid mx-auto d-block'}, {suppress_link: true} %> -
    - <%= link_to recent_document.title_or_label, generate_work_url(recent_document.to_h, request) %> +<%= link_to(generate_work_url(recent_document.to_h, request)) do %> +
    + <%= render_thumbnail_tag recent_document, {class: 'img-fluid mx-auto d-block'}, {suppress_link: true} %> +
    +

    <%= markdown(recent_document.title_or_label) %>

    +
    -
    +<% end %> diff --git a/config/features.rb b/config/features.rb index 7db5d84ab8..2c01dca155 100644 --- a/config/features.rb +++ b/config/features.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true Flipflop.configure do + feature :home_page_recent_document_show_depositor, + default: false, # Default to false as this is PALNI/PALCI preference + description: "Shows the depositor of each homepage's recent documents." + + feature :home_page_recent_document_show_keyword, + default: false, # Default to false as this is PALNI/PALCI preference + description: "Shows the keywords of each homepage's recent documents." + feature :show_workflow_roles_menu_item_in_admin_dashboard_sidebar, default: false, description: "Shows the Workflow Roles menu item in the admin dashboard sidebar." From b913ec11b691c128c01512993e392082d7873a9e Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Wed, 10 Apr 2024 10:19:31 -0400 Subject: [PATCH 05/77] Add some quality of life functions to Hyku from PALs --- app/models/sort_title.rb | 27 +++++++++++++++++++++++++++ app/models/user.rb | 6 ++++++ spec/models/sort_title_spec.rb | 11 +++++++++++ 3 files changed, 44 insertions(+) create mode 100644 app/models/sort_title.rb create mode 100644 spec/models/sort_title_spec.rb diff --git a/app/models/sort_title.rb b/app/models/sort_title.rb new file mode 100644 index 0000000000..55ff31337c --- /dev/null +++ b/app/models/sort_title.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class SortTitle + def initialize(title) + @title = title + end + + def alphabetical + title = @title.downcase + title = title.gsub(/^an(?:[[:space:]])/, '') + .gsub(/^a(?:[[:space:]])/, '') + .gsub(/^the(?:[[:space:]])/, '') + .gsub(/"|'/, '') + title_elements = title.split(' ') + new_title = [] + title_elements.each do |element| + numbers = element.gsub(/[^\d]/, '') + unless numbers.empty? + zero_num = numbers.rjust(6, '0') + element = element.gsub(numbers, zero_num) + end + new_title.push(element) + end + title = new_title.join(' ') + title.strip + end +end diff --git a/app/models/user.rb b/app/models/user.rb index c81d0255e8..82f8dcfffd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -50,10 +50,16 @@ def admin? has_role?(:admin) || has_role?(:admin, Site.instance) end + # Favor admin? over is_admin? but provided for backwards compatability. + alias is_admin? admin? + def superadmin? has_role? :superadmin end + # Favor admin? over is_admin? but provided for backwards compatability. + alias is_superadmin? superadmin? + # This comes from a checkbox in the proprietor interface # Rails checkboxes are often nil or "0" so we handle that # case directly diff --git a/spec/models/sort_title_spec.rb b/spec/models/sort_title_spec.rb new file mode 100644 index 0000000000..729a93cfd1 --- /dev/null +++ b/spec/models/sort_title_spec.rb @@ -0,0 +1,11 @@ +RSpec.describe SortTitle, type: :model do + describe "Alphabetical" do + it "Titlecase title and remove 'The', 'And' and 'A'" do + expect(SortTitle.new("THE APPLE").alphabetical).to eq "apple" + expect(SortTitle.new("a apple").alphabetical).to eq "apple" + expect(SortTitle.new("An apple").alphabetical).to eq "apple" + expect(SortTitle.new("The A apple And An").alphabetical).to eq "a apple and an" + expect(SortTitle.new("LS575 Course with no name").alphabetical).to eq "ls000575 course with no name" + end + end +end From 62c79d41a3fcc6944897f65b6e76f3e80fe6f3b1 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Wed, 10 Apr 2024 10:58:44 -0400 Subject: [PATCH 06/77] This is now handled in Hyrax --- app/helpers/hyrax_helper.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/helpers/hyrax_helper.rb b/app/helpers/hyrax_helper.rb index bbe600c700..9275961ded 100644 --- a/app/helpers/hyrax_helper.rb +++ b/app/helpers/hyrax_helper.rb @@ -63,17 +63,4 @@ def display_hyrax_group_name(hyrax_group_name) label end - - ## - # OVERRIDE Hyrax::FileSetHelper#display_media_download_link? - # - # @return [Boolean] whether to display the download link for the given file - # set - def display_media_download_link?(*args) - if Site.account.settings[:allow_downloads].nil? || Site.account.settings[:allow_downloads].to_i.nonzero? - super - else - false - end - end end From 7145bdd9a6fbb33425f8235bd6736dde3ccc84aa Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Wed, 10 Apr 2024 11:10:57 -0400 Subject: [PATCH 07/77] Favor feature flag for login/authentication --- app/views/_user_util_links.html.erb | 24 +++++++++++-------- .../_user_util_links.html.erb | 24 +++++++++++-------- .../_user_util_links.html.erb | 24 +++++++++++-------- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb index db04aca6d3..820293b8a3 100644 --- a/app/views/_user_util_links.html.erb +++ b/app/views/_user_util_links.html.erb @@ -13,18 +13,22 @@ <% end %> <% else %> - + <% if Flipflop.show_login_link? %> + + <% end %> <% end %> diff --git a/app/views/themes/cultural_repository/_user_util_links.html.erb b/app/views/themes/cultural_repository/_user_util_links.html.erb index f0f71a6c54..0e38dc995d 100644 --- a/app/views/themes/cultural_repository/_user_util_links.html.erb +++ b/app/views/themes/cultural_repository/_user_util_links.html.erb @@ -25,18 +25,22 @@ <% end %> <% else %> - + <% if Flipflop.show_login_link? %> + + <% end %> <% end %> diff --git a/app/views/themes/institutional_repository/_user_util_links.html.erb b/app/views/themes/institutional_repository/_user_util_links.html.erb index b27812e576..ee1c9010cb 100644 --- a/app/views/themes/institutional_repository/_user_util_links.html.erb +++ b/app/views/themes/institutional_repository/_user_util_links.html.erb @@ -15,19 +15,23 @@ <% end %> <% else %> - + <% if Flipflop.show_login_link? %> + + <% end %> <% end %>
    From 99fd32b6259ef66815b666c89655e10b5871cc17 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Wed, 10 Apr 2024 14:28:10 -0400 Subject: [PATCH 08/77] Reviewing decorators that should have PALS contributions --- .../contact_form_controller_decorator.rb | 5 + .../hyrax/pages_controller_decorator.rb | 6 ++ .../hyrax/forms/admin/appearance_decorator.rb | 96 ++++++++++++++++--- .../hyrax/collection_presenter_decorator.rb | 6 ++ .../manifest_builder_service_decorator.rb | 6 ++ lib/oai/provider/model_decorator.rb | 29 ++++++ 6 files changed, 133 insertions(+), 15 deletions(-) diff --git a/app/controllers/hyrax/contact_form_controller_decorator.rb b/app/controllers/hyrax/contact_form_controller_decorator.rb index 63d7bef19a..0807fcfb72 100644 --- a/app/controllers/hyrax/contact_form_controller_decorator.rb +++ b/app/controllers/hyrax/contact_form_controller_decorator.rb @@ -43,6 +43,7 @@ def new @featured_work_list = FeaturedWorkList.new @featured_collection_list = FeaturedCollectionList.new @announcement_text = ContentBlock.for(:announcement) + ir_counts if home_page_theme == 'institutional_repository' end # rubocop:disable Metrics/AbcSize, Metrics/MethodLength @@ -69,6 +70,10 @@ def create private + def ir_counts + @ir_counts = get_facet_field_response('resource_type_sim', {}, "f.resource_type_sim.facet.limit" => "-1") + end + # OVERRIDE: return collections for theming # Return 6 collections, sorts by title def collections(rows: 6) diff --git a/app/controllers/hyrax/pages_controller_decorator.rb b/app/controllers/hyrax/pages_controller_decorator.rb index 3f470a4dba..702ca81560 100644 --- a/app/controllers/hyrax/pages_controller_decorator.rb +++ b/app/controllers/hyrax/pages_controller_decorator.rb @@ -43,6 +43,7 @@ def show @featured_work_list = FeaturedWorkList.new @featured_collection_list = FeaturedCollectionList.new @announcement_text = ContentBlock.for(:announcement) + ir_counts if home_page_theme == 'institutional_repository' end private @@ -58,6 +59,11 @@ def collections(rows: 6) [] end + # OVERRIDE: Hyrax v5.0.1 to add facet counts for resource types for IR theme + def ir_counts + @ir_counts = get_facet_field_response('resource_type_sim', {}, "f.resource_type_sim.facet.limit" => "-1") + end + # OVERRIDE: Adding to prepend the theme views into the view_paths def inject_theme_views if home_page_theme && home_page_theme != 'default_home' diff --git a/app/forms/hyrax/forms/admin/appearance_decorator.rb b/app/forms/hyrax/forms/admin/appearance_decorator.rb index 02a432e76c..04403b9803 100644 --- a/app/forms/hyrax/forms/admin/appearance_decorator.rb +++ b/app/forms/hyrax/forms/admin/appearance_decorator.rb @@ -106,37 +106,88 @@ def default_image_fields # rubocop:disable Metrics/MethodLength def customization_params %i[ + active_tabs_background_color + banner_image_text body_font - headline_font - header_and_footer_background_color - header_and_footer_text_color - link_color - link_hover_color - footer_link_color - footer_link_hover_color - primary_button_hover_color + collection_banner_text_color + custom_css_block default_button_background_color default_button_border_color default_button_text_color - active_tabs_background_color + default_collection_image_text + default_work_image_text + directory_image_alt_text + directory_image_text facet_panel_background_color facet_panel_text_color + footer_link_color + footer_link_hover_color + header_and_footer_background_color + header_and_footer_text_color + headline_font + link_color + link_hover_color + logo_image_text navbar_background_color + navbar_link_background_color navbar_link_background_hover_color navbar_link_text_color navbar_link_text_hover_color - custom_css_block - logo_image_text - banner_image_text - directory_image_text - default_collection_image_text - default_work_image_text + primary_button_hover_color ] end # rubocop:enable Metrics/MethodLength + + # @return [Array] a list of fields that are related to the banner + def banner_fields + %i[ + banner_image banner_label + ] + end + + def favicon_fields + %i[ + favicon + ] + end + + # @return [Array] a list of fields that are related to the logo + def logo_fields + %i[ + logo_image logo_label + ] + end + + # @return [Array] a list of fields that are related to the directory + def directory_fields + %i[ + directory_image directory_image_label directory_image_alt_text + ] + end + + # @return [Array] a list of fields that are related to default works & collections + def default_image_fields + %i[ + default_collection_image + default_work_image + default_collection_label + default_work_label + ] + end + end # rubocop:enable Metrics/BlockLength + # Required to back a form + def to_key + [] + end + + # Required to back a form (for route determination) + def persisted? + true + end + def site @site ||= Site.instance end @@ -254,6 +305,21 @@ def primary_button_border_color @primary_button_border ||= darken_color(primary_button_hover_color, 0.05) end + # The mouse over color for the border of "primary" buttons + def primary_button_hover_border_color + darken_color(primary_button_border_color, 0.12) + end + + # The color for the border of active "primary" buttons + def primary_button_active_border_color + darken_color(primary_button_border_color, 0.12) + end + + # The color for the border of focused "primary" buttons + def primary_button_focus_border_color + darken_color(primary_button_border_color, 0.25) + end + # The mouse over color for "primary" buttons def primary_button_hover_background_color darken_color(primary_button_hover_color, 0.1) diff --git a/app/presenters/hyrax/collection_presenter_decorator.rb b/app/presenters/hyrax/collection_presenter_decorator.rb index 24e2c66368..9294abe4de 100644 --- a/app/presenters/hyrax/collection_presenter_decorator.rb +++ b/app/presenters/hyrax/collection_presenter_decorator.rb @@ -14,8 +14,14 @@ def terms # OVERRIDE Hyrax - removed size super - [:size] end + + def primary_terms do + %i[title description collection_subtitle] + end end + delegate :collection_subtitle, to: :solr_document + # Add new method to check if a user has permissions to create any works. # This is used to restrict who can deposit new works through collections. # diff --git a/app/services/hyrax/manifest_builder_service_decorator.rb b/app/services/hyrax/manifest_builder_service_decorator.rb index 297cf9f9aa..4c7a82fb75 100644 --- a/app/services/hyrax/manifest_builder_service_decorator.rb +++ b/app/services/hyrax/manifest_builder_service_decorator.rb @@ -22,6 +22,12 @@ def sanitize_value(text) def loof(text) CGI.unescapeHTML(Loofah.fragment(text.to_s).scrub!(:prune).to_s) end + + def sanitize_v3(hash:, presenter:, solr_doc_hits:) + returning_hash = super + returning_hash['viewingHint'] = 'paged' + returning_hash + end end end diff --git a/lib/oai/provider/model_decorator.rb b/lib/oai/provider/model_decorator.rb index 049c1beb87..dd31875468 100644 --- a/lib/oai/provider/model_decorator.rb +++ b/lib/oai/provider/model_decorator.rb @@ -9,28 +9,57 @@ def map_oai_hyku { abstract: :abstract, access_right: :access_right, + accessibility_feature: :accessibility_feature, + accessibility_hazard: :accessibility_hazard, + accessibility_summary: :accessibility_summary, + additional_information: :additional_information, + advisor: :advisor, + alternate_version_id: :alternate_version_id, alternative_title: :alternative_title, + audience: :audience, based_near: :based_near, bibliographic_citation: :bibliographic_citation, + chronology_note: :chronology_note, + committee_member: :committee_member, + contributing_library: :contributing_library, contributor: :contributor, creator: :creator, date_created: :date_created, date_modified: :date_modified, date_uploaded: :date_uploaded, + degree_discipline: :degree_discipline, + degree_grantor: :degree_grantor, + degree_level: :degree_level, + degree_name: :degree_name, + department: :department, depositor: :depositor, description: :description, + discipline: :discipline, + education_level: :education_level, + extent: :extent, + format: :format, + has_model: :has_model, identifier: :identifier, keyword: :keyword, language: :language, + learning_resource_type: :learning_resource_type, + library_catalog_identifier: :library_catalog_identifier, license: :license, + newer_version_id: :newer_version_id, + oai_id: :id, + oer_size: :oer_size, owner: :owner, + previous_version_id: :previous_version_id, publisher: :publisher, + related_item_id: :related_item_id, related_url: :related_url, resource_type: :resource_type, + rights_holder: :rights_holder, rights_notes: :rights_notes, rights_statement: :rights_statement, source: :source, subject: :subject, + table_of_contents: :table_of_contents, title: :title } end From 8479c1859dd7d6199a0518bee43981b3970551a7 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Wed, 10 Apr 2024 15:42:50 -0400 Subject: [PATCH 09/77] Ongoing review of files that were different in 3 or more repositories --- app/models/collection.rb | 9 + app/models/file_set.rb | 8 + app/views/_head_tag_extras.html.erb | 1 + app/views/_logo.html.erb | 20 ++- app/views/hyrax/admin/stats/show.html.erb | 160 +++++++++--------- app/views/hyrax/contact_form/new.html.erb | 4 +- app/views/hyrax/dashboard/_sidebar.html.erb | 2 +- .../collections/_form_share.html.erb | 40 ++--- .../hyrax/my/_collection_action_menu.html.erb | 2 +- app/views/layouts/homepage.html.erb | 3 +- app/views/layouts/hyrax.html.erb | 5 +- spec/factories/accounts.rb | 3 +- spec/models/generic_work_spec.rb | 10 ++ spec/views/_user_util_links.html.erb_spec.rb | 99 ++++++++++- 14 files changed, 246 insertions(+), 120 deletions(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index 246913adaf..055490e481 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -2,6 +2,15 @@ # Generated by hyrax:models class Collection < ActiveFedora::Base + + property :bulkrax_identifier, predicate: ::RDF::URI("https://hykucommons.org/terms/bulkrax_identifier"), multiple: false do |index| + index.as :stored_searchable, :facetable + end + + property :collection_subtitle, predicate: ::RDF::URI('https://hykucommons.org/terms/collection_subtitle') do |index| + index.as :stored_searchable, :facetable + end + include ::Hyrax::CollectionBehavior # You can replace these metadata if they're not suitable include Hyrax::BasicMetadata diff --git a/app/models/file_set.rb b/app/models/file_set.rb index a4f6deacb1..bd4303f00b 100644 --- a/app/models/file_set.rb +++ b/app/models/file_set.rb @@ -2,5 +2,13 @@ # Generated by hyrax:models:install class FileSet < ActiveFedora::Base + property :is_derived, + predicate: ::RDF::URI.intern('https://hykucommons.org/terms/isDerived'), + multiple: false do |index| + index.as :stored_searchable + end + property :bulkrax_identifier, predicate: ::RDF::URI("https://hykucommons.org/terms/bulkrax_identifier"), multiple: false do |index| + index.as :stored_searchable, :facetable + end include ::Hyrax::FileSetBehavior end diff --git a/app/views/_head_tag_extras.html.erb b/app/views/_head_tag_extras.html.erb index d690322c8c..36f4125996 100644 --- a/app/views/_head_tag_extras.html.erb +++ b/app/views/_head_tag_extras.html.erb @@ -1,4 +1,5 @@ <%= render_gtm_head(request.original_url) %> + diff --git a/app/views/_logo.html.erb b/app/views/_logo.html.erb index 08eca88bde..5d786ed7f6 100644 --- a/app/views/_logo.html.erb +++ b/app/views/_logo.html.erb @@ -1,10 +1,12 @@ -<% if logo_image %> - -<% else %> - +<% unless controller.controller_name == 'splash' %> + <% if logo_image %> + + <% else %> + + <% end %> <% end %> diff --git a/app/views/hyrax/admin/stats/show.html.erb b/app/views/hyrax/admin/stats/show.html.erb index 4996fe330e..44713ae8fb 100644 --- a/app/views/hyrax/admin/stats/show.html.erb +++ b/app/views/hyrax/admin/stats/show.html.erb @@ -2,94 +2,94 @@

    Statistics for <%= application_name %>

    <% end %> -
    - - +
    + + - -
    -
    -
    -
    -

    Collections over time

    - <%= - graph_tag('collection-graph', [Hyrax::Statistics::Collections::OverTime.new.points], { - xaxis: { - mode: 'time', - minTickSize: [7, 'day'] - }, - yaxis: { - minTickSize: 1, - tickDecimals: 0, - min: 0 - } - }) - %> -
    + +
    +
    +
    +
    +

    Collections over time

    + <%= + graph_tag('collection-graph', [Hyrax::Statistics::Collections::OverTime.new.points], { + xaxis: { + mode: 'time', + minTickSize: [7, 'day'] + }, + yaxis: { + minTickSize: 1, + tickDecimals: 0, + min: 0 + } + }) + %>
    -
    -
    -
    -

    Works over time

    - <%= - graph_tag('works-graph', [Hyrax::Statistics::Works::OverTime.new.points], { - xaxis: { - mode: 'time', - minTickSize: [7, 'day'] - }, - yaxis: { - minTickSize: 1, - tickDecimals: 0, - min: 0 - } - }) - %> -

    Works by type:

    - <%= - graph_tag('works-by-type', Hyrax::Statistics::Works::ByResourceType.new.query, series: { - pie: { - show: true, - } - }) - %> -
    +
    +
    +
    +
    +

    Works over time

    + <%= + graph_tag('works-graph', [Hyrax::Statistics::Works::OverTime.new.points], { + xaxis: { + mode: 'time', + minTickSize: [7, 'day'] + }, + yaxis: { + minTickSize: 1, + tickDecimals: 0, + min: 0 + } + }) + %> +

    Works by type:

    + <%= + graph_tag('works-by-type', Hyrax::Statistics::Works::ByResourceType.new.query, series: { + pie: { + show: true, + } + }) + %>
    -
    -
    -
    - ... -
    +
    +
    +
    +
    + ...
    -
    -
    -
    - <%= render "hyrax/admin/stats/stats_by_date" %> - <%= render "hyrax/admin/stats/top_data" %> -
    +
    +
    +
    +
    + <%= render "hyrax/admin/stats/stats_by_date" %> + <%= render "hyrax/admin/stats/top_data" %>
    -
    +
    +
    diff --git a/app/views/hyrax/contact_form/new.html.erb b/app/views/hyrax/contact_form/new.html.erb index b1c6d774c6..c79984e05f 100644 --- a/app/views/hyrax/contact_form/new.html.erb +++ b/app/views/hyrax/contact_form/new.html.erb @@ -39,8 +39,8 @@
    - <%= f.label :subject, t('hyrax.contact_form.subject_label'), class: "col-sm-2 col-form-label" %> -
    <%= f.text_field :subject, class: 'form-control', required: true %>
    + <%= negative_label_tag(@captcha, :subject, t('hyrax.contact_form.subject_label'), class: "col-sm-2 col-form-label" %> +
    <%= negative_text_field_tag(@captcha, :subject, class: 'form-control', required: true %>
    diff --git a/app/views/hyrax/dashboard/_sidebar.html.erb b/app/views/hyrax/dashboard/_sidebar.html.erb index 8a3522e07b..5052cc4296 100644 --- a/app/views/hyrax/dashboard/_sidebar.html.erb +++ b/app/views/hyrax/dashboard/_sidebar.html.erb @@ -1,4 +1,4 @@ -<% menu = Hyrax::MenuPresenter.new(self) %> +<% menu = Hyku::MenuPresenter.new(self) %>